diff --git a/extensions/bluebubbles/src/send.test.ts b/extensions/bluebubbles/src/send.test.ts index bc3938b327c..e2f501f3fe4 100644 --- a/extensions/bluebubbles/src/send.test.ts +++ b/extensions/bluebubbles/src/send.test.ts @@ -6,6 +6,7 @@ import { clearBlueBubblesRuntime, setBlueBubblesRuntime } from "./runtime.js"; import { sendMessageBlueBubbles, resolveChatGuidForTarget, createChatForHandle } from "./send.js"; import { BLUE_BUBBLES_PRIVATE_API_STATUS, + createBlueBubblesFetchGuardPassthroughInstaller, installBlueBubblesFetchTestHooks, mockBlueBubblesPrivateApiStatusOnce, } from "./test-harness.js"; @@ -13,6 +14,7 @@ import { _setFetchGuardForTesting, type BlueBubblesSendTarget } from "./types.js const mockFetch = vi.fn(); const privateApiStatusMock = vi.mocked(getCachedBlueBubblesPrivateApiStatus); +const setFetchGuardPassthrough = createBlueBubblesFetchGuardPassthroughInstaller(); installBlueBubblesFetchTestHooks({ mockFetch, @@ -62,29 +64,8 @@ function mockNewChatSendResponse(guid: string) { } function installSsrFPolicyCapture(policies: unknown[]) { - _setFetchGuardForTesting(async (params) => { - policies.push(params.policy); - const raw = await globalThis.fetch(params.url, params.init); - let body: ArrayBuffer; - if (typeof raw.arrayBuffer === "function") { - body = await raw.arrayBuffer(); - } else { - const text = - typeof (raw as { text?: () => Promise }).text === "function" - ? await (raw as { text: () => Promise }).text() - : typeof (raw as { json?: () => Promise }).json === "function" - ? JSON.stringify(await (raw as { json: () => Promise }).json()) - : ""; - body = new TextEncoder().encode(text).buffer; - } - return { - response: new Response(body, { - status: (raw as { status?: number }).status ?? 200, - headers: (raw as { headers?: HeadersInit }).headers, - }), - release: async () => {}, - finalUrl: params.url, - }; + setFetchGuardPassthrough((policy) => { + policies.push(policy); }); } diff --git a/extensions/bluebubbles/src/test-harness.ts b/extensions/bluebubbles/src/test-harness.ts index 91ac0545016..544625514fb 100644 --- a/extensions/bluebubbles/src/test-harness.ts +++ b/extensions/bluebubbles/src/test-harness.ts @@ -68,12 +68,29 @@ export function installBlueBubblesFetchTestHooks(params: { mockReturnValue: (value: boolean | null) => unknown; }; }) { + const setFetchGuardPassthrough = createBlueBubblesFetchGuardPassthroughInstaller(); beforeEach(() => { vi.stubGlobal("fetch", params.mockFetch); // Replace the SSRF guard with a passthrough that delegates to the mocked global.fetch, // wrapping the result in a real Response so callers can call .arrayBuffer() on it. - _setFetchGuardForTesting(async (p) => { - const raw = await globalThis.fetch(p.url, p.init); + setFetchGuardPassthrough(); + params.mockFetch.mockReset(); + params.privateApiStatusMock.mockReset?.(); + params.privateApiStatusMock.mockClear?.(); + params.privateApiStatusMock.mockReturnValue(BLUE_BUBBLES_PRIVATE_API_STATUS.unknown); + }); + + afterEach(() => { + _setFetchGuardForTesting(null); + vi.unstubAllGlobals(); + }); +} + +export function createBlueBubblesFetchGuardPassthroughInstaller() { + return (capturePolicy?: (policy: unknown) => void) => { + _setFetchGuardForTesting(async (params) => { + capturePolicy?.(params.policy); + const raw = await globalThis.fetch(params.url, params.init); let body: ArrayBuffer; if (typeof raw.arrayBuffer === "function") { body = await raw.arrayBuffer(); @@ -92,17 +109,8 @@ export function installBlueBubblesFetchTestHooks(params: { headers: (raw as { headers?: HeadersInit }).headers, }), release: async () => {}, - finalUrl: p.url, + finalUrl: params.url, }; }); - params.mockFetch.mockReset(); - params.privateApiStatusMock.mockReset?.(); - params.privateApiStatusMock.mockClear?.(); - params.privateApiStatusMock.mockReturnValue(BLUE_BUBBLES_PRIVATE_API_STATUS.unknown); - }); - - afterEach(() => { - _setFetchGuardForTesting(null); - vi.unstubAllGlobals(); - }); + }; } diff --git a/extensions/imessage/src/channel.outbound.test.ts b/extensions/imessage/src/channel.outbound.test.ts index 1780297d265..bddb4488a83 100644 --- a/extensions/imessage/src/channel.outbound.test.ts +++ b/extensions/imessage/src/channel.outbound.test.ts @@ -55,6 +55,61 @@ function getSentParams() { return requestMock.mock.calls[0]?.[1] as Record; } +async function expectDirectOutboundResult(params: { + invoke: () => Promise<{ channel: string; messageId: string }>; + sendIMessage: ReturnType; + to: string; + text: string; + expectedOptions: Record; + expectedResult: { channel: string; messageId: string }; +}) { + const result = await params.invoke(); + expect(params.sendIMessage).toHaveBeenCalledWith( + params.to, + params.text, + expect.objectContaining(params.expectedOptions), + ); + expect(result).toEqual(params.expectedResult); +} + +async function expectReplyToTextForwarding(params: { + invoke: () => Promise<{ channel: string; messageId: string }>; + sendIMessage: ReturnType; +}) { + await expectDirectOutboundResult({ + invoke: params.invoke, + sendIMessage: params.sendIMessage, + to: "chat_id:12", + text: "hello", + expectedOptions: { + accountId: "default", + replyToId: "reply-1", + maxBytes: 3 * 1024 * 1024, + }, + expectedResult: { channel: "imessage", messageId: "m-text" }, + }); +} + +async function expectMediaLocalRootsForwarding(params: { + invoke: () => Promise<{ channel: string; messageId: string }>; + sendIMessage: ReturnType; +}) { + await expectDirectOutboundResult({ + invoke: params.invoke, + sendIMessage: params.sendIMessage, + to: "chat_id:88", + text: "caption", + expectedOptions: { + mediaUrl: "/tmp/workspace/pic.png", + mediaLocalRoots: ["/tmp/workspace"], + accountId: "acct-1", + replyToId: "reply-2", + maxBytes: 3 * 1024 * 1024, + }, + expectedResult: { channel: "imessage", messageId: "m-media-local" }, + }); +} + describe("imessagePlugin outbound", () => { const cfg = { channels: { @@ -68,25 +123,18 @@ describe("imessagePlugin outbound", () => { const sendIMessage = vi.fn().mockResolvedValue({ messageId: "m-text" }); const sendText = requireIMessageSendText(); - const result = await sendText({ - cfg, - to: "chat_id:12", - text: "hello", - accountId: "default", - replyToId: "reply-1", - deps: { sendIMessage }, + await expectReplyToTextForwarding({ + invoke: async () => + await sendText({ + cfg, + to: "chat_id:12", + text: "hello", + accountId: "default", + replyToId: "reply-1", + deps: { sendIMessage }, + }), + sendIMessage, }); - - expect(sendIMessage).toHaveBeenCalledWith( - "chat_id:12", - "hello", - expect.objectContaining({ - accountId: "default", - replyToId: "reply-1", - maxBytes: 3 * 1024 * 1024, - }), - ); - expect(result).toEqual({ channel: "imessage", messageId: "m-text" }); }); it("forwards replyToId on direct sendMedia adapter path", async () => { @@ -121,27 +169,20 @@ describe("imessagePlugin outbound", () => { const sendMedia = requireIMessageSendMedia(); const mediaLocalRoots = ["/tmp/workspace"]; - const result = await sendMedia({ - cfg, - to: "chat_id:88", - text: "caption", - mediaUrl: "/tmp/workspace/pic.png", - mediaLocalRoots, - accountId: "acct-1", - deps: { sendIMessage }, + await expectMediaLocalRootsForwarding({ + invoke: async () => + await sendMedia({ + cfg, + to: "chat_id:88", + text: "caption", + mediaUrl: "/tmp/workspace/pic.png", + mediaLocalRoots, + accountId: "acct-1", + replyToId: "reply-2", + deps: { sendIMessage }, + }), + sendIMessage, }); - - expect(sendIMessage).toHaveBeenCalledWith( - "chat_id:88", - "caption", - expect.objectContaining({ - mediaUrl: "/tmp/workspace/pic.png", - mediaLocalRoots, - accountId: "acct-1", - maxBytes: 3 * 1024 * 1024, - }), - ); - expect(result).toEqual({ channel: "imessage", messageId: "m-media-local" }); }); }); @@ -157,53 +198,37 @@ describe("imessageOutbound", () => { it("forwards replyToId on direct text sends", async () => { const sendIMessage = vi.fn().mockResolvedValueOnce({ messageId: "m-text" }); - const result = await imessageOutbound.sendText!({ - cfg, - to: "chat_id:12", - text: "hello", - accountId: "default", - replyToId: "reply-1", - deps: { sendIMessage }, + await expectReplyToTextForwarding({ + invoke: async () => + await imessageOutbound.sendText!({ + cfg, + to: "chat_id:12", + text: "hello", + accountId: "default", + replyToId: "reply-1", + deps: { sendIMessage }, + }), + sendIMessage, }); - - expect(sendIMessage).toHaveBeenCalledWith( - "chat_id:12", - "hello", - expect.objectContaining({ - accountId: "default", - replyToId: "reply-1", - maxBytes: 3 * 1024 * 1024, - }), - ); - expect(result).toEqual({ channel: "imessage", messageId: "m-text" }); }); it("forwards mediaLocalRoots on direct media sends", async () => { const sendIMessage = vi.fn().mockResolvedValueOnce({ messageId: "m-media-local" }); - const result = await imessageOutbound.sendMedia!({ - cfg, - to: "chat_id:88", - text: "caption", - mediaUrl: "/tmp/workspace/pic.png", - mediaLocalRoots: ["/tmp/workspace"], - accountId: "acct-1", - replyToId: "reply-2", - deps: { sendIMessage }, + await expectMediaLocalRootsForwarding({ + invoke: async () => + await imessageOutbound.sendMedia!({ + cfg, + to: "chat_id:88", + text: "caption", + mediaUrl: "/tmp/workspace/pic.png", + mediaLocalRoots: ["/tmp/workspace"], + accountId: "acct-1", + replyToId: "reply-2", + deps: { sendIMessage }, + }), + sendIMessage, }); - - expect(sendIMessage).toHaveBeenCalledWith( - "chat_id:88", - "caption", - expect.objectContaining({ - mediaUrl: "/tmp/workspace/pic.png", - mediaLocalRoots: ["/tmp/workspace"], - accountId: "acct-1", - replyToId: "reply-2", - maxBytes: 3 * 1024 * 1024, - }), - ); - expect(result).toEqual({ channel: "imessage", messageId: "m-media-local" }); }); }); diff --git a/extensions/msteams/src/send.test.ts b/extensions/msteams/src/send.test.ts index 3c11cf4d55e..78805e15ae4 100644 --- a/extensions/msteams/src/send.test.ts +++ b/extensions/msteams/src/send.test.ts @@ -77,6 +77,65 @@ function mockContinueConversationFailure(error: string) { return mockContinueConversation; } +function createSharePointSendContext(params: { + conversationId: string; + graphChatId: string | null; + siteId: string; +}) { + return { + adapter: { + continueConversation: vi.fn( + async ( + _id: string, + _ref: unknown, + fn: (ctx: { sendActivity: () => { id: "msg-1" } }) => Promise, + ) => fn({ sendActivity: () => ({ id: "msg-1" }) }), + ), + }, + appId: "app-id", + conversationId: params.conversationId, + graphChatId: params.graphChatId, + ref: {}, + log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, + conversationType: "groupChat" as const, + tokenProvider: { getAccessToken: vi.fn(async () => "token") }, + mediaMaxBytes: 8 * 1024 * 1024, + sharePointSiteId: params.siteId, + }; +} + +function mockSharePointPdfUpload(params: { + bufferSize: number; + fileName: string; + itemId: string; + uniqueId: string; +}) { + mockState.loadOutboundMediaFromUrl.mockResolvedValueOnce({ + buffer: Buffer.alloc(params.bufferSize, "pdf"), + contentType: "application/pdf", + fileName: params.fileName, + kind: "file", + }); + mockState.requiresFileConsent.mockReturnValue(false); + mockState.uploadAndShareSharePoint.mockResolvedValue({ + itemId: params.itemId, + webUrl: `https://sp.example.com/${params.fileName}`, + shareUrl: `https://sp.example.com/share/${params.fileName}`, + name: params.fileName, + }); + mockState.getDriveItemProperties.mockResolvedValue({ + eTag: `"${params.uniqueId},1"`, + webDavUrl: `https://sp.example.com/dav/${params.fileName}`, + name: params.fileName, + }); + mockState.buildTeamsFileInfoCard.mockReturnValue({ + contentType: "application/vnd.microsoft.teams.card.file.info", + contentUrl: `https://sp.example.com/dav/${params.fileName}`, + name: params.fileName, + content: { uniqueId: params.uniqueId, fileType: "pdf" }, + }); +} + describe("sendMessageMSTeams", () => { beforeEach(() => { mockState.loadOutboundMediaFromUrl.mockReset(); @@ -148,51 +207,18 @@ describe("sendMessageMSTeams", () => { const graphChatId = "19:graph-native-chat-id@thread.tacv2"; const botFrameworkConversationId = "19:bot-framework-id@thread.tacv2"; - mockState.resolveMSTeamsSendContext.mockResolvedValue({ - adapter: { - continueConversation: vi.fn( - async ( - _id: string, - _ref: unknown, - fn: (ctx: { sendActivity: () => { id: "msg-1" } }) => Promise, - ) => fn({ sendActivity: () => ({ id: "msg-1" }) }), - ), - }, - appId: "app-id", - conversationId: botFrameworkConversationId, - graphChatId, - ref: {}, - log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, - conversationType: "groupChat", - tokenProvider: { getAccessToken: vi.fn(async () => "token") }, - mediaMaxBytes: 8 * 1024 * 1024, - sharePointSiteId: "site-123", - }); - - const pdfBuffer = Buffer.alloc(100, "pdf"); - mockState.loadOutboundMediaFromUrl.mockResolvedValueOnce({ - buffer: pdfBuffer, - contentType: "application/pdf", + mockState.resolveMSTeamsSendContext.mockResolvedValue( + createSharePointSendContext({ + conversationId: botFrameworkConversationId, + graphChatId, + siteId: "site-123", + }), + ); + mockSharePointPdfUpload({ + bufferSize: 100, fileName: "doc.pdf", - kind: "file", - }); - mockState.requiresFileConsent.mockReturnValue(false); - mockState.uploadAndShareSharePoint.mockResolvedValue({ itemId: "item-1", - webUrl: "https://sp.example.com/doc.pdf", - shareUrl: "https://sp.example.com/share/doc.pdf", - name: "doc.pdf", - }); - mockState.getDriveItemProperties.mockResolvedValue({ - eTag: '"{GUID-123},1"', - webDavUrl: "https://sp.example.com/dav/doc.pdf", - name: "doc.pdf", - }); - mockState.buildTeamsFileInfoCard.mockReturnValue({ - contentType: "application/vnd.microsoft.teams.card.file.info", - contentUrl: "https://sp.example.com/dav/doc.pdf", - name: "doc.pdf", - content: { uniqueId: "GUID-123", fileType: "pdf" }, + uniqueId: "{GUID-123}", }); await sendMessageMSTeams({ @@ -214,51 +240,18 @@ describe("sendMessageMSTeams", () => { it("falls back to conversationId when graphChatId is not available", async () => { const botFrameworkConversationId = "19:fallback-id@thread.tacv2"; - mockState.resolveMSTeamsSendContext.mockResolvedValue({ - adapter: { - continueConversation: vi.fn( - async ( - _id: string, - _ref: unknown, - fn: (ctx: { sendActivity: () => { id: "msg-1" } }) => Promise, - ) => fn({ sendActivity: () => ({ id: "msg-1" }) }), - ), - }, - appId: "app-id", - conversationId: botFrameworkConversationId, - graphChatId: null, // resolution failed — must fall back - ref: {}, - log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, - conversationType: "groupChat", - tokenProvider: { getAccessToken: vi.fn(async () => "token") }, - mediaMaxBytes: 8 * 1024 * 1024, - sharePointSiteId: "site-456", - }); - - const pdfBuffer = Buffer.alloc(50, "pdf"); - mockState.loadOutboundMediaFromUrl.mockResolvedValueOnce({ - buffer: pdfBuffer, - contentType: "application/pdf", + mockState.resolveMSTeamsSendContext.mockResolvedValue( + createSharePointSendContext({ + conversationId: botFrameworkConversationId, + graphChatId: null, + siteId: "site-456", + }), + ); + mockSharePointPdfUpload({ + bufferSize: 50, fileName: "report.pdf", - kind: "file", - }); - mockState.requiresFileConsent.mockReturnValue(false); - mockState.uploadAndShareSharePoint.mockResolvedValue({ itemId: "item-2", - webUrl: "https://sp.example.com/report.pdf", - shareUrl: "https://sp.example.com/share/report.pdf", - name: "report.pdf", - }); - mockState.getDriveItemProperties.mockResolvedValue({ - eTag: '"{GUID-456},1"', - webDavUrl: "https://sp.example.com/dav/report.pdf", - name: "report.pdf", - }); - mockState.buildTeamsFileInfoCard.mockReturnValue({ - contentType: "application/vnd.microsoft.teams.card.file.info", - contentUrl: "https://sp.example.com/dav/report.pdf", - name: "report.pdf", - content: { uniqueId: "GUID-456", fileType: "pdf" }, + uniqueId: "{GUID-456}", }); await sendMessageMSTeams({ diff --git a/extensions/talk-voice/index.test.ts b/extensions/talk-voice/index.test.ts index 2d7494c90b4..6cd096fb557 100644 --- a/extensions/talk-voice/index.test.ts +++ b/extensions/talk-voice/index.test.ts @@ -47,6 +47,24 @@ function createCommandContext( } describe("talk-voice plugin", () => { + function createElevenlabsVoiceSetHarness(channel = "webchat", scopes?: string[]) { + const { command, runtime } = createHarness({ + talk: { + provider: "elevenlabs", + providers: { + elevenlabs: { + apiKey: "sk-eleven", + }, + }, + }, + }); + vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]); + return { + runtime, + run: async () => await command.handler(createCommandContext("set Claudia", channel, scopes)), + }; + } + it("reports active provider status", async () => { const { command } = createHarness({ talk: { @@ -206,81 +224,32 @@ describe("talk-voice plugin", () => { }); it("rejects /voice set from gateway client with only operator.write scope", async () => { - const { command, runtime } = createHarness({ - talk: { - provider: "elevenlabs", - providers: { - elevenlabs: { - apiKey: "sk-eleven", - }, - }, - }, - }); - vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]); - - const result = await command.handler( - createCommandContext("set Claudia", "webchat", ["operator.write"]), - ); + const { runtime, run } = createElevenlabsVoiceSetHarness("webchat", ["operator.write"]); + const result = await run(); expect(result.text).toContain("requires operator.admin"); expect(runtime.config.writeConfigFile).not.toHaveBeenCalled(); }); it("allows /voice set from gateway client with operator.admin scope", async () => { - const { command, runtime } = createHarness({ - talk: { - provider: "elevenlabs", - providers: { - elevenlabs: { - apiKey: "sk-eleven", - }, - }, - }, - }); - vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]); - - const result = await command.handler( - createCommandContext("set Claudia", "webchat", ["operator.admin"]), - ); + const { runtime, run } = createElevenlabsVoiceSetHarness("webchat", ["operator.admin"]); + const result = await run(); expect(runtime.config.writeConfigFile).toHaveBeenCalled(); expect(result.text).toContain("voice-a"); }); it("rejects /voice set from webchat channel with no scopes (TUI/internal)", async () => { - const { command, runtime } = createHarness({ - talk: { - provider: "elevenlabs", - providers: { - elevenlabs: { - apiKey: "sk-eleven", - }, - }, - }, - }); - vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]); - - // gatewayClientScopes omitted — simulates internal webchat session without scopes - const result = await command.handler(createCommandContext("set Claudia", "webchat")); + const { runtime, run } = createElevenlabsVoiceSetHarness(); + const result = await run(); expect(result.text).toContain("requires operator.admin"); expect(runtime.config.writeConfigFile).not.toHaveBeenCalled(); }); it("allows /voice set from non-gateway channels without scope check", async () => { - const { command, runtime } = createHarness({ - talk: { - provider: "elevenlabs", - providers: { - elevenlabs: { - apiKey: "sk-eleven", - }, - }, - }, - }); - vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]); - - const result = await command.handler(createCommandContext("set Claudia", "telegram")); + const { runtime, run } = createElevenlabsVoiceSetHarness("telegram"); + const result = await run(); expect(runtime.config.writeConfigFile).toHaveBeenCalled(); expect(result.text).toContain("voice-a"); diff --git a/extensions/telegram/src/bot-message-context.dm-topic-threadid.test.ts b/extensions/telegram/src/bot-message-context.dm-topic-threadid.test.ts index 696f74888ea..0db56120483 100644 --- a/extensions/telegram/src/bot-message-context.dm-topic-threadid.test.ts +++ b/extensions/telegram/src/bot-message-context.dm-topic-threadid.test.ts @@ -21,6 +21,15 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889 }); } + function expectRecordedRoute(params: { to: string; threadId?: string }) { + const updateLastRoute = getRecordedUpdateLastRoute(0) as + | { threadId?: string; to?: string } + | undefined; + expect(updateLastRoute).toBeDefined(); + expect(updateLastRoute?.to).toBe(params.to); + expect(updateLastRoute?.threadId).toBe(params.threadId); + } + afterEach(() => { clearRuntimeConfigSnapshot(); recordInboundSessionMock.mockClear(); @@ -46,13 +55,7 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889 expect(ctx).not.toBeNull(); expect(recordInboundSessionMock).toHaveBeenCalled(); - // Check that updateLastRoute includes threadId - const updateLastRoute = getRecordedUpdateLastRoute(0) as - | { threadId?: string; to?: string } - | undefined; - expect(updateLastRoute).toBeDefined(); - expect(updateLastRoute?.to).toBe("telegram:1234"); - expect(updateLastRoute?.threadId).toBe("42"); + expectRecordedRoute({ to: "telegram:1234", threadId: "42" }); }); it("does not pass threadId for regular DM without topic", async () => { @@ -65,13 +68,7 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889 expect(ctx).not.toBeNull(); expect(recordInboundSessionMock).toHaveBeenCalled(); - // Check that updateLastRoute does NOT include threadId - const updateLastRoute = getRecordedUpdateLastRoute(0) as - | { threadId?: string; to?: string } - | undefined; - expect(updateLastRoute).toBeDefined(); - expect(updateLastRoute?.to).toBe("telegram:1234"); - expect(updateLastRoute?.threadId).toBeUndefined(); + expectRecordedRoute({ to: "telegram:1234" }); }); it("passes threadId to updateLastRoute for forum topic group messages", async () => { @@ -88,12 +85,7 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889 expect(ctx).not.toBeNull(); expect(recordInboundSessionMock).toHaveBeenCalled(); - const updateLastRoute = getRecordedUpdateLastRoute(0) as - | { threadId?: string; to?: string } - | undefined; - expect(updateLastRoute).toBeDefined(); - expect(updateLastRoute?.to).toBe("telegram:-1001234567890"); - expect(updateLastRoute?.threadId).toBe("99"); + expectRecordedRoute({ to: "telegram:-1001234567890", threadId: "99" }); }); it("passes threadId to updateLastRoute for the forum General topic", async () => { @@ -109,11 +101,6 @@ describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#889 expect(ctx).not.toBeNull(); expect(recordInboundSessionMock).toHaveBeenCalled(); - const updateLastRoute = getRecordedUpdateLastRoute(0) as - | { threadId?: string; to?: string } - | undefined; - expect(updateLastRoute).toBeDefined(); - expect(updateLastRoute?.to).toBe("telegram:-1001234567890"); - expect(updateLastRoute?.threadId).toBe("1"); + expectRecordedRoute({ to: "telegram:-1001234567890", threadId: "1" }); }); }); diff --git a/extensions/telegram/src/token.test.ts b/extensions/telegram/src/token.test.ts index 93e825344e8..4e9429bf238 100644 --- a/extensions/telegram/src/token.test.ts +++ b/extensions/telegram/src/token.test.ts @@ -23,6 +23,25 @@ describe("resolveTelegramToken", () => { return tokenFile; } + function createUnknownAccountConfig(): OpenClawConfig { + return { + channels: { + telegram: { + botToken: "wrong-bot-token", + accounts: { + knownBot: { botToken: "known-bot-token" }, + }, + }, + }, + } as OpenClawConfig; + } + + function expectNoTokenForUnknownAccount(cfg: OpenClawConfig) { + const res = resolveTelegramToken(cfg, { accountId: "unknownBot" }); + expect(res.token).toBe(""); + expect(res.source).toBe("none"); + } + afterEach(() => { vi.unstubAllEnvs(); for (const dir of tempDirs.splice(0)) { @@ -207,20 +226,7 @@ describe("resolveTelegramToken", () => { it("does not fall through to channel-level token when non-default accountId is not in config", () => { vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); - const cfg = { - channels: { - telegram: { - botToken: "wrong-bot-token", - accounts: { - knownBot: { botToken: "known-bot-token" }, - }, - }, - }, - } as OpenClawConfig; - - const res = resolveTelegramToken(cfg, { accountId: "unknownBot" }); - expect(res.token).toBe(""); - expect(res.source).toBe("none"); + expectNoTokenForUnknownAccount(createUnknownAccountConfig()); }); it("throws when botToken is an unresolved SecretRef object", () => { @@ -257,20 +263,7 @@ describe("resolveTelegramToken", () => { it("still blocks fallthrough for unknown accountId when accounts section exists", () => { vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); - const cfg = { - channels: { - telegram: { - botToken: "wrong-bot-token", - accounts: { - knownBot: { botToken: "known-bot-token" }, - }, - }, - }, - } as OpenClawConfig; - - const res = resolveTelegramToken(cfg, { accountId: "unknownBot" }); - expect(res.token).toBe(""); - expect(res.source).toBe("none"); + expectNoTokenForUnknownAccount(createUnknownAccountConfig()); }); }); diff --git a/extensions/zalouser/src/setup-surface.test.ts b/extensions/zalouser/src/setup-surface.test.ts index 222090d1a34..8666f4aa7f6 100644 --- a/extensions/zalouser/src/setup-surface.test.ts +++ b/extensions/zalouser/src/setup-surface.test.ts @@ -26,6 +26,50 @@ async function runSetup(params: { } describe("zalouser setup wizard", () => { + function createQuickstartPrompter(params?: { + note?: ReturnType["note"]; + seen?: string[]; + dmPolicy?: "pairing" | "allowlist"; + groupAccess?: boolean; + groupPolicy?: "allowlist"; + textByMessage?: Record; + }) { + const select = vi.fn( + async ({ message, options }: { message: string; options: Array<{ value: string }> }) => { + const first = options[0]; + if (!first) { + throw new Error("no options"); + } + params?.seen?.push(message); + if (message === "Zalo Personal DM policy" && params?.dmPolicy) { + return params.dmPolicy; + } + if (message === "Zalo groups access" && params?.groupPolicy) { + return params.groupPolicy; + } + return first.value; + }, + ) as ReturnType["select"]; + const text = vi.fn( + async ({ message }: { message: string }) => params?.textByMessage?.[message] ?? "", + ) as ReturnType["text"]; + return createTestWizardPrompter({ + ...(params?.note ? { note: params.note } : {}), + confirm: vi.fn(async ({ message }: { message: string }) => { + params?.seen?.push(message); + if (message === "Login via QR code now?") { + return false; + } + if (message === "Configure Zalo groups access?") { + return params?.groupAccess ?? false; + } + return false; + }), + select, + text, + }); + } + it("enables the account without forcing QR login", async () => { const prompter = createTestWizardPrompter({ confirm: vi.fn(async ({ message }: { message: string }) => { @@ -48,31 +92,7 @@ describe("zalouser setup wizard", () => { it("prompts DM policy before group access in quickstart", async () => { const seen: string[] = []; - const prompter = createTestWizardPrompter({ - confirm: vi.fn(async ({ message }: { message: string }) => { - seen.push(message); - if (message === "Login via QR code now?") { - return false; - } - if (message === "Configure Zalo groups access?") { - return false; - } - return false; - }), - select: vi.fn( - async ({ message, options }: { message: string; options: Array<{ value: string }> }) => { - const first = options[0]; - if (!first) { - throw new Error("no options"); - } - seen.push(message); - if (message === "Zalo Personal DM policy") { - return "pairing"; - } - return first.value; - }, - ) as ReturnType["select"], - }); + const prompter = createQuickstartPrompter({ seen, dmPolicy: "pairing" }); const result = await runSetup({ prompter, @@ -92,35 +112,12 @@ describe("zalouser setup wizard", () => { it("allows an empty quickstart DM allowlist with a warning", async () => { const note = vi.fn(async (_message: string, _title?: string) => {}); - const prompter = createTestWizardPrompter({ + const prompter = createQuickstartPrompter({ note, - confirm: vi.fn(async ({ message }: { message: string }) => { - if (message === "Login via QR code now?") { - return false; - } - if (message === "Configure Zalo groups access?") { - return false; - } - return false; - }), - select: vi.fn( - async ({ message, options }: { message: string; options: Array<{ value: string }> }) => { - const first = options[0]; - if (!first) { - throw new Error("no options"); - } - if (message === "Zalo Personal DM policy") { - return "allowlist"; - } - return first.value; - }, - ) as ReturnType["select"], - text: vi.fn(async ({ message }: { message: string }) => { - if (message === "Zalouser allowFrom (name or user id)") { - return ""; - } - return ""; - }) as ReturnType["text"], + dmPolicy: "allowlist", + textByMessage: { + "Zalouser allowFrom (name or user id)": "", + }, }); const result = await runSetup({ @@ -142,35 +139,13 @@ describe("zalouser setup wizard", () => { it("allows an empty group allowlist with a warning", async () => { const note = vi.fn(async (_message: string, _title?: string) => {}); - const prompter = createTestWizardPrompter({ + const prompter = createQuickstartPrompter({ note, - confirm: vi.fn(async ({ message }: { message: string }) => { - if (message === "Login via QR code now?") { - return false; - } - if (message === "Configure Zalo groups access?") { - return true; - } - return false; - }), - select: vi.fn( - async ({ message, options }: { message: string; options: Array<{ value: string }> }) => { - const first = options[0]; - if (!first) { - throw new Error("no options"); - } - if (message === "Zalo groups access") { - return "allowlist"; - } - return first.value; - }, - ) as ReturnType["select"], - text: vi.fn(async ({ message }: { message: string }) => { - if (message === "Zalo groups allowlist (comma-separated)") { - return ""; - } - return ""; - }) as ReturnType["text"], + groupAccess: true, + groupPolicy: "allowlist", + textByMessage: { + "Zalo groups allowlist (comma-separated)": "", + }, }); const result = await runSetup({ prompter });