























11// Feishu tests cover monitor.cleanup plugin behavior.
2+import type { Server } from "node:http";
23import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
3-import { botNames, botOpenIds, stopFeishuMonitorState, wsClients } from "./monitor.state.js";
4+import {
5+botNames,
6+botOpenIds,
7+FEISHU_HTTP_SERVER_CLOSE_TIMEOUT_MS,
8+httpServers,
9+setFeishuBotIdentityState,
10+stopFeishuMonitorState,
11+wsClients,
12+} from "./monitor.state.js";
413import type { ResolvedFeishuAccount } from "./types.js";
514615const createFeishuWSClientMock = vi.hoisted(() => vi.fn());
@@ -38,6 +47,34 @@ function createWsClient(): MockWsClient {
3847};
3948}
404950+function createHttpServerMock(): {
51+server: Server;
52+close: ReturnType<typeof vi.fn>;
53+closeAllConnections: ReturnType<typeof vi.fn>;
54+finishClose: (error?: Error) => void;
55+} {
56+let closeCallback: ((err?: Error) => void) | undefined;
57+const server = {} as Server;
58+const close = vi.fn((callback?: (err?: Error) => void) => {
59+closeCallback = callback;
60+return server;
61+});
62+const closeAllConnections = vi.fn();
63+server.close = close as unknown as Server["close"];
64+server.closeAllConnections = closeAllConnections;
65+return {
66+ server,
67+ close,
68+ closeAllConnections,
69+finishClose: (error?: Error) => {
70+if (!closeCallback) {
71+throw new Error("expected HTTP server close callback");
72+}
73+closeCallback(error);
74+},
75+};
76+}
77+4178function firstRuntimeError(runtime: { error: ReturnType<typeof vi.fn> }): string {
4279return String(runtime.error.mock.calls[0]?.[0] ?? "");
4380}
@@ -50,9 +87,9 @@ function firstWsCallbacks(): { onError?: (err: Error) => void } {
5087return callbacks as { onError?: (err: Error) => void };
5188}
528953-afterEach(() => {
90+afterEach(async () => {
5491vi.useRealTimers();
55-stopFeishuMonitorState();
92+await stopFeishuMonitorState();
5693vi.clearAllMocks();
5794});
5895@@ -339,7 +376,7 @@ describe("feishu websocket cleanup", () => {
339376expect(errorMessage).not.toContain("secret_token");
340377});
341378342-it("closes targeted websocket clients during stop cleanup", () => {
379+it("closes targeted websocket clients during stop cleanup", async () => {
343380const alphaClient = createWsClient();
344381const betaClient = createWsClient();
345382@@ -350,7 +387,7 @@ describe("feishu websocket cleanup", () => {
350387botNames.set("alpha", "Alpha");
351388botNames.set("beta", "Beta");
352389353-stopFeishuMonitorState("alpha");
390+await stopFeishuMonitorState("alpha");
354391355392expect(alphaClient.close).toHaveBeenCalledTimes(1);
356393expect(betaClient.close).not.toHaveBeenCalled();
@@ -362,7 +399,7 @@ describe("feishu websocket cleanup", () => {
362399expect(botNames.has("beta")).toBe(true);
363400});
364401365-it("closes all websocket clients during global stop cleanup", () => {
402+it("closes all websocket clients during global stop cleanup", async () => {
366403const alphaClient = createWsClient();
367404const betaClient = createWsClient();
368405@@ -373,12 +410,136 @@ describe("feishu websocket cleanup", () => {
373410botNames.set("alpha", "Alpha");
374411botNames.set("beta", "Beta");
375412376-stopFeishuMonitorState();
413+await stopFeishuMonitorState();
377414378415expect(alphaClient.close).toHaveBeenCalledTimes(1);
379416expect(betaClient.close).toHaveBeenCalledTimes(1);
380417expect(wsClients.size).toBe(0);
381418expect(botOpenIds.size).toBe(0);
382419expect(botNames.size).toBe(0);
383420});
421+422+it("keeps targeted HTTP server state until close completes", async () => {
423+const { server, close, closeAllConnections, finishClose } = createHttpServerMock();
424+425+httpServers.set("alpha", server);
426+botOpenIds.set("alpha", "ou_alpha");
427+botNames.set("alpha", "Alpha");
428+429+const stopPromise = stopFeishuMonitorState("alpha");
430+await Promise.resolve();
431+432+expect(close).toHaveBeenCalledTimes(1);
433+expect(httpServers.get("alpha")).toBe(server);
434+expect(botOpenIds.get("alpha")).toBe("ou_alpha");
435+expect(botNames.get("alpha")).toBe("Alpha");
436+437+finishClose();
438+await stopPromise;
439+440+expect(closeAllConnections).not.toHaveBeenCalled();
441+expect(httpServers.has("alpha")).toBe(false);
442+expect(botOpenIds.has("alpha")).toBe(false);
443+expect(botNames.has("alpha")).toBe(false);
444+});
445+446+it("preserves replacement HTTP state after delayed targeted cleanup", async () => {
447+const oldServer = createHttpServerMock();
448+const replacementServer = createHttpServerMock();
449+450+httpServers.set("alpha", oldServer.server);
451+setFeishuBotIdentityState("alpha", { botOpenId: "ou_old", botName: "Old" });
452+453+const stopPromise = stopFeishuMonitorState("alpha");
454+await Promise.resolve();
455+456+setFeishuBotIdentityState("alpha", { botOpenId: "ou_new", botName: "New" });
457+httpServers.set("alpha", replacementServer.server);
458+459+oldServer.finishClose();
460+await stopPromise;
461+462+expect(httpServers.get("alpha")).toBe(replacementServer.server);
463+expect(botOpenIds.get("alpha")).toBe("ou_new");
464+expect(botNames.get("alpha")).toBe("New");
465+466+const cleanupPromise = stopFeishuMonitorState("alpha");
467+await Promise.resolve();
468+replacementServer.finishClose();
469+await cleanupPromise;
470+});
471+472+it("preserves replacement identity written before the replacement HTTP server is tracked", async () => {
473+const oldServer = createHttpServerMock();
474+475+httpServers.set("alpha", oldServer.server);
476+setFeishuBotIdentityState("alpha", { botOpenId: "ou_old", botName: "Old" });
477+478+const stopPromise = stopFeishuMonitorState("alpha");
479+await Promise.resolve();
480+481+setFeishuBotIdentityState("alpha", { botOpenId: "ou_new", botName: "New" });
482+483+oldServer.finishClose();
484+await stopPromise;
485+486+expect(httpServers.has("alpha")).toBe(false);
487+expect(botOpenIds.get("alpha")).toBe("ou_new");
488+expect(botNames.get("alpha")).toBe("New");
489+490+await stopFeishuMonitorState("alpha");
491+});
492+493+it("forces targeted HTTP server cleanup after the close timeout", async () => {
494+vi.useFakeTimers();
495+const { server, close, closeAllConnections } = createHttpServerMock();
496+497+httpServers.set("alpha", server);
498+botOpenIds.set("alpha", "ou_alpha");
499+botNames.set("alpha", "Alpha");
500+501+const stopPromise = stopFeishuMonitorState("alpha");
502+await Promise.resolve();
503+504+expect(close).toHaveBeenCalledTimes(1);
505+expect(httpServers.get("alpha")).toBe(server);
506+507+await vi.advanceTimersByTimeAsync(FEISHU_HTTP_SERVER_CLOSE_TIMEOUT_MS - 1);
508+expect(closeAllConnections).not.toHaveBeenCalled();
509+expect(httpServers.get("alpha")).toBe(server);
510+511+await vi.advanceTimersByTimeAsync(1);
512+await stopPromise;
513+514+expect(closeAllConnections).toHaveBeenCalledTimes(1);
515+expect(httpServers.has("alpha")).toBe(false);
516+expect(botOpenIds.has("alpha")).toBe(false);
517+expect(botNames.has("alpha")).toBe(false);
518+});
519+520+it("preserves replacement HTTP state after delayed global cleanup", async () => {
521+const oldServer = createHttpServerMock();
522+const replacementServer = createHttpServerMock();
523+524+httpServers.set("alpha", oldServer.server);
525+setFeishuBotIdentityState("alpha", { botOpenId: "ou_old", botName: "Old" });
526+527+const stopPromise = stopFeishuMonitorState();
528+await Promise.resolve();
529+530+setFeishuBotIdentityState("alpha", { botOpenId: "ou_new", botName: "New" });
531+httpServers.set("alpha", replacementServer.server);
532+533+oldServer.finishClose();
534+await stopPromise;
535+536+expect(httpServers.get("alpha")).toBe(replacementServer.server);
537+expect(botOpenIds.get("alpha")).toBe("ou_new");
538+expect(botNames.get("alpha")).toBe("New");
539+540+const cleanupPromise = stopFeishuMonitorState("alpha");
541+await Promise.resolve();
542+replacementServer.finishClose();
543+await cleanupPromise;
544+});
384545});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。