diff --git a/packages/adapter-discord/package.json b/packages/adapter-discord/package.json index 801d7f9a8..d97ef5fc8 100644 --- a/packages/adapter-discord/package.json +++ b/packages/adapter-discord/package.json @@ -34,6 +34,7 @@ "discord.js": "^14.25.1" }, "devDependencies": { + "@chat-adapter/tests": "workspace:*", "@types/node": "^25.3.2", "tsup": "^8.3.5", "typescript": "^5.7.2", diff --git a/packages/adapter-discord/src/gateway.test.ts b/packages/adapter-discord/src/gateway.test.ts index 6f8722277..c27d1afb8 100644 --- a/packages/adapter-discord/src/gateway.test.ts +++ b/packages/adapter-discord/src/gateway.test.ts @@ -5,6 +5,7 @@ * intents and partials for receiving DM events. */ +import { createMockChatInstance, mockLogger } from "@chat-adapter/tests"; import { GatewayIntentBits, Partials } from "discord.js"; import { describe, expect, it, vi } from "vitest"; import { createDiscordAdapter } from "./index"; @@ -33,30 +34,6 @@ vi.mock("discord.js", async () => { }; }); -const mockLogger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - child: vi.fn().mockReturnThis(), -}; - -const mockChat = { - getLogger: vi.fn().mockReturnValue(mockLogger), - getState: vi.fn(), - getUserName: vi.fn().mockReturnValue("bot"), - handleIncomingMessage: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processAppHomeOpened: vi.fn(), - processAssistantContextChanged: vi.fn(), - processAssistantThreadStarted: vi.fn(), - processMessage: vi.fn(), - processModalClose: vi.fn(), - processModalSubmit: vi.fn(), - processReaction: vi.fn(), -}; - describe("Gateway client configuration", () => { it("includes Partials.Channel for DM support", async () => { MockClient.mockClear(); @@ -71,7 +48,7 @@ describe("Gateway client configuration", () => { logger: mockLogger, }); - await adapter.initialize(mockChat as never); + await adapter.initialize(createMockChatInstance()); // Use a pre-aborted signal so the listener resolves immediately const controller = new AbortController(); diff --git a/packages/adapter-discord/src/index.test.ts b/packages/adapter-discord/src/index.test.ts index a40c332e8..b850017d2 100644 --- a/packages/adapter-discord/src/index.test.ts +++ b/packages/adapter-discord/src/index.test.ts @@ -5,7 +5,7 @@ import { generateKeyPairSync, sign } from "node:crypto"; import { EventEmitter } from "node:events"; import { ValidationError } from "@chat-adapter/shared"; -import type { ChatInstance, Logger } from "chat"; +import { createMockChatInstance, mockLogger } from "@chat-adapter/tests"; import { Actions, Button, Card } from "chat"; import { type Client, Events } from "discord.js"; import { InteractionType } from "discord-api-types/v10"; @@ -19,14 +19,6 @@ import { DiscordFormatConverter } from "./markdown"; const AT_ME_REGEX = /\/@me$/; -const mockLogger: Logger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - child: vi.fn().mockReturnThis(), -}; - // ============================================================================ // Test Helpers // ============================================================================ @@ -582,10 +574,8 @@ describe("handleWebhook - APPLICATION_COMMAND", () => { }); it("dispatches slash command to chat core", async () => { - const processSlashCommand = vi.fn(); - await adapter.initialize({ - processSlashCommand, - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: InteractionType.ApplicationCommand, @@ -627,8 +617,8 @@ describe("handleWebhook - APPLICATION_COMMAND", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(processSlashCommand).toHaveBeenCalledTimes(1); - expect(processSlashCommand).toHaveBeenCalledWith( + expect(chat.processSlashCommand).toHaveBeenCalledTimes(1); + expect(chat.processSlashCommand).toHaveBeenCalledWith( expect.objectContaining({ command: "/test", text: "status true", @@ -646,10 +636,8 @@ describe("handleWebhook - APPLICATION_COMMAND", () => { }); it("expands subcommand path into event.command", async () => { - const processSlashCommand = vi.fn(); - await adapter.initialize({ - processSlashCommand, - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: InteractionType.ApplicationCommand, @@ -695,8 +683,8 @@ describe("handleWebhook - APPLICATION_COMMAND", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(processSlashCommand).toHaveBeenCalledTimes(1); - expect(processSlashCommand).toHaveBeenCalledWith( + expect(chat.processSlashCommand).toHaveBeenCalledTimes(1); + expect(chat.processSlashCommand).toHaveBeenCalledWith( expect.objectContaining({ command: "/project issue create", text: "Login fails high", @@ -739,11 +727,14 @@ describe("handleWebhook - APPLICATION_COMMAND", () => { try { let replyTask: Promise | undefined; - await adapter.initialize({ - processSlashCommand: (event: { channelId: string }) => { - replyTask = adapter.postChannelMessage(event.channelId, "Pong!"); + const chat = createMockChatInstance({ + overrides: { + processSlashCommand: (event) => { + replyTask = adapter.postChannelMessage(event.channelId, "Pong!"); + }, }, - } as unknown as ChatInstance); + }); + await adapter.initialize(chat); const body = JSON.stringify({ type: InteractionType.ApplicationCommand, @@ -3310,7 +3301,6 @@ describe("legacy gateway interactions", () => { } it("handles slash command interactions from the gateway", async () => { - const processSlashCommand = vi.fn(); const adapter = new TestGatewayDiscordAdapter({ botToken: "test-token", publicKey: testPublicKey, @@ -3318,12 +3308,8 @@ describe("legacy gateway interactions", () => { logger: mockLogger, }); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand, - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const client = createGatewayClient(); const deferReply = vi.fn().mockResolvedValue(undefined); @@ -3363,7 +3349,7 @@ describe("legacy gateway interactions", () => { await waitForGatewayHandlers(); expect(deferReply).toHaveBeenCalled(); - expect(processSlashCommand).toHaveBeenCalledWith( + expect(chat.processSlashCommand).toHaveBeenCalledWith( expect.objectContaining({ command: "/test", text: "status true", @@ -3379,7 +3365,6 @@ describe("legacy gateway interactions", () => { }); it("sets gateway deferred slash command interaction flags from config", async () => { - const processSlashCommand = vi.fn(); const interactionFlags = vi .fn() .mockReturnValue(DiscordInteractionResponseFlag.Ephemeral); @@ -3391,12 +3376,8 @@ describe("legacy gateway interactions", () => { interactionFlags, }); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand, - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const client = createGatewayClient(); const deferReply = vi.fn().mockResolvedValue(undefined); @@ -3443,7 +3424,7 @@ describe("legacy gateway interactions", () => { user: expect.objectContaining({ id: "user789" }), }) ); - expect(processSlashCommand).toHaveBeenCalledWith( + expect(chat.processSlashCommand).toHaveBeenCalledWith( expect.objectContaining({ command: "/test", text: "status", @@ -3453,7 +3434,6 @@ describe("legacy gateway interactions", () => { }); it("handles component interactions from the gateway", async () => { - const processAction = vi.fn(); const adapter = new TestGatewayDiscordAdapter({ botToken: "test-token", publicKey: testPublicKey, @@ -3461,12 +3441,8 @@ describe("legacy gateway interactions", () => { logger: mockLogger, }); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand: vi.fn(), - processAction, - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const client = createGatewayClient(); const deferUpdate = vi.fn().mockResolvedValue(undefined); @@ -3503,7 +3479,7 @@ describe("legacy gateway interactions", () => { await waitForGatewayHandlers(); expect(deferUpdate).toHaveBeenCalled(); - expect(processAction).toHaveBeenCalledWith( + expect(chat.processAction).toHaveBeenCalledWith( expect.objectContaining({ actionId: "approve_btn", value: "approve_btn", @@ -3582,14 +3558,8 @@ describe("handleWebhook - forwarded gateway events", () => { }); it("handles GATEWAY_MESSAGE_CREATE event", async () => { - const processMessage = vi.fn(); - await adapter.initialize({ - handleIncomingMessage: processMessage, - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: "GATEWAY_MESSAGE_CREATE", @@ -3621,18 +3591,12 @@ describe("handleWebhook - forwarded gateway events", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(processMessage).toHaveBeenCalled(); + expect(chat.handleIncomingMessage).toHaveBeenCalled(); }); it("handles GATEWAY_MESSAGE_REACTION_ADD event", async () => { - const processReaction = vi.fn(); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processReaction, - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: "GATEWAY_MESSAGE_REACTION_ADD", @@ -3663,7 +3627,7 @@ describe("handleWebhook - forwarded gateway events", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(processReaction).toHaveBeenCalledWith( + expect(chat.processReaction).toHaveBeenCalledWith( expect.objectContaining({ added: true, messageId: "msg123", @@ -3672,14 +3636,8 @@ describe("handleWebhook - forwarded gateway events", () => { }); it("handles GATEWAY_MESSAGE_REACTION_REMOVE event", async () => { - const processReaction = vi.fn(); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processReaction, - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: "GATEWAY_MESSAGE_REACTION_REMOVE", @@ -3710,7 +3668,7 @@ describe("handleWebhook - forwarded gateway events", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(processReaction).toHaveBeenCalledWith( + expect(chat.processReaction).toHaveBeenCalledWith( expect.objectContaining({ added: false, messageId: "msg123", @@ -3732,13 +3690,8 @@ describe("handleForwardedMessage - thread handling", () => { logger: mockLogger, }); - const handleIncomingMessage = vi.fn(); - await adapter.initialize({ - handleIncomingMessage, - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: "GATEWAY_MESSAGE_CREATE", @@ -3774,7 +3727,7 @@ describe("handleForwardedMessage - thread handling", () => { await adapter.handleWebhook(request); - expect(handleIncomingMessage).toHaveBeenCalledWith( + expect(chat.handleIncomingMessage).toHaveBeenCalledWith( adapter, "discord:guild1:channel456:thread789", expect.anything() @@ -3789,13 +3742,8 @@ describe("handleForwardedMessage - thread handling", () => { logger: mockLogger, }); - const handleIncomingMessage = vi.fn(); - await adapter.initialize({ - handleIncomingMessage, - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); // Mock discordFetch to return parent channel info const fetchSpy = vi.spyOn(adapter as any, "discordFetch").mockResolvedValue( @@ -3837,7 +3785,7 @@ describe("handleForwardedMessage - thread handling", () => { await adapter.handleWebhook(request); expect(fetchSpy).toHaveBeenCalledWith("/channels/thread789", "GET"); - expect(handleIncomingMessage).toHaveBeenCalledWith( + expect(chat.handleIncomingMessage).toHaveBeenCalledWith( adapter, "discord:guild1:channel456:thread789", expect.anything() @@ -3854,13 +3802,8 @@ describe("handleForwardedMessage - thread handling", () => { logger: mockLogger, }); - const handleIncomingMessage = vi.fn(); - await adapter.initialize({ - handleIncomingMessage, - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const fetchSpy = vi .spyOn(adapter as any, "discordFetch") @@ -3926,13 +3869,8 @@ describe("handleForwardedReaction - thread parent caching", () => { logger: mockLogger, }); - const processReaction = vi.fn(); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction, - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const fetchSpy = vi.spyOn(adapter as any, "discordFetch").mockResolvedValue( new Response(JSON.stringify({ parent_id: "channel456" }), { @@ -3971,14 +3909,14 @@ describe("handleForwardedReaction - thread parent caching", () => { // Should have fetched parent channel expect(fetchSpy).toHaveBeenCalledWith("/channels/thread789", "GET"); - expect(processReaction).toHaveBeenCalledWith( + expect(chat.processReaction).toHaveBeenCalledWith( expect.objectContaining({ threadId: "discord:guild1:channel456:thread789", }) ); fetchSpy.mockClear(); - processReaction.mockClear(); + vi.mocked(chat.processReaction).mockClear(); // Second reaction on same thread - should use cache const body2 = JSON.stringify({ @@ -4010,7 +3948,7 @@ describe("handleForwardedReaction - thread parent caching", () => { // Should NOT have fetched again (used cache) expect(fetchSpy).not.toHaveBeenCalled(); - expect(processReaction).toHaveBeenCalledWith( + expect(chat.processReaction).toHaveBeenCalledWith( expect.objectContaining({ threadId: "discord:guild1:channel456:thread789", }) @@ -4027,13 +3965,8 @@ describe("handleForwardedReaction - thread parent caching", () => { logger: mockLogger, }); - const processReaction = vi.fn(); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction, - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: "GATEWAY_MESSAGE_REACTION_ADD", @@ -4060,7 +3993,7 @@ describe("handleForwardedReaction - thread parent caching", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); // Should not call processReaction since there's no user info - expect(processReaction).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processReaction"); }); it("handles custom emoji with ID in reaction", async () => { @@ -4071,13 +4004,8 @@ describe("handleForwardedReaction - thread parent caching", () => { logger: mockLogger, }); - const processReaction = vi.fn(); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction, - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: "GATEWAY_MESSAGE_REACTION_ADD", @@ -4105,7 +4033,7 @@ describe("handleForwardedReaction - thread parent caching", () => { await adapter.handleWebhook(request); - expect(processReaction).toHaveBeenCalledWith( + expect(chat.processReaction).toHaveBeenCalledWith( expect.objectContaining({ rawEmoji: "<:custom_emoji:emoji123>", }) @@ -4126,12 +4054,7 @@ describe("initialize", () => { logger: mockLogger, }); - const mockChat = { - handleIncomingMessage: vi.fn(), - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance; + const mockChat = createMockChatInstance(); await adapter.initialize(mockChat); @@ -4179,13 +4102,8 @@ describe("handleWebhook - component interaction edge cases", () => { logger: mockLogger, }); - const processAction = vi.fn(); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand: vi.fn(), - processAction, - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: InteractionType.MessageComponent, @@ -4234,7 +4152,7 @@ describe("handleWebhook - component interaction edge cases", () => { await adapter.handleWebhook(request); - expect(processAction).toHaveBeenCalledWith( + expect(chat.processAction).toHaveBeenCalledWith( expect.objectContaining({ actionId: "approve_btn", threadId: "discord:guild123:channel789:thread456", @@ -4251,13 +4169,8 @@ describe("handleWebhook - component interaction edge cases", () => { logger: mockLogger, }); - const processSlashCommand = vi.fn(); - await adapter.initialize({ - handleIncomingMessage: vi.fn(), - processSlashCommand, - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: InteractionType.ApplicationCommand, @@ -4290,7 +4203,7 @@ describe("handleWebhook - component interaction edge cases", () => { await adapter.handleWebhook(request); - expect(processSlashCommand).toHaveBeenCalledWith( + expect(chat.processSlashCommand).toHaveBeenCalledWith( expect.objectContaining({ command: "/status", channelId: "discord:guild123:channel789:thread456", @@ -4313,13 +4226,8 @@ describe("handleForwardedMessage - DM messages", () => { logger: mockLogger, }); - const handleIncomingMessage = vi.fn(); - await adapter.initialize({ - handleIncomingMessage, - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: "GATEWAY_MESSAGE_CREATE", @@ -4351,7 +4259,7 @@ describe("handleForwardedMessage - DM messages", () => { await adapter.handleWebhook(request); - expect(handleIncomingMessage).toHaveBeenCalledWith( + expect(chat.handleIncomingMessage).toHaveBeenCalledWith( adapter, "discord:@me:dm456", expect.anything() @@ -4373,7 +4281,6 @@ describe("mentionRoleIds handling", () => { mentionRoleIds: ["role123"], }); - const handleIncomingMessage = vi.fn(); const fetchSpy = vi.spyOn(adapter as any, "discordFetch").mockResolvedValue( new Response(JSON.stringify({ id: "new-thread", name: "Thread" }), { status: 200, @@ -4381,12 +4288,8 @@ describe("mentionRoleIds handling", () => { }) ); - await adapter.initialize({ - handleIncomingMessage, - processSlashCommand: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - } as unknown as ChatInstance); + const chat = createMockChatInstance(); + await adapter.initialize(chat); const body = JSON.stringify({ type: "GATEWAY_MESSAGE_CREATE", diff --git a/packages/adapter-discord/vitest.config.ts b/packages/adapter-discord/vitest.config.ts index edc2d946b..18e0de9ae 100644 --- a/packages/adapter-discord/vitest.config.ts +++ b/packages/adapter-discord/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-gchat/package.json b/packages/adapter-gchat/package.json index 449eab6ac..242f88a3e 100644 --- a/packages/adapter-gchat/package.json +++ b/packages/adapter-gchat/package.json @@ -33,6 +33,7 @@ "chat": "workspace:*" }, "devDependencies": { + "@chat-adapter/tests": "workspace:*", "@types/node": "^25.3.2", "tsup": "^8.3.5", "typescript": "^5.7.2", diff --git a/packages/adapter-gchat/src/index.test.ts b/packages/adapter-gchat/src/index.test.ts index d01bc759c..8636e2d3d 100644 --- a/packages/adapter-gchat/src/index.test.ts +++ b/packages/adapter-gchat/src/index.test.ts @@ -1,6 +1,11 @@ import { AdapterRateLimitError } from "@chat-adapter/shared"; +import { + createMockChatInstance, + createMockLogger, + mockLogger, +} from "@chat-adapter/tests"; import { auth } from "@googleapis/chat"; -import type { ChatInstance, Lock, Logger, StateAdapter } from "chat"; +import type { ChatInstance, Lock, StateAdapter } from "chat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createGoogleChatAdapter, @@ -19,13 +24,6 @@ const VERIFICATION_REQUIRED_PATTERN = /Webhook signature verification is required/; // Test credentials -const mockLogger: Logger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), -}; - const TEST_CREDENTIALS = { client_email: "test@test.iam.gserviceaccount.com", private_key: "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----\n", @@ -68,23 +66,6 @@ function createMockStateAdapter(): StateAdapter & { }; } -// Mock ChatInstance for testing -function createMockChatInstance(state: StateAdapter): ChatInstance { - return { - getState: () => state, - getLogger: () => ({ - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }), - processMessage: vi.fn(), - processReaction: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - } as unknown as ChatInstance; -} - /** Helper: build a minimal GoogleChatEvent with a message payload */ function makeMessageEvent( overrides: { @@ -174,7 +155,7 @@ async function createInitializedAdapter(opts?: { pubsubAudience: opts?.pubsubAudience, }); const mockState = createMockStateAdapter(); - const mockChat = createMockChatInstance(mockState); + const mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); return { adapter, mockState, mockChat }; } @@ -323,7 +304,7 @@ describe("GoogleChatAdapter", () => { }); const mockState = createMockStateAdapter(); mockState.storage.set("gchat:botUserId", "users/BOT999"); - const mockChat = createMockChatInstance(mockState); + const mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); @@ -339,7 +320,7 @@ describe("GoogleChatAdapter", () => { const mockState = createMockStateAdapter(); mockState.storage.set("gchat:botUserId", "users/OTHERFROMSTATE"); - const mockChat = createMockChatInstance(mockState); + const mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); @@ -785,7 +766,7 @@ describe("GoogleChatAdapter", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).toHaveBeenCalled(); + expect(mockChat).toHaveDispatched("processMessage"); }); it("should handle ADDED_TO_SPACE events", async () => { @@ -866,7 +847,7 @@ describe("GoogleChatAdapter", () => { expect(response.status).toBe(200); const body = await response.json(); expect(body).toEqual({}); - expect(mockChat.processAction).toHaveBeenCalled(); + expect(mockChat).toHaveDispatched("processAction"); }); it("should handle non-message events gracefully", async () => { @@ -936,7 +917,7 @@ describe("GoogleChatAdapter", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).toHaveBeenCalled(); + expect(mockChat).toHaveDispatched("processMessage"); }); it("should skip unsupported Pub/Sub event types", async () => { @@ -961,7 +942,7 @@ describe("GoogleChatAdapter", () => { expect(response.status).toBe(200); // Updated event not in allowed list, should not process - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); it("should handle malformed Pub/Sub data gracefully", async () => { @@ -1059,7 +1040,7 @@ describe("GoogleChatAdapter", () => { }); await adapter.handleWebhook(request); - expect(mockChat.processAction).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processAction"); }); it("should use invokedFunction as actionId", async () => { @@ -1213,7 +1194,7 @@ describe("GoogleChatAdapter", () => { }); await adapter.handleWebhook(request); - expect(mockChat.processAction).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processAction"); }); }); @@ -1247,7 +1228,7 @@ describe("GoogleChatAdapter", () => { await adapter.handleWebhook(request); - expect(mockChat.processMessage).toHaveBeenCalled(); + expect(mockChat).toHaveDispatched("processMessage"); const call = (mockChat.processMessage as any).mock.calls[0]; // threadId for DM should end with :dm and not include thread name expect(call[1]).toMatch(DM_SUFFIX_PATTERN); @@ -1266,7 +1247,7 @@ describe("GoogleChatAdapter", () => { await adapter.handleWebhook(request); - expect(mockChat.processMessage).toHaveBeenCalled(); + expect(mockChat).toHaveDispatched("processMessage"); const call = (mockChat.processMessage as any).mock.calls[0]; // threadId should NOT end with :dm expect(call[1]).not.toMatch(DM_SUFFIX_PATTERN); @@ -1752,12 +1733,7 @@ describe("GoogleChatAdapter", () => { }); it("should log context information", () => { - const localLogger: Logger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; + const localLogger = createMockLogger(); const adapter = createGoogleChatAdapter({ credentials: TEST_CREDENTIALS, logger: localLogger, @@ -1835,18 +1811,13 @@ describe("GoogleChatAdapter", () => { describe("onThreadSubscribe", () => { it("should warn when no pubsubTopic configured", async () => { - const localLogger: Logger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; + const localLogger = createMockLogger(); const adapter = createGoogleChatAdapter({ credentials: TEST_CREDENTIALS, logger: localLogger, }); const mockState = createMockStateAdapter(); - const mockChat = createMockChatInstance(mockState); + const mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); const threadId = adapter.encodeThreadId({ @@ -2687,7 +2658,7 @@ describe("GoogleChatAdapter", () => { logger: mockLogger, }); mockState = createMockStateAdapter(); - mockChat = createMockChatInstance(mockState); + mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); }); @@ -2960,7 +2931,7 @@ describe("GoogleChatAdapter", () => { disableSignatureVerification: true, }); const mockState = createMockStateAdapter(); - const mockChat = createMockChatInstance(mockState); + const mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); const event = makeMessageEvent({ messageText: "Hello" }); @@ -2991,7 +2962,7 @@ describe("GoogleChatAdapter", () => { googleChatProjectNumber: "123456789", }); const mockState = createMockStateAdapter(); - const mockChat = createMockChatInstance(mockState); + const mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); const pubsubMessage = makePubSubPushMessage({ @@ -3029,7 +3000,7 @@ describe("GoogleChatAdapter", () => { pubsubAudience: "https://example.com/webhook/pubsub", }); const mockState = createMockStateAdapter(); - const mockChat = createMockChatInstance(mockState); + const mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); const event = makeMessageEvent({ messageText: "Hello" }); @@ -3080,7 +3051,7 @@ describe("GoogleChatAdapter", () => { disableSignatureVerification: true, }); const mockState = createMockStateAdapter(); - const mockChat = createMockChatInstance(mockState); + const mockChat = createMockChatInstance({ state: mockState }); await adapter.initialize(mockChat); const pubsubMessage = makePubSubPushMessage({ diff --git a/packages/adapter-gchat/src/user-info.test.ts b/packages/adapter-gchat/src/user-info.test.ts index c1335c369..0ccde8538 100644 --- a/packages/adapter-gchat/src/user-info.test.ts +++ b/packages/adapter-gchat/src/user-info.test.ts @@ -1,41 +1,7 @@ -import type { Logger, StateAdapter } from "chat"; -import { describe, expect, it, vi } from "vitest"; +import { createMockState, mockLogger } from "@chat-adapter/tests"; +import { describe, expect, it } from "vitest"; import { UserInfoCache } from "./user-info"; -const mockLogger: Logger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - child: () => mockLogger, -}; - -function createMockState() { - const cache = new Map(); - return { - cache, - connect: vi.fn().mockResolvedValue(undefined), - disconnect: vi.fn().mockResolvedValue(undefined), - subscribe: vi.fn(), - unsubscribe: vi.fn(), - isSubscribed: vi.fn(), - acquireLock: vi.fn(), - releaseLock: vi.fn(), - extendLock: vi.fn(), - get: vi.fn().mockImplementation((key: string) => { - return Promise.resolve(cache.get(key) ?? null); - }), - set: vi.fn().mockImplementation((key: string, value: unknown) => { - cache.set(key, value); - return Promise.resolve(); - }), - delete: vi.fn().mockImplementation((key: string) => { - cache.delete(key); - return Promise.resolve(); - }), - } as unknown as StateAdapter & { cache: Map }; -} - describe("UserInfoCache", () => { describe("set", () => { it("should store in-memory and persist to state", async () => { diff --git a/packages/adapter-gchat/vitest.config.ts b/packages/adapter-gchat/vitest.config.ts index edc2d946b..18e0de9ae 100644 --- a/packages/adapter-gchat/vitest.config.ts +++ b/packages/adapter-gchat/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-github/src/index.test.ts b/packages/adapter-github/src/index.test.ts index 38aff08a1..d73db7a67 100644 --- a/packages/adapter-github/src/index.test.ts +++ b/packages/adapter-github/src/index.test.ts @@ -1,6 +1,11 @@ import type { AsyncLocalStorage } from "node:async_hooks"; import { createHmac } from "node:crypto"; -import { connectWebhookContract } from "@chat-adapter/tests"; +import { + connectWebhookContract, + createMockChatInstance, + createMockState, + mockLogger, +} from "@chat-adapter/tests"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createGitHubAdapter, GitHubAdapter } from "./index"; import type { @@ -84,13 +89,6 @@ vi.mock("@octokit/auth-app", () => ({ // ─── Helpers ───────────────────────────────────────────────────────────────── -const mockLogger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), -}; - const WEBHOOK_SECRET = "test-secret"; const INSTALLATION_ERROR_PATTERN = /installation/i; const EMPTY_INSTALLATION_TOKEN_PATTERN = /empty installation token/i; @@ -193,25 +191,12 @@ function makeWebhookRequest( }); } -function createMockState() { - const cache = new Map(); - - return { - get: vi.fn(async (key: string) => { - return (cache.get(key) as T | undefined) ?? null; - }), - set: vi.fn(async (key: string, value: unknown) => { - cache.set(key, value); - }), - }; -} - function stringifyLoggerCalls(): string { return JSON.stringify({ - debug: mockLogger.debug.mock.calls, - error: mockLogger.error.mock.calls, - info: mockLogger.info.mock.calls, - warn: mockLogger.warn.mock.calls, + debug: vi.mocked(mockLogger.debug).mock.calls, + error: vi.mocked(mockLogger.error).mock.calls, + info: vi.mocked(mockLogger.info).mock.calls, + warn: vi.mocked(mockLogger.warn).mock.calls, }); } @@ -403,13 +388,7 @@ describe("GitHubAdapter", () => { data: { id: 777, login: "test-bot" }, }); - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); await adapter.initialize(mockChat); @@ -425,13 +404,7 @@ describe("GitHubAdapter", () => { new Error("Bad credentials") ); - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); await adapter.initialize(mockChat); @@ -451,13 +424,7 @@ describe("GitHubAdapter", () => { logger: mockLogger, }); - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); await a.initialize(mockChat); @@ -516,13 +483,7 @@ describe("GitHubAdapter", () => { logger: mockLogger, }); const state = createMockState(); - const chat = { - getLogger: vi.fn(), - getState: vi.fn(() => state), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const chat = createMockChatInstance({ state }); await multiTenantAdapter.initialize(chat); const payload = makeIssueCommentPayload({ @@ -549,13 +510,7 @@ describe("GitHubAdapter", () => { logger: mockLogger, }); const state = createMockState(); - const chat = { - getLogger: vi.fn(), - getState: vi.fn(() => state), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const chat = createMockChatInstance({ state }); await multiTenantAdapter.initialize(chat); await expect( @@ -708,13 +663,7 @@ describe("GitHubAdapter", () => { }); it("should not log raw payload content for valid webhooks", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); mockUsersGetAuthenticated.mockResolvedValueOnce({ data: { id: 777, login: "test-bot" }, }); @@ -768,13 +717,7 @@ describe("GitHubAdapter", () => { }); it("should process issue_comment on PR with valid signature", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); mockUsersGetAuthenticated.mockResolvedValueOnce({ data: { id: 777, login: "test-bot" }, @@ -797,13 +740,7 @@ describe("GitHubAdapter", () => { }); it("should process issue_comment on a plain issue", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); mockUsersGetAuthenticated.mockResolvedValueOnce({ data: { id: 777, login: "test-bot" }, }); @@ -830,13 +767,7 @@ describe("GitHubAdapter", () => { }); it("should ignore issue_comment with action other than created", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); mockUsersGetAuthenticated.mockResolvedValueOnce({ data: { id: 777, login: "test-bot" }, }); @@ -849,17 +780,11 @@ describe("GitHubAdapter", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); it("should process pull_request_review_comment event", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); mockUsersGetAuthenticated.mockResolvedValueOnce({ data: { id: 777, login: "test-bot" }, }); @@ -886,13 +811,7 @@ describe("GitHubAdapter", () => { }); it("should use in_reply_to_id as root for review comment replies", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); mockUsersGetAuthenticated.mockResolvedValueOnce({ data: { id: 777, login: "test-bot" }, }); @@ -925,13 +844,7 @@ describe("GitHubAdapter", () => { }); it("should ignore review_comment with action other than created", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); mockUsersGetAuthenticated.mockResolvedValueOnce({ data: { id: 777, login: "test-bot" }, }); @@ -948,7 +861,7 @@ describe("GitHubAdapter", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); it("should return ok for unrecognized event types", async () => { @@ -971,13 +884,7 @@ describe("GitHubAdapter", () => { logger: mockLogger, }); const state = createMockState(); - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(() => state), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance({ state }); await multiTenantAdapter.initialize(mockChat); const body = JSON.stringify({ @@ -1027,13 +934,7 @@ describe("GitHubAdapter", () => { describe("self-message detection", () => { it("should ignore messages from the bot itself (issue comment)", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); // Set bot user ID to 777 mockUsersGetAuthenticated.mockResolvedValueOnce({ @@ -1055,17 +956,11 @@ describe("GitHubAdapter", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); it("should ignore messages from the bot itself (review comment)", async () => { - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance(); mockUsersGetAuthenticated.mockResolvedValueOnce({ data: { id: 777, login: "test-bot" }, @@ -1085,7 +980,7 @@ describe("GitHubAdapter", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); it("should auto-detect botUserId on first webhook in multi-tenant mode (regression for self-reply loop)", async () => { @@ -1096,13 +991,7 @@ describe("GitHubAdapter", () => { // always false and the bot would re-process its own replies in an // unbounded loop. const state = createMockState(); - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(() => state), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance({ state }); const multiTenantAdapter = new GitHubAdapter({ appId: "12345", @@ -1139,7 +1028,7 @@ describe("GitHubAdapter", () => { // botUserId must be set after the first webhook so isMe works. expect(multiTenantAdapter.botUserId).toBe("777"); // The bot's own reply must NOT be dispatched to handlers. - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); it("should fall back to apps.getAuthenticated when users.getAuthenticated rejects (multi-tenant App)", async () => { @@ -1147,13 +1036,7 @@ describe("GitHubAdapter", () => { // available with installation tokens depending on permissions. Fall // back to /app + /users/{slug}[bot] to recover the bot user ID. const state = createMockState(); - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(() => state), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; + const mockChat = createMockChatInstance({ state }); const multiTenantAdapter = new GitHubAdapter({ appId: "12345", diff --git a/packages/adapter-github/vitest.config.ts b/packages/adapter-github/vitest.config.ts index edc2d946b..18e0de9ae 100644 --- a/packages/adapter-github/vitest.config.ts +++ b/packages/adapter-github/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-linear/src/index.test.ts b/packages/adapter-linear/src/index.test.ts index 25a899b1b..c7b47a333 100644 --- a/packages/adapter-linear/src/index.test.ts +++ b/packages/adapter-linear/src/index.test.ts @@ -1,8 +1,12 @@ import type { AsyncLocalStorage } from "node:async_hooks"; import { createHmac } from "node:crypto"; -import { connectWebhookContract } from "@chat-adapter/tests"; +import { + connectWebhookContract, + createMockChatInstance, + createMockLogger, + createMockState, +} from "@chat-adapter/tests"; import type { LinearClient } from "@linear/sdk"; -import type { ChatInstance, StateAdapter } from "chat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { LinearAgentSessionCommentRawMessage, @@ -20,16 +24,6 @@ afterEach(() => { vi.unstubAllGlobals(); }); -/** Mock logger that captures calls */ -function createMockLogger() { - return { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }; -} - function createRawCommentMessage( overrides?: Partial & { user?: Partial; @@ -153,55 +147,6 @@ function expectAgentSessionCommentRawMessage(raw: { return raw as LinearAgentSessionCommentRawMessage; } -function createMockState(): StateAdapter & { cache: Map } { - const cache = new Map(); - return { - cache, - connect: vi.fn().mockResolvedValue(undefined), - disconnect: vi.fn().mockResolvedValue(undefined), - subscribe: vi.fn().mockResolvedValue(undefined), - unsubscribe: vi.fn().mockResolvedValue(undefined), - isSubscribed: vi.fn().mockResolvedValue(false), - acquireLock: vi.fn().mockResolvedValue(null), - releaseLock: vi.fn().mockResolvedValue(undefined), - extendLock: vi.fn().mockResolvedValue(true), - get: vi.fn().mockImplementation((key: string) => { - return Promise.resolve(cache.get(key) ?? null); - }), - set: vi.fn().mockImplementation((key: string, value: unknown) => { - cache.set(key, value); - return Promise.resolve(); - }), - delete: vi.fn().mockImplementation((key: string) => { - cache.delete(key); - return Promise.resolve(); - }), - appendToList: vi.fn().mockResolvedValue(undefined), - getList: vi.fn().mockResolvedValue([]), - }; -} - -function createMockChatInstance( - state: StateAdapter, - logger = createMockLogger(), - userName = "test-bot" -): ChatInstance { - return { - processMessage: vi.fn(), - handleIncomingMessage: vi.fn().mockResolvedValue(undefined), - processReaction: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processModalSubmit: vi.fn().mockResolvedValue(undefined), - processModalClose: vi.fn(), - processSlashCommand: vi.fn(), - processMemberJoinedChannel: vi.fn(), - getState: () => state, - getUserName: () => userName, - getLogger: () => logger, - }; -} - /** * Create a minimal LinearAdapter for testing thread ID methods. * We pass a dummy apiKey - it won't be used for encoding/decoding. @@ -1140,7 +1085,10 @@ describe("Vercel Connect mode", () => { }); setBotUserId(adapter, "bot-user-id"); setDefaultOrganizationId(adapter, "org-123"); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload(); @@ -1229,7 +1177,10 @@ describe("Vercel Connect mode", () => { internal.defaultOrganizationId = "org-123"; return Promise.resolve(); }); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const request = buildWebhookRequest(JSON.stringify(createCommentPayload())); @@ -1237,7 +1188,7 @@ describe("Vercel Connect mode", () => { expect(response.status).toBe(200); expect(resolveSpy).toHaveBeenCalled(); - expect(mockChat.processMessage).toHaveBeenCalled(); + expect(mockChat).toHaveDispatched("processMessage"); }); it("returns 500 when bot identity cannot be resolved (no silent empty id)", async () => { @@ -1253,7 +1204,10 @@ describe("Vercel Connect mode", () => { resolveConnectIdentity: () => Promise; }; vi.spyOn(internal, "resolveConnectIdentity").mockResolvedValue(undefined); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const request = buildWebhookRequest(JSON.stringify(createCommentPayload())); @@ -1262,7 +1216,7 @@ describe("Vercel Connect mode", () => { // M1 guard turns the thrown identity error into a clean 500 (not an // unhandled rejection), and no message is dispatched with an empty id. expect(response.status).toBe(500); - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); }); @@ -1270,7 +1224,10 @@ describe("handleWebhook - comment created", () => { it("should process comment create events via chat.processMessage", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload(); @@ -1294,7 +1251,10 @@ describe("handleWebhook - comment created", () => { it("should use parentId as root comment when present (threaded reply)", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload({ @@ -1334,7 +1294,7 @@ describe("handleWebhook - comment created", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); it("should skip comments without issueId", async () => { @@ -1358,7 +1318,7 @@ describe("handleWebhook - comment created", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); expect(logger.debug).toHaveBeenCalledWith( "Ignoring non-issue comment", expect.any(Object) @@ -1368,7 +1328,10 @@ describe("handleWebhook - comment created", () => { it("should mark bot-authored comments as self", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; setBotUserId(adapter, "bot-user-id"); @@ -1407,7 +1370,7 @@ describe("handleWebhook - agent session events", () => { it("ignores agent session events in comment mode", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger, "comments"); - const chat = createMockChatInstance(createMockState(), logger); + const chat = createMockChatInstance({ state: createMockState(), logger }); (adapter as unknown as { chat: typeof chat }).chat = chat; const payload = createAgentSessionPayload(); @@ -1417,7 +1380,7 @@ describe("handleWebhook - agent session events", () => { ); expect(response.status).toBe(200); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); expect(logger.warn).toHaveBeenCalledWith( "Received AgentSessionEvent webhook but adapter is not in agent-sessions mode, ignoring" ); @@ -1426,7 +1389,7 @@ describe("handleWebhook - agent session events", () => { it("dispatches created events in agent-session mode when owned by this bot", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger, "agent-sessions"); - const chat = createMockChatInstance(createMockState(), logger); + const chat = createMockChatInstance({ state: createMockState(), logger }); (adapter as unknown as { chat: typeof chat }).chat = chat; const payload = createAgentSessionPayload(); @@ -1449,7 +1412,7 @@ describe("handleWebhook - agent session events", () => { it("routes prompted events to the same session thread without mention flag", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger, "agent-sessions"); - const chat = createMockChatInstance(createMockState(), logger); + const chat = createMockChatInstance({ state: createMockState(), logger }); (adapter as unknown as { chat: typeof chat }).chat = chat; const payload = createAgentSessionPayload({ @@ -1461,13 +1424,13 @@ describe("handleWebhook - agent session events", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); }); it("falls back to the bot author when a created session has no creator", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger, "agent-sessions"); - const chat = createMockChatInstance(createMockState(), logger); + const chat = createMockChatInstance({ state: createMockState(), logger }); (adapter as unknown as { chat: typeof chat }).chat = chat; const payload = createAgentSessionPayload({ @@ -1490,7 +1453,7 @@ describe("handleWebhook - agent session events", () => { it("ignores comment webhooks in agent-session mode", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger, "agent-sessions"); - const chat = createMockChatInstance(createMockState(), logger); + const chat = createMockChatInstance({ state: createMockState(), logger }); (adapter as unknown as { chat: typeof chat }).chat = chat; const commentPayload = createCommentPayload({ @@ -1503,13 +1466,13 @@ describe("handleWebhook - agent session events", () => { ); expect(response.status).toBe(200); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); }); it("does not emit an automatic acknowledgement for created events", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger, "agent-sessions"); - const chat = createMockChatInstance(createMockState(), logger); + const chat = createMockChatInstance({ state: createMockState(), logger }); (adapter as unknown as { chat: typeof chat }).chat = chat; setDefaultOrganizationId(adapter, "org-123"); @@ -1534,7 +1497,7 @@ describe("handleWebhook - agent session events", () => { it("ignores created events that belong to another bot", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger, "agent-sessions"); - const chat = createMockChatInstance(createMockState(), logger); + const chat = createMockChatInstance({ state: createMockState(), logger }); (adapter as unknown as { chat: typeof chat }).chat = chat; const payload = createAgentSessionPayload({ @@ -1546,7 +1509,7 @@ describe("handleWebhook - agent session events", () => { ); expect(response.status).toBe(200); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); expect(logger.warn).toHaveBeenCalledWith( "Ignoring agent session event from another bot", expect.objectContaining({ @@ -1560,7 +1523,7 @@ describe("handleWebhook - agent session events", () => { const logger = createMockLogger(); const adapter = createMultiTenantAdapter(logger, "agent-sessions"); const state = createMockState(); - const chat = createMockChatInstance(state, logger); + const chat = createMockChatInstance({ state, logger }); await adapter.initialize(chat); await adapter.setInstallation("org-123", createInstallation()); @@ -1586,7 +1549,7 @@ describe("handleWebhook - agent session events", () => { const logger = createMockLogger(); const adapter = createMultiTenantAdapter(logger, "agent-sessions"); const state = createMockState(); - const chat = createMockChatInstance(state, logger); + const chat = createMockChatInstance({ state, logger }); await adapter.initialize(chat); await adapter.setInstallation("org-123", createInstallation()); @@ -1600,7 +1563,7 @@ describe("handleWebhook - agent session events", () => { ); expect(response.status).toBe(200); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); }); it("ignores comment webhooks in agent-session mode even if they mention chat userName", async () => { @@ -1615,7 +1578,11 @@ describe("handleWebhook - agent session events", () => { }) ); const state = createMockState(); - const chat = createMockChatInstance(state, logger, "getsquad-dev-samy"); + const chat = createMockChatInstance({ + state, + logger, + userName: "getsquad-dev-samy", + }); await adapter.initialize(chat); await adapter.setInstallation("org-123", createInstallation()); @@ -1629,7 +1596,7 @@ describe("handleWebhook - agent session events", () => { ); expect(response.status).toBe(200); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); }); }); @@ -1717,7 +1684,10 @@ describe("buildMessage via webhook", () => { it("should set author fields from webhook comment user", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload({ actorType: "user" }); @@ -1736,7 +1706,10 @@ describe("buildMessage via webhook", () => { it("should ignore application actor type for comment authors", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload({ actorType: "application" }); @@ -1752,7 +1725,10 @@ describe("buildMessage via webhook", () => { it("should ignore integration actor type for comment authors", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload({ actorType: "integration" }); @@ -1768,7 +1744,10 @@ describe("buildMessage via webhook", () => { it("should set dateSent from createdAt", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload(); @@ -1789,7 +1768,10 @@ describe("buildMessage via webhook", () => { it("should detect edited messages from differing createdAt/updatedAt", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload(); @@ -1810,7 +1792,10 @@ describe("buildMessage via webhook", () => { it("should include raw comment data in message", async () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); (adapter as unknown as { chat: typeof mockChat }).chat = mockChat; const payload = createCommentPayload({ body: "Some text" }); @@ -2706,7 +2691,10 @@ describe("initialize", () => { client: { rawRequest: mockRawRequest }, }); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); await adapter.initialize(mockChat); @@ -2740,7 +2728,10 @@ describe("initialize", () => { }); setDefaultClient(adapter, failingClient); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); await adapter.initialize(mockChat); @@ -2777,7 +2768,10 @@ describe("initialize", () => { organizationId: "org-123", } as never); - const mockChat = createMockChatInstance(createMockState(), logger); + const mockChat = createMockChatInstance({ + state: createMockState(), + logger, + }); await adapter.initialize(mockChat); @@ -3470,7 +3464,7 @@ describe("initialize", () => { const logger = createMockLogger(); const adapter = createWebhookAdapter(logger); const mockState = createMockState(); - const chat = createMockChatInstance(mockState, logger); + const chat = createMockChatInstance({ state: mockState, logger }); const mockRawRequest = vi.fn().mockResolvedValue({ data: { viewer: { @@ -3504,7 +3498,7 @@ describe("initialize", () => { const logger = createMockLogger(); const adapter = createClientCredentialsAdapter(logger); const state = createMockState(); - const chat = createMockChatInstance(state, logger); + const chat = createMockChatInstance({ state, logger }); const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => @@ -3616,7 +3610,7 @@ describe("multi-tenant installations", () => { const logger = createMockLogger(); const adapter = createMultiTenantAdapter(logger); const state = createMockState(); - const chat = createMockChatInstance(state, logger); + const chat = createMockChatInstance({ state, logger }); await adapter.initialize(chat); const mockFetch = vi.fn().mockResolvedValue({ @@ -3658,7 +3652,7 @@ describe("multi-tenant installations", () => { it("handleOAuthCallback rejects callback errors and missing codes", async () => { const adapter = createMultiTenantAdapter(); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await expect( adapter.handleOAuthCallback( @@ -3682,7 +3676,7 @@ describe("multi-tenant installations", () => { it("withInstallation refreshes expired installations and seeds request context", async () => { const adapter = createMultiTenantAdapter(); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation( "org-123", createInstallation({ @@ -3720,7 +3714,7 @@ describe("multi-tenant installations", () => { const logger = createMockLogger(); const adapter = createMultiTenantAdapter(logger); const state = createMockState(); - const chat = createMockChatInstance(state, logger); + const chat = createMockChatInstance({ state, logger }); await adapter.initialize(chat); await adapter.setInstallation("org-123", createInstallation()); @@ -3743,7 +3737,7 @@ describe("multi-tenant installations", () => { const logger = createMockLogger(); const adapter = createMultiTenantAdapter(logger); const state = createMockState(); - const chat = createMockChatInstance(state, logger); + const chat = createMockChatInstance({ state, logger }); await adapter.initialize(chat); const missingBody = JSON.stringify(createCommentPayload()); @@ -3790,7 +3784,7 @@ describe("multi-tenant installations", () => { encryptionKey: TEST_KEY_HEX, }) ); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation( "org-123", @@ -3832,7 +3826,7 @@ describe("multi-tenant installations", () => { it("stores plaintext when no encryptionKey is configured (legacy behavior)", async () => { const state = createMockState(); const adapter = createMultiTenantAdapter(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation( "org-123", createInstallation({ accessToken: "leak-me" }) @@ -3866,7 +3860,7 @@ describe("multi-tenant installations", () => { encryptionKey: TEST_KEY_HEX, }) ); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const fetched = await adapter.getInstallation("org-123"); expect(fetched?.accessToken).toBe("legacy-plaintext"); diff --git a/packages/adapter-linear/vitest.config.ts b/packages/adapter-linear/vitest.config.ts index edc2d946b..18e0de9ae 100644 --- a/packages/adapter-linear/vitest.config.ts +++ b/packages/adapter-linear/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-messenger/package.json b/packages/adapter-messenger/package.json index c94fa9ada..6ca46f738 100644 --- a/packages/adapter-messenger/package.json +++ b/packages/adapter-messenger/package.json @@ -31,6 +31,7 @@ "chat": "workspace:*" }, "devDependencies": { + "@chat-adapter/tests": "workspace:*", "@types/node": "^25.3.2", "tsup": "^8.3.5", "typescript": "^5.7.2", diff --git a/packages/adapter-messenger/src/index.test.ts b/packages/adapter-messenger/src/index.test.ts index 6abc84a45..95a4700ab 100644 --- a/packages/adapter-messenger/src/index.test.ts +++ b/packages/adapter-messenger/src/index.test.ts @@ -7,7 +7,8 @@ import { ValidationError as SharedValidationError, ValidationError, } from "@chat-adapter/shared"; -import type { ChatInstance, Logger } from "chat"; +import { createMockChatInstance, createMockLogger } from "@chat-adapter/tests"; +import type { ChatInstance } from "chat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createMessengerAdapter, @@ -26,13 +27,7 @@ function signPayload(body: string): string { return `sha256=${hash}`; } -const mockLogger: Logger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - child: vi.fn().mockReturnThis(), -}; +const mockLogger = createMockLogger(); const mockFetch = vi.fn(); @@ -53,21 +48,7 @@ function graphApiOk(result: unknown): Response { } function createMockChat(): ChatInstance { - return { - getLogger: vi.fn().mockReturnValue(mockLogger), - getState: vi.fn(), - getUserName: vi.fn().mockReturnValue("TestBot"), - handleIncomingMessage: vi.fn().mockResolvedValue(undefined), - processMessage: vi.fn(), - processReaction: vi.fn(), - processAction: vi.fn(), - processModalClose: vi.fn(), - processModalSubmit: vi.fn().mockResolvedValue(undefined), - processSlashCommand: vi.fn(), - processAssistantThreadStarted: vi.fn(), - processAssistantContextChanged: vi.fn(), - processAppHomeOpened: vi.fn(), - } as unknown as ChatInstance; + return createMockChatInstance({ logger: mockLogger, userName: "TestBot" }); } function sampleMessagingEvent( @@ -529,7 +510,7 @@ describe("MessengerAdapter", () => { }); await adapter.handleWebhook(request); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); }); it("caches echo messages", async () => { @@ -559,7 +540,7 @@ describe("MessengerAdapter", () => { }); await adapter.handleWebhook(request); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); const cached = await adapter.fetchMessage( "messenger:USER_123", "mid.echo1" diff --git a/packages/adapter-messenger/vitest.config.ts b/packages/adapter-messenger/vitest.config.ts index edc2d946b..18e0de9ae 100644 --- a/packages/adapter-messenger/vitest.config.ts +++ b/packages/adapter-messenger/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index 529fef454..3c782282c 100644 --- a/packages/adapter-slack/src/index.test.ts +++ b/packages/adapter-slack/src/index.test.ts @@ -4,14 +4,14 @@ import { createHmac, randomBytes } from "node:crypto"; import { AuthenticationError, ValidationError } from "@chat-adapter/shared"; -import { connectWebhookContract } from "@chat-adapter/tests"; +import { + connectWebhookContract, + createMockChatInstance, + createMockState, + mockLogger, +} from "@chat-adapter/tests"; import { WebClient } from "@slack/web-api"; -import type { - AdapterPostableMessage, - ChatInstance, - Logger, - StateAdapter, -} from "chat"; +import type { AdapterPostableMessage, ChatInstance, StateAdapter } from "chat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SlackInstallation } from "./index"; import { createSlackAdapter, SlackAdapter } from "./index"; @@ -37,14 +37,6 @@ vi.mock("@slack/socket-mode", () => { }; }); -const mockLogger: Logger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - child: vi.fn(() => mockLogger), -}; - // ============================================================================ // Test Helpers // ============================================================================ @@ -581,7 +573,7 @@ describe("handleWebhook - event_callback", () => { }) ); - const mockChat = createMockChatInstance(createMockState()); + const mockChat = createMockChatInstance({ state: createMockState() }); (adapter as unknown as { chat: ChatInstance }).chat = mockChat; const body = JSON.stringify({ @@ -632,7 +624,7 @@ describe("handleWebhook - event_callback", () => { }); mockClientMethod(adapter, "users.info", usersInfoMock); - const mockChat = createMockChatInstance(createMockState()); + const mockChat = createMockChatInstance({ state: createMockState() }); (adapter as unknown as { chat: ChatInstance }).chat = mockChat; const body = JSON.stringify({ @@ -775,7 +767,7 @@ describe("handleWebhook - interactive payloads", () => { it("responds with response_action: clear when handler returns clear", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); chatInstance.processModalSubmit = vi .fn() .mockResolvedValue({ action: "clear" }); @@ -835,7 +827,7 @@ describe("handleWebhook - interactive payloads", () => { it("handles block_suggestion payloads and returns options JSON", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); ( chatInstance.processOptionsLoad as ReturnType ).mockResolvedValue([{ label: "Maria Garcia", value: "person_123" }]); @@ -882,7 +874,7 @@ describe("handleWebhook - interactive payloads", () => { it("handles block_suggestion with option_groups response", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); ( chatInstance.processOptionsLoad as ReturnType ).mockResolvedValue([ @@ -933,7 +925,7 @@ describe("handleWebhook - interactive payloads", () => { vi.useFakeTimers(); try { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const timeoutAdapter = createSlackAdapter({ botToken: "xoxb-test-token", logger: mockLogger, @@ -1495,73 +1487,6 @@ describe("formatted text extraction", () => { }); }); -// ============================================================================ -// Multi-workspace Test Helpers -// ============================================================================ - -function createMockState(): StateAdapter & { cache: Map } { - const cache = new Map(); - return { - cache, - connect: vi.fn().mockResolvedValue(undefined), - disconnect: vi.fn().mockResolvedValue(undefined), - subscribe: vi.fn().mockResolvedValue(undefined), - unsubscribe: vi.fn().mockResolvedValue(undefined), - isSubscribed: vi.fn().mockResolvedValue(false), - acquireLock: vi.fn().mockResolvedValue(null), - releaseLock: vi.fn().mockResolvedValue(undefined), - extendLock: vi.fn().mockResolvedValue(true), - get: vi.fn().mockImplementation((key: string) => { - return Promise.resolve(cache.get(key) ?? null); - }), - set: vi.fn().mockImplementation((key: string, value: unknown) => { - cache.set(key, value); - return Promise.resolve(); - }), - delete: vi.fn().mockImplementation((key: string) => { - cache.delete(key); - return Promise.resolve(); - }), - appendToList: vi - .fn() - .mockImplementation( - ( - key: string, - value: unknown, - options?: { maxLength?: number; ttlMs?: number } - ) => { - let list = (cache.get(key) as unknown[]) ?? []; - list.push(value); - if (options?.maxLength && list.length > options.maxLength) { - list = list.slice(list.length - options.maxLength); - } - cache.set(key, list); - return Promise.resolve(); - } - ), - getList: vi.fn().mockImplementation((key: string) => { - return Promise.resolve((cache.get(key) as unknown[]) ?? []); - }), - }; -} - -function createMockChatInstance(state: StateAdapter): ChatInstance { - return { - processMessage: vi.fn(), - handleIncomingMessage: vi.fn().mockResolvedValue(undefined), - processReaction: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processModalSubmit: vi.fn().mockResolvedValue(undefined), - processModalClose: vi.fn(), - processSlashCommand: vi.fn(), - processMemberJoinedChannel: vi.fn(), - getState: () => state, - getUserName: () => "test-bot", - getLogger: () => mockLogger, - }; -} - // ============================================================================ // Multi-workspace Mode Tests // ============================================================================ @@ -1594,7 +1519,7 @@ describe("multi-workspace mode", () => { signingSecret: secret, logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const installation: SlackInstallation = { botToken: "xoxb-workspace-token", @@ -1614,7 +1539,7 @@ describe("multi-workspace mode", () => { signingSecret: secret, logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const result = await adapter.getInstallation("T_UNKNOWN"); expect(result).toBeNull(); @@ -1626,7 +1551,7 @@ describe("multi-workspace mode", () => { signingSecret: secret, logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation("T_TEAM_2", { botToken: "xoxb-token", @@ -1639,7 +1564,7 @@ describe("multi-workspace mode", () => { it("handleWebhook resolves token from state for event_callback", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -1667,12 +1592,12 @@ describe("multi-workspace mode", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); // processMessage should have been called (message was dispatched) - expect(chatInstance.processMessage).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processMessage"); }); it("handleWebhook resolves token for interactive payloads", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -1711,7 +1636,7 @@ describe("multi-workspace mode", () => { it("handleWebhook resolves token for block_suggestion payloads", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -1798,7 +1723,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -1837,7 +1762,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -1878,7 +1803,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -1915,7 +1840,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -1954,7 +1879,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -2000,7 +1925,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -2045,7 +1970,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -2089,7 +2014,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -2133,7 +2058,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -2179,7 +2104,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -2233,7 +2158,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -2282,7 +2207,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -2317,7 +2242,7 @@ describe("installationProvider", () => { }; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); // Invoke the factory while still inside the AsyncLocalStorage frame so // createAttachment can read the per-request enterprise context. The @@ -2394,7 +2319,7 @@ describe("multi-workspace mode with encryption", () => { logger: mockLogger, encryptionKey, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation("T_ENC_1", { botToken: "xoxb-secret-token", @@ -2422,7 +2347,7 @@ describe("multi-workspace mode with encryption", () => { logger: mockLogger, encryptionKey, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation("T_ENC_2", { botToken: "xoxb-encrypted-token", @@ -2461,7 +2386,7 @@ describe("installationKeyPrefix", () => { logger: mockLogger, installationKeyPrefix: "myapp:workspaces", }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation("T_CUSTOM_1", { botToken: "xoxb-token" }); @@ -2478,7 +2403,7 @@ describe("installationKeyPrefix", () => { signingSecret: secret, logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation("T_DEFAULT_1", { botToken: "xoxb-token" }); @@ -2526,7 +2451,7 @@ describe("handleOAuthCallback", () => { it("exchanges code for token and saves installation", async () => { const { adapter, state, mockAccess } = createOAuthAdapter(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const request = new Request( "https://example.com/auth/callback/slack?code=oauth-code-123" @@ -2551,7 +2476,7 @@ describe("handleOAuthCallback", () => { it("forwards redirect_uri from callback options", async () => { const { adapter, state, mockAccess } = createOAuthAdapter(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const request = new Request( "https://example.com/auth/callback/slack?code=oauth-code-123" @@ -2570,7 +2495,7 @@ describe("handleOAuthCallback", () => { it("prefers callback options redirect_uri over the query param", async () => { const { adapter, state, mockAccess } = createOAuthAdapter(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const request = new Request( "https://example.com/auth/callback/slack?code=oauth-code-123&redirect_uri=https%3A%2F%2Fexample.com%2Fquery-callback" @@ -2589,7 +2514,7 @@ describe("handleOAuthCallback", () => { it("falls back to redirect_uri from the callback query param", async () => { const { adapter, state, mockAccess } = createOAuthAdapter(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const request = new Request( "https://example.com/auth/callback/slack?code=oauth-code-123&redirect_uri=https%3A%2F%2Fexample.com%2Fquery-callback" @@ -2606,7 +2531,7 @@ describe("handleOAuthCallback", () => { it("throws when the callback code is missing", async () => { const { adapter, state } = createOAuthAdapter(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const request = new Request("https://example.com/auth/callback/slack"); await expect(adapter.handleOAuthCallback(request)).rejects.toThrow( @@ -2620,7 +2545,7 @@ describe("handleOAuthCallback", () => { signingSecret: secret, logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const request = new Request( "https://example.com/auth/callback/slack?code=test" @@ -2644,7 +2569,7 @@ describe("withBotToken", () => { signingSecret: secret, logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); let callbackRan = false; await adapter.withBotToken("xoxb-context-token", () => { @@ -2659,7 +2584,7 @@ describe("withBotToken", () => { signingSecret: secret, logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const tokens: string[] = []; @@ -2974,7 +2899,7 @@ describe("adapter.client end-to-end with multi-workspace webhook", () => { it("resolves the installation's bot token in the action handler context", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); // Capture the token observed by `event.adapter.client` when an action // handler runs inside the request context set up by handleWebhook. @@ -3014,7 +2939,7 @@ describe("adapter.client end-to-end with multi-workspace webhook", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(chatInstance.processAction).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processAction"); expect(observedToken).toBe("xoxb-installation-token"); }); }); @@ -3028,7 +2953,7 @@ describe("DM message handling", () => { it("top-level DM messages use empty threadTs (matches openDM subscriptions)", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -3062,7 +2987,7 @@ describe("DM message handling", () => { it("DM thread replies use parent thread_ts", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -3097,7 +3022,7 @@ describe("DM message handling", () => { it("DM messages do NOT have isMention set (routed via onDirectMessage)", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); // Capture the factory function to invoke it chatInstance.processMessage = vi.fn(); const adapter = createSlackAdapter({ @@ -3141,7 +3066,7 @@ describe("DM message handling", () => { it("channel messages do NOT have isMention auto-set", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); chatInstance.processMessage = vi.fn(); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", @@ -3190,7 +3115,7 @@ describe("message subtype handling", () => { it("allows file_share messages through to processMessage", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -3234,7 +3159,7 @@ describe("message subtype handling", () => { it("allows thread_broadcast messages through to processMessage", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -3269,7 +3194,7 @@ describe("message subtype handling", () => { it("ignores message_changed subtypes", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -3291,12 +3216,12 @@ describe("message subtype handling", () => { const request = createWebhookRequest(body, secret); await adapter.handleWebhook(request); - expect(chatInstance.processMessage).not.toHaveBeenCalled(); + expect(chatInstance).not.toHaveDispatched("processMessage"); }); it("ignores message_deleted subtypes", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -3318,12 +3243,12 @@ describe("message subtype handling", () => { const request = createWebhookRequest(body, secret); await adapter.handleWebhook(request); - expect(chatInstance.processMessage).not.toHaveBeenCalled(); + expect(chatInstance).not.toHaveDispatched("processMessage"); }); it("ignores channel_join subtypes", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -3346,7 +3271,7 @@ describe("message subtype handling", () => { const request = createWebhookRequest(body, secret); await adapter.handleWebhook(request); - expect(chatInstance.processMessage).not.toHaveBeenCalled(); + expect(chatInstance).not.toHaveDispatched("processMessage"); }); }); @@ -3377,7 +3302,7 @@ describe("handleWebhook - slash commands", () => { it("detects slash command payload (form-urlencoded with command field)", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); await adapter.initialize(chatInstance); const body = new URLSearchParams({ @@ -3395,12 +3320,12 @@ describe("handleWebhook - slash commands", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(chatInstance.processSlashCommand).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processSlashCommand"); }); it("passes command, text, user, and triggerId to processSlashCommand", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); await adapter.initialize(chatInstance); const body = new URLSearchParams({ @@ -3451,7 +3376,7 @@ describe("handleWebhook - slash commands", () => { it("returns 200 immediately for slash commands", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); await adapter.initialize(chatInstance); const body = new URLSearchParams({ @@ -3473,7 +3398,7 @@ describe("handleWebhook - slash commands", () => { it("handles slash command in multi-workspace mode", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const multiAdapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -3509,12 +3434,12 @@ describe("handleWebhook - slash commands", () => { const response = await multiAdapter.handleWebhook(request); expect(response.status).toBe(200); - expect(chatInstance.processSlashCommand).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processSlashCommand"); }); it("includes raw payload in event", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); await adapter.initialize(chatInstance); const body = new URLSearchParams({ @@ -4628,7 +4553,7 @@ describe("fetchMessages", () => { ); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const result = await adapter.fetchMessages("slack:C123:1234567890.000000", { direction: "forward", @@ -4681,7 +4606,7 @@ describe("fetchMessages", () => { ); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const result = await adapter.fetchMessages("slack:C123:1234567890.000000"); @@ -4720,7 +4645,7 @@ describe("fetchMessages", () => { ); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.fetchMessages("slack:C123:1234567890.000000", { cursor: "1000.000", @@ -4775,7 +4700,7 @@ describe("fetchMessage", () => { ); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const msg = await adapter.fetchMessage( "slack:C123:1234567890.000000", @@ -4812,7 +4737,7 @@ describe("fetchMessage", () => { ); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const msg = await adapter.fetchMessage( "slack:C123:1234567890.000000", @@ -4930,7 +4855,7 @@ describe("fetchChannelMessages", () => { ); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const result = await adapter.fetchChannelMessages("slack:C123"); @@ -4968,7 +4893,7 @@ describe("fetchChannelMessages", () => { ); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const result = await adapter.fetchChannelMessages("slack:C123", { direction: "forward", @@ -5088,7 +5013,7 @@ describe("listThreads", () => { ); const state = createMockState(); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); const result = await adapter.listThreads("slack:C123"); @@ -5153,7 +5078,7 @@ describe("ephemeral message ID encoding", () => { // Access private methods via the adapter for block_actions test // Block actions with ephemeral containers encode responseUrl in messageId const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); await adapter.initialize(chatInstance); // Simulate block_actions with is_ephemeral container @@ -5357,7 +5282,7 @@ describe("resolveInlineMentions", () => { it("resolves user mentions in incoming messages via webhook", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); chatInstance.processMessage = vi.fn(); const adapter = createSlackAdapter({ @@ -5416,7 +5341,7 @@ describe("resolveInlineMentions", () => { it("skips self-mention resolution in incoming webhooks", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); chatInstance.processMessage = vi.fn(); const adapter = createSlackAdapter({ @@ -5459,7 +5384,7 @@ describe("resolveInlineMentions", () => { it("skips request-scoped self mention resolution in multi-workspace mode", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ signingSecret: secret, logger: mockLogger, @@ -5494,7 +5419,7 @@ describe("resolveInlineMentions", () => { it("resolves bare channel mentions in incoming messages", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); chatInstance.processMessage = vi.fn(); const adapter = createSlackAdapter({ @@ -5560,7 +5485,7 @@ describe("resolveInlineMentions", () => { it("leaves channel mentions with existing labels unchanged", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); chatInstance.processMessage = vi.fn(); const adapter = createSlackAdapter({ @@ -5622,7 +5547,7 @@ describe("resolveInlineMentions", () => { it("falls back to channel ID when conversations.info fails", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); chatInstance.processMessage = vi.fn(); const adapter = createSlackAdapter({ @@ -5742,7 +5667,7 @@ describe("initialize", () => { }) ); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); expect(adapter.botUserId).toBe("U_INITIALIZED_BOT"); expect(adapter.userName).toBe("testbot"); @@ -5763,7 +5688,7 @@ describe("initialize", () => { ); // Should not throw - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); }); it("skips auth.test in multi-workspace mode", async () => { @@ -5776,7 +5701,7 @@ describe("initialize", () => { const authTestMock = vi.fn(); mockClientMethod(adapter, "auth.test", authTestMock); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); expect(authTestMock).not.toHaveBeenCalled(); }); @@ -5997,7 +5922,7 @@ describe("handleWebhook - assistant events", () => { it("handles assistant_thread_started event", async () => { const state = createMockState(); const chatInstance = { - ...createMockChatInstance(state), + ...createMockChatInstance({ state }), processAssistantThreadStarted: vi.fn(), }; const adapter = createSlackAdapter({ @@ -6043,7 +5968,7 @@ describe("handleWebhook - assistant events", () => { it("handles assistant_thread_context_changed event", async () => { const state = createMockState(); const chatInstance = { - ...createMockChatInstance(state), + ...createMockChatInstance({ state }), processAssistantContextChanged: vi.fn(), }; const adapter = createSlackAdapter({ @@ -6090,7 +6015,7 @@ describe("handleWebhook - assistant events", () => { it("handles app_home_opened event", async () => { const state = createMockState(); const chatInstance = { - ...createMockChatInstance(state), + ...createMockChatInstance({ state }), processAppHomeOpened: vi.fn(), }; const adapter = createSlackAdapter({ @@ -6129,7 +6054,7 @@ describe("handleWebhook - assistant events", () => { it("handles member_joined_channel event", async () => { const state = createMockState(); const chatInstance = { - ...createMockChatInstance(state), + ...createMockChatInstance({ state }), processMemberJoinedChannel: vi.fn(), }; const adapter = createSlackAdapter({ @@ -6411,7 +6336,7 @@ describe("reverse user lookup", () => { signingSecret: secret, logger: mockLogger, }); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); (adapter as unknown as MentionAdapter).chat = chatInstance; return { adapter, state }; } @@ -6903,7 +6828,7 @@ describe("reverse user lookup", () => { bot_id: "B_BOT", }) ); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); // Seed user cache await state.set( @@ -6949,7 +6874,7 @@ describe("reverse user lookup", () => { clientSecret: "client-secret", logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.setInstallation("T_MULTI_1", { botToken: "xoxb-multi-workspace-token", @@ -7094,7 +7019,7 @@ describe("socket mode - initialize", () => { logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); expect( (MockedClient as unknown as { lastOpts: Record }) @@ -7117,7 +7042,7 @@ describe("socket mode - routeSocketEvent", () => { mockSocketDisconnect.mockClear(); const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ mode: "socket", appToken: "xapp-test-token", @@ -7157,7 +7082,7 @@ describe("socket mode - routeSocketEvent", () => { }, }); - expect(chatInstance.processMessage).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processMessage"); }); it("dispatches slash_commands to processSlashCommand", async () => { @@ -7175,7 +7100,7 @@ describe("socket mode - routeSocketEvent", () => { }); await vi.waitFor(() => { - expect(chatInstance.processSlashCommand).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processSlashCommand"); }); }); @@ -7206,7 +7131,7 @@ describe("socket mode - routeSocketEvent", () => { }, }); - expect(chatInstance.processAction).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processAction"); }); it("acks interactive with response payload for view_submission", async () => { @@ -7255,7 +7180,7 @@ describe("socket mode - routeSocketEvent", () => { // routed normally; Chat.processMessage dedupes by message id when the // original WAS handled. expect(ack).toHaveBeenCalled(); - expect(chatInstance.processMessage).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processMessage"); }); it("acks events_api before processing", async () => { @@ -7309,7 +7234,7 @@ describe("socket mode - disconnect", () => { logger: mockLogger, }); - await adapter.initialize(createMockChatInstance(state)); + await adapter.initialize(createMockChatInstance({ state })); await adapter.disconnect(); expect(mockSocketDisconnect).toHaveBeenCalled(); @@ -7337,7 +7262,7 @@ describe("socket mode forwarding - handleWebhook", () => { it("accepts forwarded event with valid appToken", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -7372,7 +7297,7 @@ describe("socket mode forwarding - handleWebhook", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(chatInstance.processMessage).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processMessage"); }); it("rejects forwarded event with invalid token", async () => { @@ -7433,7 +7358,7 @@ describe("socket mode forwarding - handleWebhook", () => { it("accepts forwarded event with dedicated socketForwardingSecret", async () => { const forwardingSecret = "my-forwarding-secret"; const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -7564,7 +7489,7 @@ describe("socket mode forwarding - handleWebhook", () => { it("bypasses signature verification for forwarded events", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -7603,7 +7528,7 @@ describe("socket mode forwarding - handleWebhook", () => { it("passes options through to handlers for forwarded events", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -7707,7 +7632,7 @@ describe("routeSocketEvent with options", () => { mockSocketDisconnect.mockClear(); const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ mode: "socket", appToken: "xapp-test-token", @@ -7746,13 +7671,13 @@ describe("routeSocketEvent with options", () => { }); await vi.waitFor(() => { - expect(chatInstance.processSlashCommand).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processSlashCommand"); }); }); it("forwards slash_commands with eventType in forwarded envelope", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -7786,13 +7711,13 @@ describe("routeSocketEvent with options", () => { expect(response.status).toBe(200); await vi.waitFor(() => { - expect(chatInstance.processSlashCommand).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processSlashCommand"); }); }); it("forwards interactive payloads with eventType in forwarded envelope", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -7831,7 +7756,7 @@ describe("routeSocketEvent with options", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(chatInstance.processAction).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processAction"); }); }); @@ -8020,7 +7945,7 @@ describe("scheduleMessage with empty threadTs", () => { describe("getUser", () => { it("should return user info with email and avatar", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -8059,7 +7984,7 @@ describe("getUser", () => { it("should return null when API fails", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -8079,7 +8004,7 @@ describe("getUser", () => { it("should return null when user not found", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -8099,7 +8024,7 @@ describe("getUser", () => { it("should return isBot true for bot users", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -8132,7 +8057,7 @@ describe("getUser", () => { it("should fall through to real_name when display_name is empty", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -8167,7 +8092,7 @@ describe("getUser", () => { it("should call users.info with correct user ID", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -8197,7 +8122,7 @@ describe("getUser", () => { it("should return cached user without hitting API", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: "test-secret", @@ -8229,7 +8154,7 @@ describe("link unfurl enrichment", () => { it("should enrich links with metadata from attachments", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -8274,7 +8199,7 @@ describe("link unfurl enrichment", () => { const request = createWebhookRequest(body, secret); await adapter.handleWebhook(request); - expect(chatInstance.processMessage).toHaveBeenCalled(); + expect(chatInstance).toHaveDispatched("processMessage"); const factory = (chatInstance.processMessage as ReturnType) .mock.calls[0][2] as () => Promise<{ links: Array<{ @@ -8299,7 +8224,7 @@ describe("link unfurl enrichment", () => { it("should extract links from attachments even without blocks", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -8343,7 +8268,7 @@ describe("link unfurl enrichment", () => { it("should return bare links when no attachments present", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -8389,7 +8314,7 @@ describe("link unfurl enrichment", () => { it("should match unfurl metadata with trailing slash differences", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -8443,7 +8368,7 @@ describe("link unfurl enrichment", () => { it("should not re-dispatch message_changed as a new message", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -8478,12 +8403,12 @@ describe("link unfurl enrichment", () => { const request = createWebhookRequest(body, secret); await adapter.handleWebhook(request); - expect(chatInstance.processMessage).not.toHaveBeenCalled(); + expect(chatInstance).not.toHaveDispatched("processMessage"); }); it("should ignore message_changed without unfurl attachments", async () => { const state = createMockState(); - const chatInstance = createMockChatInstance(state); + const chatInstance = createMockChatInstance({ state }); const adapter = createSlackAdapter({ botToken: "xoxb-test-token", signingSecret: secret, @@ -8512,7 +8437,7 @@ describe("link unfurl enrichment", () => { const request = createWebhookRequest(body, secret); await adapter.handleWebhook(request); - expect(chatInstance.processMessage).not.toHaveBeenCalled(); + expect(chatInstance).not.toHaveDispatched("processMessage"); }); }); diff --git a/packages/adapter-slack/vitest.config.ts b/packages/adapter-slack/vitest.config.ts index edc2d946b..18e0de9ae 100644 --- a/packages/adapter-slack/vitest.config.ts +++ b/packages/adapter-slack/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-teams/package.json b/packages/adapter-teams/package.json index 78bfd001f..2cb54fd74 100644 --- a/packages/adapter-teams/package.json +++ b/packages/adapter-teams/package.json @@ -60,6 +60,7 @@ }, "devDependencies": { "@microsoft/teams.graph": "^2.0.8", + "@chat-adapter/tests": "workspace:*", "@types/node": "^25.3.2", "tsup": "^8.3.5", "typescript": "^5.7.2", diff --git a/packages/adapter-teams/src/index.test.ts b/packages/adapter-teams/src/index.test.ts index 6f46572a1..a1f726c0d 100644 --- a/packages/adapter-teams/src/index.test.ts +++ b/packages/adapter-teams/src/index.test.ts @@ -2,6 +2,7 @@ import { execSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { AuthenticationError, ValidationError } from "@chat-adapter/shared"; +import { createMockChatInstance, createMockState } from "@chat-adapter/tests"; import { ConsoleLogger } from "chat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createTeamsAdapter, TeamsAdapter } from "./index"; @@ -788,18 +789,8 @@ describe("TeamsAdapter", () => { logger, }); - const mockChat = { - getState: vi.fn(), - processMessage: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processReaction: vi.fn(), - }; - // initialize() calls app.initialize() which registers the bridge route handler - await adapter.initialize( - mockChat as unknown as Parameters[0] - ); + await adapter.initialize(createMockChatInstance()); expect(adapter.name).toBe("teams"); }); @@ -1002,27 +993,12 @@ describe("TeamsAdapter", () => { logger, }); - const mockState = { - get: vi.fn(async () => null), - set: vi.fn(async () => undefined), - delete: vi.fn(async () => undefined), - }; - const mockChat = { - getState: () => mockState, - processMessage: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processReaction: vi.fn(), - }; - // Mock app.initialize to avoid real HTTP setup const mockApp = ( adapter as unknown as { app: { initialize: ReturnType } } ).app; mockApp.initialize = vi.fn(async () => undefined); - await adapter.initialize( - mockChat as unknown as Parameters[0] - ); + await adapter.initialize(createMockChatInstance()); await expect(adapter.openDM("user-123")).rejects.toThrow(ValidationError); }); @@ -1040,22 +1016,9 @@ describe("TeamsAdapter", () => { logger, }); - const mockState = { - get: vi.fn(async (key: string) => { - if (key === "teams:aadObjectId:29:user-123") { - return "aad-object-id-456"; - } - return null; - }), - set: vi.fn(async () => undefined), - delete: vi.fn(async () => undefined), - }; - const mockChat = { - getState: () => mockState, - processMessage: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - }; + const state = createMockState(); + state.cache.set("teams:aadObjectId:29:user-123", "aad-object-id-456"); + const mockChat = createMockChatInstance({ state }); const mockApp = ( adapter as unknown as { @@ -1075,9 +1038,7 @@ describe("TeamsAdapter", () => { })), }; - await adapter.initialize( - mockChat as unknown as Parameters[0] - ); + await adapter.initialize(mockChat); const user = await adapter.getUser("29:user-123"); expect(user).not.toBeNull(); @@ -1095,17 +1056,7 @@ describe("TeamsAdapter", () => { logger, }); - const mockState = { - get: vi.fn(async () => null), - set: vi.fn(async () => undefined), - delete: vi.fn(async () => undefined), - }; - const mockChat = { - getState: () => mockState, - processMessage: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - }; + const mockChat = createMockChatInstance(); const mockApp = ( adapter as unknown as { @@ -1114,9 +1065,7 @@ describe("TeamsAdapter", () => { ).app; mockApp.initialize = vi.fn(async () => undefined); - await adapter.initialize( - mockChat as unknown as Parameters[0] - ); + await adapter.initialize(mockChat); const user = await adapter.getUser("29:unknown-user"); expect(user).toBeNull(); @@ -1129,22 +1078,9 @@ describe("TeamsAdapter", () => { logger, }); - const mockState = { - get: vi.fn(async (key: string) => { - if (key === "teams:aadObjectId:29:user-123") { - return "aad-object-id-456"; - } - return null; - }), - set: vi.fn(async () => undefined), - delete: vi.fn(async () => undefined), - }; - const mockChat = { - getState: () => mockState, - processMessage: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - }; + const state = createMockState(); + state.cache.set("teams:aadObjectId:29:user-123", "aad-object-id-456"); + const mockChat = createMockChatInstance({ state }); const mockApp = ( adapter as unknown as { @@ -1161,9 +1097,7 @@ describe("TeamsAdapter", () => { }), }; - await adapter.initialize( - mockChat as unknown as Parameters[0] - ); + await adapter.initialize(mockChat); const user = await adapter.getUser("29:user-123"); expect(user).toBeNull(); @@ -1176,22 +1110,9 @@ describe("TeamsAdapter", () => { logger, }); - const mockState = { - get: vi.fn(async (key: string) => { - if (key === "teams:aadObjectId:29:user-123") { - return "aad-object-id-456"; - } - return null; - }), - set: vi.fn(async () => undefined), - delete: vi.fn(async () => undefined), - }; - const mockChat = { - getState: () => mockState, - processMessage: vi.fn(), - processAction: vi.fn(), - processReaction: vi.fn(), - }; + const state = createMockState(); + state.cache.set("teams:aadObjectId:29:user-123", "aad-object-id-456"); + const mockChat = createMockChatInstance({ state }); const mockApp = ( adapter as unknown as { @@ -1211,9 +1132,7 @@ describe("TeamsAdapter", () => { })), }; - await adapter.initialize( - mockChat as unknown as Parameters[0] - ); + await adapter.initialize(mockChat); const user = await adapter.getUser("29:user-123"); expect(user).not.toBeNull(); diff --git a/packages/adapter-teams/vitest.config.ts b/packages/adapter-teams/vitest.config.ts index edc2d946b..18e0de9ae 100644 --- a/packages/adapter-teams/vitest.config.ts +++ b/packages/adapter-teams/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-telegram/package.json b/packages/adapter-telegram/package.json index 7c368c4a6..469f97e2d 100644 --- a/packages/adapter-telegram/package.json +++ b/packages/adapter-telegram/package.json @@ -31,6 +31,7 @@ "chat": "workspace:*" }, "devDependencies": { + "@chat-adapter/tests": "workspace:*", "@types/node": "^25.3.2", "tsup": "^8.3.5", "typescript": "^5.7.2", diff --git a/packages/adapter-telegram/src/index.test.ts b/packages/adapter-telegram/src/index.test.ts index ddeed56cd..8a4daa178 100644 --- a/packages/adapter-telegram/src/index.test.ts +++ b/packages/adapter-telegram/src/index.test.ts @@ -5,7 +5,8 @@ import { PermissionError, ValidationError, } from "@chat-adapter/shared"; -import type { ChatInstance, Logger } from "chat"; +import { createMockChatInstance, mockLogger } from "@chat-adapter/tests"; +import type { ChatInstance } from "chat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { encodeTelegramCallbackData } from "./cards"; import { @@ -21,14 +22,6 @@ import { TelegramFormatConverter, } from "./markdown"; -const mockLogger: Logger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - child: vi.fn().mockReturnThis(), -}; - const mockFetch = vi.fn(); const SERVERLESS_ENV_KEYS = [ "VERCEL", @@ -89,22 +82,12 @@ function telegramError( } function createMockChat(options?: { userName?: unknown }): ChatInstance { - return { - getLogger: vi.fn().mockReturnValue(mockLogger), - getState: vi.fn(), - getUserName: vi.fn().mockReturnValue(options?.userName ?? "mybot"), - handleIncomingMessage: vi.fn().mockResolvedValue(undefined), - processMessage: vi.fn(), - processReaction: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processModalClose: vi.fn(), - processModalSubmit: vi.fn().mockResolvedValue(undefined), - processSlashCommand: vi.fn(), - processAssistantThreadStarted: vi.fn(), - processAssistantContextChanged: vi.fn(), - processAppHomeOpened: vi.fn(), - } as unknown as ChatInstance; + // Thin wrapper over the shared factory that preserves this adapter's + // "mybot" default bot username (the shared factory defaults to "test-bot"). + return createMockChatInstance({ + logger: mockLogger, + userName: (options?.userName as string | undefined) ?? "mybot", + }); } function sampleMessage(overrides?: Partial): TelegramMessage { @@ -441,7 +424,7 @@ describe("TelegramAdapter", () => { typeof vi.fn >; expect(processSlashCommand).toHaveBeenCalledTimes(1); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); const [event] = processSlashCommand.mock.calls[0] as [ { @@ -561,7 +544,7 @@ describe("TelegramAdapter", () => { typeof vi.fn >; expect(processSlashCommand).toHaveBeenCalledTimes(1); - expect(chat.processMessage).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processMessage"); const [event] = processSlashCommand.mock.calls[0] as [ { @@ -609,7 +592,7 @@ describe("TelegramAdapter", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(chat.processSlashCommand).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processSlashCommand"); expect(chat.processMessage).toHaveBeenCalledTimes(1); }); @@ -648,7 +631,7 @@ describe("TelegramAdapter", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(chat.processSlashCommand).not.toHaveBeenCalled(); + expect(chat).not.toHaveDispatched("processSlashCommand"); expect(chat.processMessage).toHaveBeenCalledTimes(1); }); diff --git a/packages/adapter-telegram/vitest.config.ts b/packages/adapter-telegram/vitest.config.ts index f9fe2900d..e05ed7d1b 100644 --- a/packages/adapter-telegram/vitest.config.ts +++ b/packages/adapter-telegram/vitest.config.ts @@ -10,6 +10,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-twilio/package.json b/packages/adapter-twilio/package.json index 8114ba422..6ac20f215 100644 --- a/packages/adapter-twilio/package.json +++ b/packages/adapter-twilio/package.json @@ -47,6 +47,7 @@ "chat": "workspace:*" }, "devDependencies": { + "@chat-adapter/tests": "workspace:*", "@types/node": "^25.3.2", "tsup": "^8.3.5", "typescript": "^5.7.2", diff --git a/packages/adapter-twilio/src/index.test.ts b/packages/adapter-twilio/src/index.test.ts index 29d67fba8..f60b97749 100644 --- a/packages/adapter-twilio/src/index.test.ts +++ b/packages/adapter-twilio/src/index.test.ts @@ -1,4 +1,4 @@ -import type { ChatInstance } from "chat"; +import { createMockChatInstance } from "@chat-adapter/tests"; import { Message } from "chat"; import { describe, expect, it, vi } from "vitest"; import { createTwilioAdapter } from "./index"; @@ -31,7 +31,7 @@ describe("TwilioAdapter", () => { }); it("routes incoming message webhooks to chat processing", async () => { - const chat = mockChat(); + const chat = createMockChatInstance(); const adapter = createTwilioAdapter({ fetch: mockFetch("media"), webhookVerifier: () => true, @@ -261,15 +261,6 @@ function formRequest(fields: Record): Request { }); } -function mockChat() { - return { - getLogger: () => ({ child: () => console }), - processMessage: vi.fn(), - } as unknown as ChatInstance & { - processMessage: ReturnType; - }; -} - function mockFetch(body: unknown) { return vi.fn( async () => diff --git a/packages/adapter-twilio/vitest.config.ts b/packages/adapter-twilio/vitest.config.ts index edc2d946b..18e0de9ae 100644 --- a/packages/adapter-twilio/vitest.config.ts +++ b/packages/adapter-twilio/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-web/package.json b/packages/adapter-web/package.json index 2f18202c6..5c0e69f71 100644 --- a/packages/adapter-web/package.json +++ b/packages/adapter-web/package.json @@ -76,6 +76,7 @@ "@ai-sdk/svelte": "^4.0.182", "@ai-sdk/vue": "^3.0.182", "@chat-adapter/state-memory": "workspace:*", + "@chat-adapter/tests": "workspace:*", "@types/node": "^25.3.2", "@types/react": "^19.0.1", "@vitest/coverage-v8": "^4.0.18", diff --git a/packages/adapter-web/vitest.config.ts b/packages/adapter-web/vitest.config.ts index e02a16e16..f0c59be93 100644 --- a/packages/adapter-web/vitest.config.ts +++ b/packages/adapter-web/vitest.config.ts @@ -4,6 +4,7 @@ export default defineProject({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/packages/adapter-whatsapp/package.json b/packages/adapter-whatsapp/package.json index df0210b78..9419cd4ab 100644 --- a/packages/adapter-whatsapp/package.json +++ b/packages/adapter-whatsapp/package.json @@ -31,6 +31,7 @@ "chat": "workspace:*" }, "devDependencies": { + "@chat-adapter/tests": "workspace:*", "@types/node": "^25.3.2", "tsup": "^8.3.5", "typescript": "^5.7.2", diff --git a/packages/adapter-whatsapp/src/index.test.ts b/packages/adapter-whatsapp/src/index.test.ts index 069f85a0c..673456274 100644 --- a/packages/adapter-whatsapp/src/index.test.ts +++ b/packages/adapter-whatsapp/src/index.test.ts @@ -1,4 +1,5 @@ import { createHmac } from "node:crypto"; +import { createMockChatInstance, createMockLogger } from "@chat-adapter/tests"; import { afterEach, beforeEach, @@ -25,12 +26,7 @@ function createTestAdapter(): WhatsAppAdapter { phoneNumberId: "123456789", verifyToken: "test-verify-token", userName: "test-bot", - logger: { - info: () => {}, - warn: () => {}, - error: () => {}, - debug: () => {}, - }, + logger: createMockLogger(), }); } @@ -650,24 +646,7 @@ function makeWebhookPayload(overrides?: { }; } -const mockChat = { - processMessage: vi.fn(), - processReaction: vi.fn(), - processAction: vi.fn(), - processOptionsLoad: vi.fn().mockResolvedValue(undefined), - processModalSubmit: vi.fn(), - processModalClose: vi.fn(), - processSlashCommand: vi.fn(), - processMemberJoinedChannel: vi.fn(), - getState: vi.fn(), - getUserName: () => "test-bot", - getLogger: () => ({ - info: () => {}, - warn: () => {}, - error: () => {}, - debug: () => {}, - }), -}; +const mockChat = createMockChatInstance(); // --------------------------------------------------------------------------- // handleWebhook - POST with signature verification @@ -768,7 +747,7 @@ describe("handleWebhook - POST message processing", () => { it("text message calls chat.processMessage with correct thread and message", async () => { const adapter = createTestAdapter(); - await adapter.initialize(mockChat as never); + await adapter.initialize(mockChat); const payload = makeWebhookPayload(); const body = JSON.stringify(payload); @@ -792,7 +771,7 @@ describe("handleWebhook - POST message processing", () => { it("non-messages field change is skipped", async () => { const adapter = createTestAdapter(); - await adapter.initialize(mockChat as never); + await adapter.initialize(mockChat); const payload = makeWebhookPayload({ field: "statuses", @@ -811,7 +790,7 @@ describe("handleWebhook - POST message processing", () => { const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); - expect(mockChat.processMessage).not.toHaveBeenCalled(); + expect(mockChat).not.toHaveDispatched("processMessage"); }); }); @@ -1193,12 +1172,7 @@ describe("createWhatsAppAdapter", () => { verifyToken: "test-verify-token", userName: "test-bot", apiUrl: "https://custom-graph.example.com", - logger: { - info: () => {}, - warn: () => {}, - error: () => {}, - debug: () => {}, - }, + logger: createMockLogger(), }); expect((adapter as unknown as { graphApiUrl: string }).graphApiUrl).toBe( "https://custom-graph.example.com/v25.0" @@ -1237,12 +1211,7 @@ describe("createWhatsAppAdapter", () => { userName: "test-bot", apiUrl: "https://custom-graph.example.com", apiVersion: "v19.0", - logger: { - info: () => {}, - warn: () => {}, - error: () => {}, - debug: () => {}, - }, + logger: createMockLogger(), }); expect((adapter as unknown as { graphApiUrl: string }).graphApiUrl).toBe( "https://custom-graph.example.com/v19.0" diff --git a/packages/adapter-whatsapp/vitest.config.ts b/packages/adapter-whatsapp/vitest.config.ts index 5b01228bc..2ec7488c6 100644 --- a/packages/adapter-whatsapp/vitest.config.ts +++ b/packages/adapter-whatsapp/vitest.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ test: { globals: true, environment: "node", + setupFiles: ["@chat-adapter/tests/setup"], coverage: { provider: "v8", reporter: ["text", "json-summary"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1d592bff..43ee620c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -383,6 +383,9 @@ importers: specifier: ^14.25.1 version: 14.25.1 devDependencies: + '@chat-adapter/tests': + specifier: workspace:* + version: link:../tests '@types/node': specifier: ^25.3.2 version: 25.3.2 @@ -411,6 +414,9 @@ importers: specifier: workspace:* version: link:../chat devDependencies: + '@chat-adapter/tests': + specifier: workspace:* + version: link:../tests '@types/node': specifier: ^25.3.2 version: 25.3.2 @@ -492,6 +498,9 @@ importers: specifier: workspace:* version: link:../chat devDependencies: + '@chat-adapter/tests': + specifier: workspace:* + version: link:../tests '@types/node': specifier: ^25.3.2 version: 25.3.2 @@ -576,6 +585,9 @@ importers: specifier: workspace:* version: link:../chat devDependencies: + '@chat-adapter/tests': + specifier: workspace:* + version: link:../tests '@microsoft/teams.graph': specifier: ^2.0.8 version: 2.0.8 @@ -601,6 +613,9 @@ importers: specifier: workspace:* version: link:../chat devDependencies: + '@chat-adapter/tests': + specifier: workspace:* + version: link:../tests '@types/node': specifier: ^25.3.2 version: 25.3.2 @@ -623,6 +638,9 @@ importers: specifier: workspace:* version: link:../chat devDependencies: + '@chat-adapter/tests': + specifier: workspace:* + version: link:../tests '@types/node': specifier: ^25.3.2 version: 25.3.2 @@ -657,6 +675,9 @@ importers: '@chat-adapter/state-memory': specifier: workspace:* version: link:../state-memory + '@chat-adapter/tests': + specifier: workspace:* + version: link:../tests '@types/node': specifier: ^25.3.2 version: 25.3.2 @@ -697,6 +718,9 @@ importers: specifier: workspace:* version: link:../chat devDependencies: + '@chat-adapter/tests': + specifier: workspace:* + version: link:../tests '@types/node': specifier: ^25.3.2 version: 25.3.2