Files
openclaw/extensions/feishu/src/monitor.cleanup.test.ts
Lin Z bd4237c16c fix(feishu): close WebSocket connections on monitor stop (#52844)
* fix(feishu): close WebSocket connections on monitor stop/abort

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(feishu): add WebSocket cleanup tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(feishu): close WebSocket connections on monitor stop (#52844) (thanks @schumilin)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-03-25 08:32:21 -07:00

123 lines
3.5 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { botNames, botOpenIds, stopFeishuMonitorState, wsClients } from "./monitor.state.js";
import type { ResolvedFeishuAccount } from "./types.js";
const createFeishuWSClientMock = vi.hoisted(() => vi.fn());
vi.mock("./client.js", () => ({
createFeishuWSClient: createFeishuWSClientMock,
}));
import { monitorWebSocket } from "./monitor.transport.js";
type MockWsClient = {
start: ReturnType<typeof vi.fn>;
close: ReturnType<typeof vi.fn>;
};
function createAccount(accountId: string): ResolvedFeishuAccount {
return {
accountId,
enabled: true,
configured: true,
appId: `cli_${accountId}`,
appSecret: `secret_${accountId}`, // pragma: allowlist secret
domain: "feishu",
config: {
enabled: true,
connectionMode: "websocket",
},
} as ResolvedFeishuAccount;
}
function createWsClient(): MockWsClient {
return {
start: vi.fn(),
close: vi.fn(),
};
}
afterEach(() => {
stopFeishuMonitorState();
vi.clearAllMocks();
});
describe("feishu websocket cleanup", () => {
it("closes the websocket client when the monitor aborts", async () => {
const wsClient = createWsClient();
createFeishuWSClientMock.mockReturnValue(wsClient);
const abortController = new AbortController();
const accountId = "alpha";
botOpenIds.set(accountId, "ou_alpha");
botNames.set(accountId, "Alpha");
const monitorPromise = monitorWebSocket({
account: createAccount(accountId),
accountId,
runtime: {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
},
abortSignal: abortController.signal,
eventDispatcher: {} as never,
});
expect(wsClient.start).toHaveBeenCalledTimes(1);
expect(wsClients.get(accountId)).toBe(wsClient);
abortController.abort();
await monitorPromise;
expect(wsClient.close).toHaveBeenCalledTimes(1);
expect(wsClients.has(accountId)).toBe(false);
expect(botOpenIds.has(accountId)).toBe(false);
expect(botNames.has(accountId)).toBe(false);
});
it("closes targeted websocket clients during stop cleanup", () => {
const alphaClient = createWsClient();
const betaClient = createWsClient();
wsClients.set("alpha", alphaClient as never);
wsClients.set("beta", betaClient as never);
botOpenIds.set("alpha", "ou_alpha");
botOpenIds.set("beta", "ou_beta");
botNames.set("alpha", "Alpha");
botNames.set("beta", "Beta");
stopFeishuMonitorState("alpha");
expect(alphaClient.close).toHaveBeenCalledTimes(1);
expect(betaClient.close).not.toHaveBeenCalled();
expect(wsClients.has("alpha")).toBe(false);
expect(wsClients.has("beta")).toBe(true);
expect(botOpenIds.has("alpha")).toBe(false);
expect(botOpenIds.has("beta")).toBe(true);
expect(botNames.has("alpha")).toBe(false);
expect(botNames.has("beta")).toBe(true);
});
it("closes all websocket clients during global stop cleanup", () => {
const alphaClient = createWsClient();
const betaClient = createWsClient();
wsClients.set("alpha", alphaClient as never);
wsClients.set("beta", betaClient as never);
botOpenIds.set("alpha", "ou_alpha");
botOpenIds.set("beta", "ou_beta");
botNames.set("alpha", "Alpha");
botNames.set("beta", "Beta");
stopFeishuMonitorState();
expect(alphaClient.close).toHaveBeenCalledTimes(1);
expect(betaClient.close).toHaveBeenCalledTimes(1);
expect(wsClients.size).toBe(0);
expect(botOpenIds.size).toBe(0);
expect(botNames.size).toBe(0);
});
});