

























@@ -0,0 +1,142 @@
1+import { EventEmitter } from "node:events";
2+import type { ChannelGatewayContext } from "openclaw/plugin-sdk/channel-contract";
3+import { beforeEach, describe, expect, it, vi } from "vitest";
4+import type { ResolvedClickClackAccount } from "./types.js";
5+6+class FakeSocket extends EventEmitter {
7+close = vi.fn(() => {
8+this.emit("close");
9+});
10+}
11+12+const mocks = vi.hoisted(() => ({
13+client: {
14+me: vi.fn(),
15+events: vi.fn(),
16+websocket: vi.fn(),
17+channelMessages: vi.fn(),
18+directMessages: vi.fn(),
19+thread: vi.fn(),
20+},
21+handleClickClackInbound: vi.fn(),
22+resolveWorkspaceId: vi.fn(),
23+}));
24+25+vi.mock("./http-client.js", () => ({
26+createClickClackClient: vi.fn(() => mocks.client),
27+}));
28+29+vi.mock("./inbound.js", () => ({
30+handleClickClackInbound: mocks.handleClickClackInbound,
31+}));
32+33+vi.mock("./resolve.js", () => ({
34+resolveWorkspaceId: mocks.resolveWorkspaceId,
35+}));
36+37+import { startClickClackGatewayAccount } from "./gateway.js";
38+39+function createGatewayContext(
40+abortSignal: AbortSignal,
41+): ChannelGatewayContext<ResolvedClickClackAccount> {
42+const setStatus = vi.fn();
43+const log = { warn: vi.fn(), info: vi.fn(), error: vi.fn(), debug: vi.fn() };
44+return {
45+cfg: {
46+channels: {
47+clickclack: {
48+baseUrl: "https://clickclack.example",
49+token: "test-token",
50+workspace: "main",
51+reconnectMs: 1,
52+},
53+},
54+} as ChannelGatewayContext<ResolvedClickClackAccount>["cfg"],
55+accountId: "default",
56+account: {} as ResolvedClickClackAccount,
57+runtime: {} as ChannelGatewayContext<ResolvedClickClackAccount>["runtime"],
58+ abortSignal,
59+ log,
60+getStatus: () =>
61+({ accountId: "default" }) as ReturnType<
62+ChannelGatewayContext<ResolvedClickClackAccount>["getStatus"]
63+>,
64+ setStatus,
65+};
66+}
67+68+describe("ClickClack gateway", () => {
69+beforeEach(() => {
70+vi.clearAllMocks();
71+mocks.client.me.mockResolvedValue({
72+id: "bot-user",
73+display_name: "Bot",
74+handle: "bot",
75+avatar_url: "",
76+created_at: "2026-01-01T00:00:00.000Z",
77+});
78+mocks.client.events.mockResolvedValue([]);
79+mocks.resolveWorkspaceId.mockResolvedValue("workspace-1");
80+mocks.client.channelMessages.mockResolvedValue([
81+{
82+id: "msg-1",
83+workspace_id: "workspace-1",
84+channel_id: "chan-1",
85+author_id: "human-1",
86+thread_root_id: "msg-1",
87+body: "hello",
88+body_format: "markdown",
89+created_at: "2026-01-01T00:00:00.000Z",
90+author: {
91+id: "human-1",
92+kind: "human",
93+display_name: "Human",
94+handle: "human",
95+avatar_url: "",
96+created_at: "2026-01-01T00:00:00.000Z",
97+},
98+},
99+]);
100+});
101+102+it("skips malformed websocket frames without stopping the monitor", async () => {
103+const socket = new FakeSocket();
104+mocks.client.websocket.mockReturnValue(socket);
105+const abort = new AbortController();
106+const ctx = createGatewayContext(abort.signal);
107+let runError: unknown;
108+const run = startClickClackGatewayAccount(ctx).catch((error: unknown) => {
109+runError = error;
110+});
111+112+await vi.waitFor(() => expect(mocks.client.websocket).toHaveBeenCalledTimes(1));
113+114+socket.emit("message", Buffer.from("{not json"));
115+await new Promise((resolve) => setImmediate(resolve));
116+expect(runError).toBeUndefined();
117+expect(ctx.log?.warn).toHaveBeenCalledWith(
118+"[default] skipped malformed ClickClack websocket event",
119+);
120+121+socket.emit(
122+"message",
123+Buffer.from(
124+JSON.stringify({
125+id: "evt-1",
126+cursor: "cursor-1",
127+type: "message.created",
128+workspace_id: "workspace-1",
129+channel_id: "chan-1",
130+seq: 2,
131+created_at: "2026-01-01T00:00:00.000Z",
132+payload: { message_id: "msg-1", author_id: "human-1" },
133+}),
134+),
135+);
136+137+await vi.waitFor(() => expect(mocks.handleClickClackInbound).toHaveBeenCalledTimes(1));
138+abort.abort();
139+await run;
140+expect(runError).toBeUndefined();
141+});
142+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。