






















@@ -136,6 +136,165 @@ describe("feishu websocket cleanup", () => {
136136expect(errorMessage).toContain("appSecret=[redacted]");
137137});
138138139+it("recreates the websocket client after sdk reconnect exhaustion", async () => {
140+vi.useFakeTimers();
141+const exhaustedClient = createWsClient();
142+const recoveredClient = createWsClient();
143+createFeishuWSClientMock
144+.mockResolvedValueOnce(exhaustedClient)
145+.mockResolvedValueOnce(recoveredClient);
146+147+const abortController = new AbortController();
148+const runtime = {
149+log: vi.fn(),
150+error: vi.fn(),
151+exit: vi.fn(),
152+};
153+const accountId = "exhausted";
154+botOpenIds.set(accountId, "ou_exhausted");
155+botNames.set(accountId, "Exhausted");
156+157+const monitorPromise = monitorWebSocket({
158+account: createAccount(accountId),
159+ accountId,
160+ runtime,
161+abortSignal: abortController.signal,
162+eventDispatcher: {} as never,
163+});
164+165+await vi.waitFor(() => {
166+expect(exhaustedClient.start).toHaveBeenCalledTimes(1);
167+expect(wsClients.get(accountId)).toBe(exhaustedClient);
168+});
169+170+const callbacks = createFeishuWSClientMock.mock.calls[0]?.[1] as
171+| { onError?: (err: Error) => void }
172+| undefined;
173+callbacks?.onError?.(
174+new Error("WebSocket reconnect exhausted after 3 attempts\nBearer token_abc"),
175+);
176+177+await vi.waitFor(() => {
178+expect(exhaustedClient.close).toHaveBeenCalledTimes(1);
179+expect(wsClients.has(accountId)).toBe(false);
180+});
181+expect(botOpenIds.get(accountId)).toBe("ou_exhausted");
182+expect(botNames.get(accountId)).toBe("Exhausted");
183+184+await vi.advanceTimersByTimeAsync(1_000);
185+186+await vi.waitFor(() => {
187+expect(recoveredClient.start).toHaveBeenCalledTimes(1);
188+expect(wsClients.get(accountId)).toBe(recoveredClient);
189+});
190+191+abortController.abort();
192+await monitorPromise;
193+194+expect(createFeishuWSClientMock).toHaveBeenCalledTimes(2);
195+expect(recoveredClient.close).toHaveBeenCalledTimes(1);
196+expect(botOpenIds.has(accountId)).toBe(false);
197+expect(botNames.has(accountId)).toBe(false);
198+const errorMessage = String(runtime.error.mock.calls[0]?.[0] ?? "");
199+expect(errorMessage).toContain("WebSocket connection ended, recreating client in 1000ms");
200+expect(errorMessage).toContain("Bearer [redacted]");
201+expect(errorMessage).not.toContain("\n");
202+expect(errorMessage).not.toContain("token_abc");
203+});
204+205+it("keeps the websocket client alive after recoverable sdk callback errors", async () => {
206+vi.useFakeTimers();
207+const wsClient = createWsClient();
208+createFeishuWSClientMock.mockResolvedValueOnce(wsClient);
209+210+const abortController = new AbortController();
211+const runtime = {
212+log: vi.fn(),
213+error: vi.fn(),
214+exit: vi.fn(),
215+};
216+const accountId = "recoverable-callback";
217+218+const monitorPromise = monitorWebSocket({
219+account: createAccount(accountId),
220+ accountId,
221+ runtime,
222+abortSignal: abortController.signal,
223+eventDispatcher: {} as never,
224+});
225+226+await vi.waitFor(() => {
227+expect(wsClient.start).toHaveBeenCalledTimes(1);
228+expect(wsClients.get(accountId)).toBe(wsClient);
229+});
230+231+const callbacks = createFeishuWSClientMock.mock.calls[0]?.[1] as
232+| { onError?: (err: Error) => void }
233+| undefined;
234+callbacks?.onError?.(new Error("temporary callback failure\nBearer token_abc"));
235+236+await vi.advanceTimersByTimeAsync(1_000);
237+238+expect(createFeishuWSClientMock).toHaveBeenCalledTimes(1);
239+expect(wsClient.close).not.toHaveBeenCalled();
240+expect(wsClients.get(accountId)).toBe(wsClient);
241+const errorMessage = String(runtime.error.mock.calls[0]?.[0] ?? "");
242+expect(errorMessage).toContain("WebSocket SDK reported recoverable error");
243+expect(errorMessage).toContain("Bearer [redacted]");
244+expect(errorMessage).not.toContain("\n");
245+expect(errorMessage).not.toContain("token_abc");
246+247+abortController.abort();
248+await monitorPromise;
249+250+expect(createFeishuWSClientMock).toHaveBeenCalledTimes(1);
251+expect(wsClient.close).toHaveBeenCalledTimes(1);
252+});
253+254+it("clears identity without recreating a websocket when aborted during reconnect backoff", async () => {
255+vi.useFakeTimers();
256+const exhaustedClient = createWsClient();
257+createFeishuWSClientMock.mockResolvedValueOnce(exhaustedClient);
258+259+const abortController = new AbortController();
260+const accountId = "abort-backoff";
261+botOpenIds.set(accountId, "ou_abort");
262+botNames.set(accountId, "Abort");
263+264+const monitorPromise = monitorWebSocket({
265+account: createAccount(accountId),
266+ accountId,
267+runtime: {
268+log: vi.fn(),
269+error: vi.fn(),
270+exit: vi.fn(),
271+},
272+abortSignal: abortController.signal,
273+eventDispatcher: {} as never,
274+});
275+276+await vi.waitFor(() => {
277+expect(exhaustedClient.start).toHaveBeenCalledTimes(1);
278+});
279+280+const callbacks = createFeishuWSClientMock.mock.calls[0]?.[1] as
281+| { onError?: (err: Error) => void }
282+| undefined;
283+callbacks?.onError?.(new Error("WebSocket reconnect exhausted after 3 attempts"));
284+285+await vi.waitFor(() => {
286+expect(exhaustedClient.close).toHaveBeenCalledTimes(1);
287+});
288+289+abortController.abort();
290+await monitorPromise;
291+292+expect(createFeishuWSClientMock).toHaveBeenCalledTimes(1);
293+expect(wsClients.has(accountId)).toBe(false);
294+expect(botOpenIds.has(accountId)).toBe(false);
295+expect(botNames.has(accountId)).toBe(false);
296+});
297+139298it("redacts websocket close errors during abort cleanup", async () => {
140299const wsClient = createWsClient();
141300wsClient.close.mockImplementationOnce(() => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。