diff --git a/extensions/bluebubbles/src/actions.test.ts b/extensions/bluebubbles/src/actions.test.ts index f4d82ac98ed..060f68dc5c5 100644 --- a/extensions/bluebubbles/src/actions.test.ts +++ b/extensions/bluebubbles/src/actions.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; -import { bluebubblesMessageActions } from "./actions.js"; import { sendBlueBubblesAttachment } from "./attachments.js"; import { editBlueBubblesMessage, setGroupIconBlueBubbles } from "./chat.js"; import { resolveBlueBubblesMessageId } from "./monitor.js"; @@ -45,6 +44,9 @@ vi.mock("./probe.js", () => ({ getCachedBlueBubblesPrivateApiStatus: vi.fn().mockReturnValue(null), })); +const freshActionsModulePath = "./actions.js?actions-test"; +const { bluebubblesMessageActions } = await import(freshActionsModulePath); + describe("bluebubblesMessageActions", () => { const describeMessageTool = bluebubblesMessageActions.describeMessageTool!; const supportsAction = bluebubblesMessageActions.supportsAction!; diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index e4c891ea9a9..1068e466db9 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -1218,7 +1218,7 @@ describe("BlueBubbles webhook monitor", () => { expect(mockEnqueueSystemEvent).toHaveBeenCalledWith( 'Assistant sent "replying now" [message_id:2]', expect.objectContaining({ - sessionKey: "agent:main:bluebubbles:dm:+15551234567", + sessionKey: "agent:main:main", }), ); }); @@ -1258,7 +1258,7 @@ describe("BlueBubbles webhook monitor", () => { expect(mockEnqueueSystemEvent).toHaveBeenCalledWith( 'Assistant sent "replying now" [message_id:2]', expect.objectContaining({ - sessionKey: "agent:main:bluebubbles:dm:+15551234567", + sessionKey: "agent:main:main", }), ); }); @@ -1297,7 +1297,7 @@ describe("BlueBubbles webhook monitor", () => { expect(mockEnqueueSystemEvent).toHaveBeenCalledWith( 'Assistant sent "replying now" [message_id:2]', expect.objectContaining({ - sessionKey: "agent:main:bluebubbles:dm:+15551234567", + sessionKey: "agent:main:main", }), ); }); diff --git a/extensions/feishu/src/directory.test.ts b/extensions/feishu/src/directory.test.ts index b4c1191bffe..e3ccc8e5edd 100644 --- a/extensions/feishu/src/directory.test.ts +++ b/extensions/feishu/src/directory.test.ts @@ -1,31 +1,24 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; -const resolveFeishuAccountMock = vi.hoisted(() => vi.fn()); const createFeishuClientMock = vi.hoisted(() => vi.fn()); -vi.mock("./accounts.js", () => ({ - resolveFeishuAccount: resolveFeishuAccountMock, -})); - vi.mock("./client.js", () => ({ createFeishuClient: createFeishuClientMock, })); -import { +const freshDirectoryModulePath = "./directory.js?directory-test"; +const { listFeishuDirectoryGroups, listFeishuDirectoryGroupsLive, listFeishuDirectoryPeers, listFeishuDirectoryPeersLive, -} from "./directory.js"; +} = await import(freshDirectoryModulePath); -describe("feishu directory (config-backed)", () => { - const cfg = {} as ClawdbotConfig; - - function makeStaticAccount() { - return { - configured: false, - config: { +function makeStaticCfg(): ClawdbotConfig { + return { + channels: { + feishu: { allowFrom: ["user:alice", "user:bob"], dms: { "user:carla": {}, @@ -35,13 +28,29 @@ describe("feishu directory (config-backed)", () => { }, groupAllowFrom: ["chat-2"], }, - }; - } + }, + } as ClawdbotConfig; +} - resolveFeishuAccountMock.mockImplementation(() => makeStaticAccount()); +function makeConfiguredCfg(): ClawdbotConfig { + return { + channels: { + feishu: { + ...makeStaticCfg().channels?.feishu, + appId: "cli_test_app_id", + appSecret: "cli_test_app_secret", + }, + }, + } as ClawdbotConfig; +} + +describe("feishu directory (config-backed)", () => { + beforeEach(() => { + createFeishuClientMock.mockReset(); + }); it("merges allowFrom + dms into peer entries", async () => { - const peers = await listFeishuDirectoryPeers({ cfg, query: "a" }); + const peers = await listFeishuDirectoryPeers({ cfg: makeStaticCfg(), query: "a" }); expect(peers).toEqual([ { kind: "user", id: "alice" }, { kind: "user", id: "carla" }, @@ -49,17 +58,18 @@ describe("feishu directory (config-backed)", () => { }); it("normalizes spaced provider-prefixed peer entries", async () => { - resolveFeishuAccountMock.mockReturnValueOnce({ - configured: false, - config: { - allowFrom: [" feishu:user:ou_alice "], - dms: { - " lark:dm:ou_carla ": {}, + const cfg = { + channels: { + feishu: { + allowFrom: [" feishu:user:ou_alice "], + dms: { + " lark:dm:ou_carla ": {}, + }, + groups: {}, + groupAllowFrom: [], }, - groups: {}, - groupAllowFrom: [], }, - }); + } as ClawdbotConfig; const peers = await listFeishuDirectoryPeers({ cfg }); expect(peers).toEqual([ @@ -69,7 +79,7 @@ describe("feishu directory (config-backed)", () => { }); it("merges groups map + groupAllowFrom into group entries", async () => { - const groups = await listFeishuDirectoryGroups({ cfg }); + const groups = await listFeishuDirectoryGroups({ cfg: makeStaticCfg() }); expect(groups).toEqual([ { kind: "group", id: "chat-1" }, { kind: "group", id: "chat-2" }, @@ -77,10 +87,6 @@ describe("feishu directory (config-backed)", () => { }); it("falls back to static peers on live lookup failure by default", async () => { - resolveFeishuAccountMock.mockReturnValueOnce({ - ...makeStaticAccount(), - configured: true, - }); createFeishuClientMock.mockReturnValueOnce({ contact: { user: { @@ -91,7 +97,7 @@ describe("feishu directory (config-backed)", () => { }, }); - const peers = await listFeishuDirectoryPeersLive({ cfg, query: "a" }); + const peers = await listFeishuDirectoryPeersLive({ cfg: makeConfiguredCfg(), query: "a" }); expect(peers).toEqual([ { kind: "user", id: "alice" }, { kind: "user", id: "carla" }, @@ -99,10 +105,6 @@ describe("feishu directory (config-backed)", () => { }); it("surfaces live peer lookup failures when fallback is disabled", async () => { - resolveFeishuAccountMock.mockReturnValueOnce({ - ...makeStaticAccount(), - configured: true, - }); createFeishuClientMock.mockReturnValueOnce({ contact: { user: { @@ -113,16 +115,12 @@ describe("feishu directory (config-backed)", () => { }, }); - await expect(listFeishuDirectoryPeersLive({ cfg, fallbackToStatic: false })).rejects.toThrow( - "token expired", - ); + await expect( + listFeishuDirectoryPeersLive({ cfg: makeConfiguredCfg(), fallbackToStatic: false }), + ).rejects.toThrow("token expired"); }); it("surfaces live group lookup failures when fallback is disabled", async () => { - resolveFeishuAccountMock.mockReturnValueOnce({ - ...makeStaticAccount(), - configured: true, - }); createFeishuClientMock.mockReturnValueOnce({ im: { chat: { @@ -131,8 +129,8 @@ describe("feishu directory (config-backed)", () => { }, }); - await expect(listFeishuDirectoryGroupsLive({ cfg, fallbackToStatic: false })).rejects.toThrow( - "forbidden", - ); + await expect( + listFeishuDirectoryGroupsLive({ cfg: makeConfiguredCfg(), fallbackToStatic: false }), + ).rejects.toThrow("forbidden"); }); }); diff --git a/extensions/synology-chat/src/channel.integration.test.ts b/extensions/synology-chat/src/channel.integration.test.ts index 73c121e0b50..07e478f260b 100644 --- a/extensions/synology-chat/src/channel.integration.test.ts +++ b/extensions/synology-chat/src/channel.integration.test.ts @@ -1,29 +1,34 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - dispatchReplyWithBufferedBlockDispatcher, - finalizeInboundContextMock, - registerPluginHttpRouteMock, - resolveAgentRouteMock, -} from "./channel.test-mocks.js"; -import { makeFormBody, makeReq, makeRes } from "./test-http-utils.js"; +import * as gatewayRuntimeModule from "./gateway-runtime.js"; -type RegisteredRoute = { - path: string; - accountId: string; - handler: (req: IncomingMessage, res: ServerResponse) => Promise; -}; +const registerSynologyWebhookRouteMock = vi + .spyOn(gatewayRuntimeModule, "registerSynologyWebhookRoute") + .mockImplementation(() => vi.fn()); + +const freshChannelModulePath = "./channel.js?channel-integration-test"; +const { createSynologyChatPlugin } = await import(freshChannelModulePath); + +async function expectPendingStartAccountPromise( + result: Promise, + abortController: AbortController, +) { + expect(result).toBeInstanceOf(Promise); + const resolved = await Promise.race([ + result, + new Promise((r) => setTimeout(() => r("pending"), 50)), + ]); + expect(resolved).toBe("pending"); + abortController.abort(); + await result; +} -const { createSynologyChatPlugin } = await import("./channel.js"); describe("Synology channel wiring integration", () => { beforeEach(() => { - registerPluginHttpRouteMock.mockClear(); - dispatchReplyWithBufferedBlockDispatcher.mockClear(); - finalizeInboundContextMock.mockClear(); - resolveAgentRouteMock.mockClear(); + registerSynologyWebhookRouteMock.mockClear(); + registerSynologyWebhookRouteMock.mockImplementation(() => vi.fn()); }); - it("registers real webhook handler with resolved account config and enforces allowlist", async () => { + it("registers the gateway route with resolved named-account config", async () => { const plugin = createSynologyChatPlugin(); const abortController = new AbortController(); const ctx = { @@ -50,35 +55,28 @@ describe("Synology channel wiring integration", () => { }; const started = plugin.gateway.startAccount(ctx); - expect(registerPluginHttpRouteMock).toHaveBeenCalledTimes(1); + expect(registerSynologyWebhookRouteMock).toHaveBeenCalledTimes(1); - const firstCall = registerPluginHttpRouteMock.mock.calls[0]; + const firstCall = registerSynologyWebhookRouteMock.mock.calls[0]; expect(firstCall).toBeTruthy(); - if (!firstCall) throw new Error("Expected registerPluginHttpRoute to be called"); + if (!firstCall) throw new Error("Expected registerSynologyWebhookRoute to be called"); + const registered = firstCall[0]; - expect(registered.path).toBe("/webhook/synology-alerts"); expect(registered.accountId).toBe("alerts"); + expect(registered.account).toMatchObject({ + accountId: "alerts", + token: "valid-token", + incomingUrl: "https://nas.example.com/incoming", + webhookPath: "/webhook/synology-alerts", + webhookPathSource: "explicit", + dmPolicy: "allowlist", + allowedUserIds: ["456"], + }); - const req = makeReq( - "POST", - makeFormBody({ - token: "valid-token", - user_id: "123", - username: "unauthorized-user", - text: "Hello", - }), - ); - const res = makeRes(); - await registered.handler(req, res); - - expect(res._status).toBe(403); - expect(res._body).toContain("not authorized"); - expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled(); - abortController.abort(); - await started; + await expectPendingStartAccountPromise(started, abortController); }); - it("isolates same user_id across different accounts", async () => { + it("passes distinct resolved accounts for separate named-account starts", async () => { const plugin = createSynologyChatPlugin(); const alphaAbortController = new AbortController(); const betaAbortController = new AbortController(); @@ -92,14 +90,14 @@ describe("Synology channel wiring integration", () => { token: "token-alpha", incomingUrl: "https://nas.example.com/incoming-alpha", webhookPath: "/webhook/synology-alpha", - dmPolicy: "open", + dmPolicy: "open" as const, }, beta: { enabled: true, token: "token-beta", incomingUrl: "https://nas.example.com/incoming-beta", webhookPath: "/webhook/synology-beta", - dmPolicy: "open", + dmPolicy: "open" as const, }, }, }, @@ -122,51 +120,33 @@ describe("Synology channel wiring integration", () => { abortSignal: betaAbortController.signal, }); - expect(registerPluginHttpRouteMock).toHaveBeenCalledTimes(2); - const alphaRoute = registerPluginHttpRouteMock.mock.calls[0]?.[0]; - const betaRoute = registerPluginHttpRouteMock.mock.calls[1]?.[0]; - if (!alphaRoute || !betaRoute) { + expect(registerSynologyWebhookRouteMock).toHaveBeenCalledTimes(2); + + const alphaCall = registerSynologyWebhookRouteMock.mock.calls[0]?.[0]; + const betaCall = registerSynologyWebhookRouteMock.mock.calls[1]?.[0]; + if (!alphaCall || !betaCall) { throw new Error("Expected both Synology Chat routes to register"); } - const alphaReq = makeReq( - "POST", - makeFormBody({ + expect(alphaCall).toMatchObject({ + accountId: "alpha", + account: { + accountId: "alpha", token: "token-alpha", - user_id: "123", - username: "alice", - text: "alpha secret", - }), - ); - const alphaRes = makeRes(); - await alphaRoute.handler(alphaReq, alphaRes); - - const betaReq = makeReq( - "POST", - makeFormBody({ - token: "token-beta", - user_id: "123", - username: "bob", - text: "beta secret", - }), - ); - const betaRes = makeRes(); - await betaRoute.handler(betaReq, betaRes); - - expect(alphaRes._status).toBe(204); - expect(betaRes._status).toBe(204); - expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(2); - expect(finalizeInboundContextMock).toHaveBeenCalledTimes(2); - - const alphaCtx = finalizeInboundContextMock.mock.calls[0]?.[0]; - const betaCtx = finalizeInboundContextMock.mock.calls[1]?.[0]; - expect(alphaCtx).toMatchObject({ - AccountId: "alpha", - SessionKey: "agent:agent-alpha:synology-chat:alpha:direct:123", + incomingUrl: "https://nas.example.com/incoming-alpha", + webhookPath: "/webhook/synology-alpha", + webhookPathSource: "explicit", + }, }); - expect(betaCtx).toMatchObject({ - AccountId: "beta", - SessionKey: "agent:agent-beta:synology-chat:beta:direct:123", + expect(betaCall).toMatchObject({ + accountId: "beta", + account: { + accountId: "beta", + token: "token-beta", + incomingUrl: "https://nas.example.com/incoming-beta", + webhookPath: "/webhook/synology-beta", + webhookPathSource: "explicit", + }, }); alphaAbortController.abort();