
























@@ -38,6 +38,7 @@ function createWsClient(): MockWsClient {
3838}
39394040afterEach(() => {
41+vi.useRealTimers();
4142stopFeishuMonitorState();
4243vi.clearAllMocks();
4344});
@@ -79,6 +80,98 @@ describe("feishu websocket cleanup", () => {
7980expect(botNames.has(accountId)).toBe(false);
8081});
818283+it("retries with backoff after websocket start rejects", async () => {
84+vi.useFakeTimers();
85+const failedClient = createWsClient();
86+failedClient.start.mockRejectedValueOnce(
87+new Error("connect failed\nAuthorization: Bearer token_abc appSecret=secret_abc"),
88+);
89+const recoveredClient = createWsClient();
90+createFeishuWSClientMock
91+.mockResolvedValueOnce(failedClient)
92+.mockResolvedValueOnce(recoveredClient);
93+94+const abortController = new AbortController();
95+const runtime = {
96+log: vi.fn(),
97+error: vi.fn(),
98+exit: vi.fn(),
99+};
100+const accountId = "retry";
101+102+const monitorPromise = monitorWebSocket({
103+account: createAccount(accountId),
104+ accountId,
105+ runtime,
106+abortSignal: abortController.signal,
107+eventDispatcher: {} as never,
108+});
109+110+await vi.waitFor(() => {
111+expect(failedClient.start).toHaveBeenCalledTimes(1);
112+expect(failedClient.close).toHaveBeenCalledTimes(1);
113+expect(wsClients.has(accountId)).toBe(false);
114+});
115+116+await vi.advanceTimersByTimeAsync(1_000);
117+118+await vi.waitFor(() => {
119+expect(recoveredClient.start).toHaveBeenCalledTimes(1);
120+expect(wsClients.get(accountId)).toBe(recoveredClient);
121+});
122+123+abortController.abort();
124+await monitorPromise;
125+126+expect(createFeishuWSClientMock).toHaveBeenCalledTimes(2);
127+expect(recoveredClient.close).toHaveBeenCalledTimes(1);
128+expect(runtime.error).toHaveBeenCalledWith(
129+expect.stringContaining("WebSocket start failed, retrying in 1000ms"),
130+);
131+const errorMessage = String(runtime.error.mock.calls[0]?.[0] ?? "");
132+expect(errorMessage).not.toContain("\n");
133+expect(errorMessage).not.toContain("token_abc");
134+expect(errorMessage).not.toContain("secret_abc");
135+expect(errorMessage).toContain("Authorization: Bearer [redacted]");
136+expect(errorMessage).toContain("appSecret=[redacted]");
137+});
138+139+it("redacts websocket close errors during abort cleanup", async () => {
140+const wsClient = createWsClient();
141+wsClient.close.mockImplementationOnce(() => {
142+throw new Error("close failed\naccess_token=secret_token");
143+});
144+createFeishuWSClientMock.mockReturnValue(wsClient);
145+146+const abortController = new AbortController();
147+const runtime = {
148+log: vi.fn(),
149+error: vi.fn(),
150+exit: vi.fn(),
151+};
152+153+const monitorPromise = monitorWebSocket({
154+account: createAccount("close-error"),
155+accountId: "close-error",
156+ runtime,
157+abortSignal: abortController.signal,
158+eventDispatcher: {} as never,
159+});
160+161+await vi.waitFor(() => {
162+expect(wsClient.start).toHaveBeenCalledTimes(1);
163+});
164+165+abortController.abort();
166+await monitorPromise;
167+168+const errorMessage = String(runtime.error.mock.calls[0]?.[0] ?? "");
169+expect(errorMessage).toContain("error closing WebSocket client");
170+expect(errorMessage).toContain("access_token=[redacted]");
171+expect(errorMessage).not.toContain("\n");
172+expect(errorMessage).not.toContain("secret_token");
173+});
174+82175it("closes targeted websocket clients during stop cleanup", () => {
83176const alphaClient = createWsClient();
84177const betaClient = createWsClient();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。