

























@@ -8,18 +8,21 @@ import {
8899class FakeWebSocket implements MattermostWebSocketLike {
1010public readonly sent: string[] = [];
11+public pingCalls = 0;
1112public closeCalls = 0;
1213public terminateCalls = 0;
1314private openListeners: Array<() => void> = [];
1415private messageListeners: Array<(data: Buffer) => void | Promise<void>> = [];
16+private pongListeners: Array<(data: Buffer) => void> = [];
1517private closeListeners: Array<(code: number, reason: Buffer) => void> = [];
1618private errorListeners: Array<(err: unknown) => void> = [];
17191820on(event: "open", listener: () => void): void;
1921on(event: "message", listener: (data: Buffer) => void | Promise<void>): void;
22+on(event: "pong", listener: (data: Buffer) => void): void;
2023on(event: "close", listener: (code: number, reason: Buffer) => void): void;
2124on(event: "error", listener: (err: unknown) => void): void;
22-on(event: "open" | "message" | "close" | "error", listener: unknown): void {
25+on(event: "open" | "message" | "pong" | "close" | "error", listener: unknown): void {
2326if (event === "open") {
2427this.openListeners.push(listener as () => void);
2528return;
@@ -28,13 +31,21 @@ class FakeWebSocket implements MattermostWebSocketLike {
2831this.messageListeners.push(listener as (data: Buffer) => void | Promise<void>);
2932return;
3033}
34+if (event === "pong") {
35+this.pongListeners.push(listener as (data: Buffer) => void);
36+return;
37+}
3138if (event === "close") {
3239this.closeListeners.push(listener as (code: number, reason: Buffer) => void);
3340return;
3441}
3542this.errorListeners.push(listener as (err: unknown) => void);
3643}
374445+ping(): void {
46+this.pingCalls++;
47+}
48+3849send(data: string): void {
3950this.sent.push(data);
4051}
@@ -59,6 +70,12 @@ class FakeWebSocket implements MattermostWebSocketLike {
5970}
6071}
617273+emitPong(data = Buffer.alloc(0)): void {
74+for (const listener of this.pongListeners) {
75+listener(data);
76+}
77+}
78+6279emitClose(code: number, reason = ""): void {
6380const buffer = Buffer.from(reason, "utf8");
6481for (const listener of this.closeListeners) {
@@ -282,6 +299,82 @@ describe("mattermost websocket monitor", () => {
282299vi.useRealTimers();
283300});
284301302+it("continues protocol keepalive when Mattermost responds with pong", async () => {
303+vi.useFakeTimers();
304+const socket = new FakeWebSocket();
305+const connectOnce = createMattermostConnectOnce({
306+wsUrl: "wss://example.invalid/api/v4/websocket",
307+botToken: "token",
308+runtime: testRuntime(),
309+nextSeq: () => 1,
310+onPosted: async () => {},
311+webSocketFactory: () => socket,
312+pingIntervalMs: 100,
313+pongTimeoutMs: 25,
314+});
315+316+const connected = connectOnce();
317+socket.emitOpen();
318+319+await vi.advanceTimersByTimeAsync(100);
320+expect(socket.pingCalls).toBe(1);
321+322+socket.emitPong();
323+await vi.advanceTimersByTimeAsync(25);
324+expect(socket.terminateCalls).toBe(0);
325+326+await vi.advanceTimersByTimeAsync(75);
327+expect(socket.pingCalls).toBe(2);
328+329+socket.emitClose(1000);
330+await connected;
331+vi.useRealTimers();
332+});
333+334+it("terminates silent websocket drops when Mattermost misses pong timeout", async () => {
335+vi.useFakeTimers();
336+const socket = new FakeWebSocket();
337+const runtime = testRuntime();
338+let pollCount = 0;
339+const connectOnce = createMattermostConnectOnce({
340+wsUrl: "wss://example.invalid/api/v4/websocket",
341+botToken: "token",
342+ runtime,
343+nextSeq: () => 1,
344+onPosted: async () => {},
345+webSocketFactory: () => socket,
346+getBotUpdateAt: async () => {
347+pollCount++;
348+return 1000;
349+},
350+healthCheckIntervalMs: 100,
351+pingIntervalMs: 50,
352+pongTimeoutMs: 25,
353+});
354+355+const connected = connectOnce();
356+socket.emitOpen();
357+358+await vi.advanceTimersByTimeAsync(0);
359+expect(pollCount).toBe(1);
360+361+await vi.advanceTimersByTimeAsync(50);
362+expect(socket.pingCalls).toBe(1);
363+expect(socket.terminateCalls).toBe(0);
364+365+await vi.advanceTimersByTimeAsync(25);
366+expect(socket.terminateCalls).toBe(1);
367+expect(runtime.error).toHaveBeenCalledWith("mattermost websocket pong timeout — reconnecting");
368+369+await vi.advanceTimersByTimeAsync(500);
370+expect(socket.pingCalls).toBe(1);
371+expect(pollCount).toBe(1);
372+373+socket.emitClose(1006);
374+await connected;
375+vi.useRealTimers();
376+});
377+285378it("does not terminate when getBotUpdateAt throws", async () => {
286379vi.useFakeTimers();
287380const socket = new FakeWebSocket();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。