diff --git a/src/plugins/interactive.test.ts b/src/plugins/interactive.test.ts index f794cde4037..7605a6b3655 100644 --- a/src/plugins/interactive.test.ts +++ b/src/plugins/interactive.test.ts @@ -89,6 +89,30 @@ describe("plugin interactive handlers", () => { }); }); + it("rejects unsupported interactive channels and malformed handlers", () => { + expect( + registerPluginInteractiveHandler("plugin-a", { + channel: "slack", + namespace: "codex", + handler: async () => ({ handled: true }), + } as unknown as Parameters[1]), + ).toEqual({ + ok: false, + error: 'Interactive handler channel must be either "telegram" or "discord"', + }); + + expect( + registerPluginInteractiveHandler("plugin-a", { + channel: "telegram", + namespace: "codex", + handler: "not-a-function", + } as unknown as Parameters[1]), + ).toEqual({ + ok: false, + error: "Interactive handler must be a function", + }); + }); + it("routes Discord interactions by namespace and dedupes interaction ids", async () => { const handler = vi.fn(async () => ({ handled: true })); expect( @@ -198,4 +222,128 @@ describe("plugin interactive handlers", () => { }); expect(handler).toHaveBeenCalledTimes(2); }); + + it("does not share dedupe keys across channels", async () => { + const telegramHandler = vi.fn(async () => ({ handled: true })); + const discordHandler = vi.fn(async () => ({ handled: true })); + expect( + registerPluginInteractiveHandler("codex-plugin", { + channel: "telegram", + namespace: "codex", + handler: telegramHandler, + }), + ).toEqual({ ok: true }); + expect( + registerPluginInteractiveHandler("codex-plugin", { + channel: "discord", + namespace: "codex", + handler: discordHandler, + }), + ).toEqual({ ok: true }); + + const telegramResult = await dispatchPluginInteractiveHandler({ + channel: "telegram", + data: "codex:resume", + callbackId: "same-id", + ctx: { + accountId: "default", + callbackId: "same-id", + conversationId: "chat-1", + senderId: "user-1", + senderUsername: "ada", + isGroup: false, + isForum: false, + auth: { isAuthorizedSender: true }, + callbackMessage: { + messageId: 1, + chatId: "chat-1", + }, + }, + respond: { + reply: vi.fn(async () => {}), + editMessage: vi.fn(async () => {}), + editButtons: vi.fn(async () => {}), + clearButtons: vi.fn(async () => {}), + deleteMessage: vi.fn(async () => {}), + }, + }); + const discordResult = await dispatchPluginInteractiveHandler({ + channel: "discord", + data: "codex:resume", + interactionId: "same-id", + ctx: { + accountId: "default", + interactionId: "same-id", + conversationId: "channel-1", + senderId: "user-1", + senderUsername: "ada", + auth: { isAuthorizedSender: true }, + interaction: { + kind: "button", + }, + }, + respond: { + acknowledge: vi.fn(async () => {}), + reply: vi.fn(async () => {}), + followUp: vi.fn(async () => {}), + editMessage: vi.fn(async () => {}), + clearComponents: vi.fn(async () => {}), + }, + }); + + expect(telegramResult).toEqual({ matched: true, handled: true, duplicate: false }); + expect(discordResult).toEqual({ matched: true, handled: true, duplicate: false }); + expect(telegramHandler).toHaveBeenCalledTimes(1); + expect(discordHandler).toHaveBeenCalledTimes(1); + }); + + it("does not consume dedupe keys when a handler declines", async () => { + const handler = vi + .fn(async () => ({ handled: false })) + .mockResolvedValueOnce({ handled: false }) + .mockResolvedValueOnce({ handled: true }); + expect( + registerPluginInteractiveHandler("codex-plugin", { + channel: "discord", + namespace: "codex", + handler, + }), + ).toEqual({ ok: true }); + + const baseParams = { + channel: "discord" as const, + data: "codex:approve:thread-1", + interactionId: "ix-decline", + ctx: { + accountId: "default", + interactionId: "ix-decline", + conversationId: "channel-1", + senderId: "user-1", + senderUsername: "ada", + auth: { isAuthorizedSender: true }, + interaction: { + kind: "button" as const, + }, + }, + respond: { + acknowledge: vi.fn(async () => {}), + reply: vi.fn(async () => {}), + followUp: vi.fn(async () => {}), + editMessage: vi.fn(async () => {}), + clearComponents: vi.fn(async () => {}), + }, + }; + + await expect(dispatchPluginInteractiveHandler(baseParams)).resolves.toEqual({ + matched: true, + handled: false, + duplicate: false, + }); + await expect(dispatchPluginInteractiveHandler(baseParams)).resolves.toEqual({ + matched: true, + handled: true, + duplicate: false, + }); + expect(handler).toHaveBeenCalledTimes(2); + }); });