From 32b560769a42b2f07c70459bb4e69586f3d296f3 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 11:49:34 +1000 Subject: [PATCH 01/12] test(adapters): migrate Connect webhook tests to connectWebhookContract Adopt the shared `connectWebhookContract` from `@chat-adapter/tests` in the Slack, GitHub, and Linear suites, replacing the bespoke `webhookVerifier` blocks (verifier pass -> 200, throw/falsy -> 401, invoked with request+body, precedence over a native secret). Keeps adapter-specific Connect tests (token resolvers, bot-id capture, type-level mutual exclusivity, 400 on invalid JSON). Twilio is left as-is (generic verifier, no Connect gating suite to migrate). Signed-off-by: Ben Sabic --- packages/adapter-github/package.json | 1 + packages/adapter-github/src/index.test.ts | 78 +++----------- packages/adapter-linear/package.json | 1 + packages/adapter-linear/src/index.test.ts | 54 ++++------ packages/adapter-slack/package.json | 1 + packages/adapter-slack/src/index.test.ts | 120 ++++------------------ pnpm-lock.yaml | 19 ++-- 7 files changed, 72 insertions(+), 202 deletions(-) diff --git a/packages/adapter-github/package.json b/packages/adapter-github/package.json index 9700ca8c..02d99867 100644 --- a/packages/adapter-github/package.json +++ b/packages/adapter-github/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-github/src/index.test.ts b/packages/adapter-github/src/index.test.ts index fed87bbe..38aff08a 100644 --- a/packages/adapter-github/src/index.test.ts +++ b/packages/adapter-github/src/index.test.ts @@ -1,5 +1,6 @@ import type { AsyncLocalStorage } from "node:async_hooks"; import { createHmac } from "node:crypto"; +import { connectWebhookContract } from "@chat-adapter/tests"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createGitHubAdapter, GitHubAdapter } from "./index"; import type { @@ -2572,6 +2573,22 @@ describe("GitHubAdapter", () => { }); }); +connectWebhookContract({ + name: "github", + createAdapter: ({ webhookVerifier }) => + new GitHubAdapter({ + installationToken: "t", + webhookVerifier, + botUserId: 999, + logger: mockLogger, + }), + makeWebhookRequest: () => + makeWebhookRequest( + JSON.stringify(makeIssueCommentPayload()), + "issue_comment" + ), +}); + describe("GitHubAdapter - Vercel Connect mode", () => { beforeEach(() => { vi.clearAllMocks(); @@ -2634,67 +2651,6 @@ describe("GitHubAdapter - Vercel Connect mode", () => { expect(opts.headers.authorization).toBe("token ghs_static"); }); - it("uses webhookVerifier instead of the signature check", async () => { - const verifier = vi.fn(() => true); - const adapter = new GitHubAdapter({ - installationToken: "t", - webhookVerifier: verifier, - userName: "connect-bot", - botUserId: 999, - logger: mockLogger, - }); - const mockChat = { - getLogger: vi.fn(), - getState: vi.fn(), - getUserName: vi.fn(), - handleIncomingMessage: vi.fn(), - processMessage: vi.fn(), - }; - await adapter.initialize(mockChat as never); - - const body = JSON.stringify(makeIssueCommentPayload()); - // No x-hub-signature-256 header — verifier is the only gate. - const request = makeWebhookRequest(body, "issue_comment"); - const response = await adapter.handleWebhook(request); - - expect(response.status).toBe(200); - expect(verifier).toHaveBeenCalledTimes(1); - expect(mockChat.processMessage).toHaveBeenCalled(); - }); - - it("returns 401 when the webhookVerifier throws", async () => { - const verifier = vi.fn(() => { - throw new Error("bad oidc token"); - }); - const adapter = new GitHubAdapter({ - installationToken: "t", - webhookVerifier: verifier, - botUserId: 1, - logger: mockLogger, - }); - const request = makeWebhookRequest( - JSON.stringify(makeIssueCommentPayload()), - "issue_comment" - ); - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(401); - }); - - it("returns 401 when the webhookVerifier returns falsy", async () => { - const adapter = new GitHubAdapter({ - installationToken: "t", - webhookVerifier: () => false, - botUserId: 1, - logger: mockLogger, - }); - const request = makeWebhookRequest( - JSON.stringify(makeIssueCommentPayload()), - "issue_comment" - ); - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(401); - }); - it("rejects mixing Connect installationToken with App auth at the type level", () => { const adapter = new GitHubAdapter({ installationToken: "t", diff --git a/packages/adapter-linear/package.json b/packages/adapter-linear/package.json index a908439b..ab8e77df 100644 --- a/packages/adapter-linear/package.json +++ b/packages/adapter-linear/package.json @@ -32,6 +32,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-linear/src/index.test.ts b/packages/adapter-linear/src/index.test.ts index f74f1275..25a899b1 100644 --- a/packages/adapter-linear/src/index.test.ts +++ b/packages/adapter-linear/src/index.test.ts @@ -1,5 +1,6 @@ import type { AsyncLocalStorage } from "node:async_hooks"; import { createHmac } from "node:crypto"; +import { connectWebhookContract } 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"; @@ -1103,17 +1104,30 @@ describe("handleWebhook - invalid JSON", () => { // Webhook - comment created handling // ============================================================================= -describe("Vercel Connect mode", () => { - it("constructs with a function accessToken + webhookVerifier (no secret)", () => { +connectWebhookContract({ + name: "linear", + createAdapter: ({ webhookVerifier }) => { const adapter = new LinearAdapter({ - accessToken: () => Promise.resolve("lin_connect_token"), - webhookVerifier: () => true, - userName: "connect-bot", + accessToken: () => Promise.resolve("t"), + webhookVerifier, + userName: "test-bot", logger: createMockLogger(), }); - expect(adapter.name).toBe("linear"); - }); + setBotUserId(adapter, "bot-user-id"); + setDefaultOrganizationId(adapter, "org-123"); + // initialize() resolves Connect identity over the network; stub it since the + // contract only exercises inbound verification. + vi.spyOn( + adapter as unknown as { resolveConnectIdentity: () => Promise }, + "resolveConnectIdentity" + ).mockResolvedValue(undefined); + return adapter; + }, + makeWebhookRequest: () => + buildWebhookRequest(JSON.stringify(createCommentPayload())), +}); +describe("Vercel Connect mode", () => { it("verifies via webhookVerifier and dispatches comment events", async () => { const logger = createMockLogger(); const verifier = vi.fn(() => true); @@ -1150,32 +1164,6 @@ describe("Vercel Connect mode", () => { ); }); - it("returns 401 when the webhookVerifier throws", async () => { - const adapter = new LinearAdapter({ - accessToken: () => Promise.resolve("t"), - webhookVerifier: () => { - throw new Error("bad oidc token"); - }, - userName: "test-bot", - logger: createMockLogger(), - }); - const request = buildWebhookRequest(JSON.stringify(createCommentPayload())); - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(401); - }); - - it("returns 401 when the webhookVerifier returns falsy", async () => { - const adapter = new LinearAdapter({ - accessToken: () => Promise.resolve("t"), - webhookVerifier: () => false, - userName: "test-bot", - logger: createMockLogger(), - }); - const request = buildWebhookRequest(JSON.stringify(createCommentPayload())); - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(401); - }); - it("returns 400 when the verified body is not valid JSON", async () => { const adapter = new LinearAdapter({ accessToken: () => Promise.resolve("t"), diff --git a/packages/adapter-slack/package.json b/packages/adapter-slack/package.json index d2ccf3e4..1b2aded8 100644 --- a/packages/adapter-slack/package.json +++ b/packages/adapter-slack/package.json @@ -49,6 +49,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-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index 0a552d69..529fef45 100644 --- a/packages/adapter-slack/src/index.test.ts +++ b/packages/adapter-slack/src/index.test.ts @@ -4,6 +4,7 @@ import { createHmac, randomBytes } from "node:crypto"; import { AuthenticationError, ValidationError } from "@chat-adapter/shared"; +import { connectWebhookContract } from "@chat-adapter/tests"; import { WebClient } from "@slack/web-api"; import type { AdapterPostableMessage, @@ -367,116 +368,37 @@ describe("handleWebhook - signature verification", () => { }); }); -describe("handleWebhook - webhookVerifier", () => { - it("uses webhookVerifier in place of signingSecret", async () => { - const adapter = createSlackAdapter({ - botToken: "xoxb-test-token", - webhookVerifier: () => true, - logger: mockLogger, - }); - - const body = JSON.stringify({ - type: "url_verification", - challenge: "verifier-challenge", - }); - const request = new Request("https://example.com/webhook", { - method: "POST", - headers: { "content-type": "application/json" }, - body, - }); - - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(200); - const json = (await response.json()) as { challenge: string }; - expect(json.challenge).toBe("verifier-challenge"); - }); - - it("returns 401 when verifier throws", async () => { - const adapter = createSlackAdapter({ - botToken: "xoxb-test-token", - webhookVerifier: () => { - throw new Error("bad signature"); - }, - logger: mockLogger, - }); - - const request = new Request("https://example.com/webhook", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ type: "url_verification" }), - }); - - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(401); - }); - - it("returns 401 when verifier returns a falsy value", async () => { - const adapter = createSlackAdapter({ - botToken: "xoxb-test-token", - webhookVerifier: () => false, - logger: mockLogger, - }); - - const request = new Request("https://example.com/webhook", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ type: "url_verification" }), - }); - - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(401); - }); - - it("passes the body string to the verifier", async () => { - const body = JSON.stringify({ - type: "url_verification", - challenge: "verifier-challenge", - }); - const verifier = vi.fn((_req: Request, _body: string) => true); +connectWebhookContract({ + name: "slack", + createAdapter: ({ webhookVerifier }) => { const adapter = createSlackAdapter({ botToken: "xoxb-test-token", - webhookVerifier: verifier, + webhookVerifier, logger: mockLogger, }); - - const request = new Request("https://example.com/webhook", { - method: "POST", - headers: { "content-type": "application/json" }, - body, - }); - - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(200); - expect(verifier).toHaveBeenCalledTimes(1); - expect(verifier.mock.calls[0]?.[1]).toBe(body); - }); - - it("prefers webhookVerifier over signingSecret when both are set", async () => { - const secret = "test-signing-secret"; - const verifier = vi.fn(() => true); + // Skip initialize()'s auth.test network call for bot-id detection. + (adapter as unknown as { _botUserId: string })._botUserId = "UBOT"; + return adapter; + }, + createAdapterWithSecretAndVerifier: ({ webhookVerifier }) => { const adapter = createSlackAdapter({ botToken: "xoxb-test-token", - signingSecret: secret, - webhookVerifier: verifier, + signingSecret: "test-signing-secret", + webhookVerifier, logger: mockLogger, }); - - const body = JSON.stringify({ - type: "url_verification", - challenge: "test-challenge", - }); - // No signing headers — only the verifier should run. - const request = new Request("https://example.com/webhook", { + (adapter as unknown as { _botUserId: string })._botUserId = "UBOT"; + return adapter; + }, + makeWebhookRequest: () => + new Request("https://example.com/webhook", { method: "POST", headers: { "content-type": "application/json" }, - body, - }); - - const response = await adapter.handleWebhook(request); - expect(response.status).toBe(200); - expect(verifier).toHaveBeenCalledTimes(1); - }); + body: JSON.stringify({ type: "url_verification", challenge: "c" }), + }), +}); +describe("handleWebhook - webhookVerifier", () => { it("ignores SLACK_SIGNING_SECRET env var when webhookVerifier is configured", async () => { vi.stubEnv("SLACK_SIGNING_SECRET", "env-signing-secret"); try { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 069ba8c7..e372d61c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -399,6 +399,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 @@ -424,6 +427,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 @@ -493,6 +499,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 @@ -12737,14 +12746,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.3(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.22.3))': - dependencies: - '@vitest/spy': 4.0.18 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.3(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.22.3) - '@vitest/mocker@4.0.18(vite@7.3.3(@types/node@25.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.22.3))': dependencies: '@vitest/spy': 4.0.18 @@ -17675,7 +17676,7 @@ snapshots: vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.22.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.3(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.22.3)) + '@vitest/mocker': 4.0.18(vite@7.3.3(@types/node@25.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.22.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 From ea5f4bbfbf10d1fabce030c74f136d3f5c8d4c1c Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:04:15 +1000 Subject: [PATCH 02/12] test(adapters): wire @chat-adapter/tests devDep and matcher setup Add `@chat-adapter/tests` as a devDependency and register its matchers via `setupFiles: ["@chat-adapter/tests/setup"]` across all 11 platform adapters, so adapter suites can adopt the shared factories and matchers. Signed-off-by: Ben Sabic --- packages/adapter-discord/package.json | 1 + packages/adapter-discord/vitest.config.ts | 1 + packages/adapter-gchat/package.json | 1 + packages/adapter-gchat/vitest.config.ts | 1 + packages/adapter-github/vitest.config.ts | 1 + packages/adapter-linear/vitest.config.ts | 1 + packages/adapter-messenger/package.json | 1 + packages/adapter-messenger/vitest.config.ts | 1 + packages/adapter-slack/vitest.config.ts | 1 + packages/adapter-teams/package.json | 1 + packages/adapter-teams/vitest.config.ts | 1 + packages/adapter-telegram/package.json | 1 + packages/adapter-telegram/vitest.config.ts | 1 + packages/adapter-twilio/package.json | 1 + packages/adapter-twilio/vitest.config.ts | 1 + packages/adapter-web/package.json | 1 + packages/adapter-web/vitest.config.ts | 1 + packages/adapter-whatsapp/package.json | 1 + packages/adapter-whatsapp/vitest.config.ts | 1 + pnpm-lock.yaml | 24 +++++++++++++++++++++ 20 files changed, 43 insertions(+) diff --git a/packages/adapter-discord/package.json b/packages/adapter-discord/package.json index 801d7f9a..d97ef5fc 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/vitest.config.ts b/packages/adapter-discord/vitest.config.ts index edc2d946..18e0de9a 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 449eab6a..242f88a3 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/vitest.config.ts b/packages/adapter-gchat/vitest.config.ts index edc2d946..18e0de9a 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/vitest.config.ts b/packages/adapter-github/vitest.config.ts index edc2d946..18e0de9a 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/vitest.config.ts b/packages/adapter-linear/vitest.config.ts index edc2d946..18e0de9a 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 c94fa9ad..6ca46f73 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/vitest.config.ts b/packages/adapter-messenger/vitest.config.ts index edc2d946..18e0de9a 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/vitest.config.ts b/packages/adapter-slack/vitest.config.ts index edc2d946..18e0de9a 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 78bfd001..2cb54fd7 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/vitest.config.ts b/packages/adapter-teams/vitest.config.ts index edc2d946..18e0de9a 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 7c368c4a..469f97e2 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/vitest.config.ts b/packages/adapter-telegram/vitest.config.ts index f9fe2900..e05ed7d1 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 8114ba42..6ac20f21 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/vitest.config.ts b/packages/adapter-twilio/vitest.config.ts index edc2d946..18e0de9a 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 2f18202c..5c0e69f7 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 e02a16e1..f0c59be9 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 df0210b7..9419cd4a 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/vitest.config.ts b/packages/adapter-whatsapp/vitest.config.ts index 5b01228b..2ec7488c 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 e372d61c..ea03a8aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,6 +343,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 @@ -371,6 +374,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 @@ -452,6 +458,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 @@ -536,6 +545,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 @@ -561,6 +573,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 @@ -583,6 +598,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 @@ -617,6 +635,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 @@ -657,6 +678,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 From 3d37d4c332792e4c6cbc7a5af36c6605a88b9530 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:27:14 +1000 Subject: [PATCH 03/12] test(gchat): adopt shared @chat-adapter/tests factories and matchers Replace the local mockLogger/createMockState/createMockChatInstance and inline logger literals with the shared factories, convert call sites to the options-based createMockChatInstance, and adopt toHaveDispatched. Keeps the bespoke createMockStateAdapter (uses .storage + constrained spy behavior) and the Google SDK mocks local. Signed-off-by: Ben Sabic --- packages/adapter-gchat/src/index.test.ts | 79 +++++++------------- packages/adapter-gchat/src/user-info.test.ts | 38 +--------- 2 files changed, 27 insertions(+), 90 deletions(-) diff --git a/packages/adapter-gchat/src/index.test.ts b/packages/adapter-gchat/src/index.test.ts index d01bc759..8636e2d3 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 c1335c36..0ccde853 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 () => { From 4d95363d7879f165b851ec4c9b9c861a729f584a Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:27:56 +1000 Subject: [PATCH 04/12] test(messenger): adopt shared @chat-adapter/tests factories and matchers Replace the local mockLogger/createMockChat with the shared factories (createMockChat kept as a thin wrapper preserving userName + logger wiring) and adopt not.toHaveDispatched for the echo-message cases. Signed-off-by: Ben Sabic --- packages/adapter-messenger/src/index.test.ts | 31 ++++---------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/packages/adapter-messenger/src/index.test.ts b/packages/adapter-messenger/src/index.test.ts index 6abc84a4..95a4700a 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" From 217884cdb4c6e852c4d68dc5e32dde57edecda53 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:27:58 +1000 Subject: [PATCH 05/12] test(teams): adopt shared @chat-adapter/tests factories Replace the inline mock chat/state objects in index.test.ts with the shared createMockChatInstance/createMockState (seeding the aadObjectId via state.cache), dropping redundant casts. Platform SDK mocks stay local. Signed-off-by: Ben Sabic --- packages/adapter-teams/src/index.test.ts | 115 ++++------------------- 1 file changed, 17 insertions(+), 98 deletions(-) diff --git a/packages/adapter-teams/src/index.test.ts b/packages/adapter-teams/src/index.test.ts index 6f46572a..a1f726c0 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(); From 32c3d79a6af80aa2dc06a15084cf8667df6a4ef3 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:28:34 +1000 Subject: [PATCH 06/12] test(whatsapp): adopt shared @chat-adapter/tests factories and matchers Replace the inline mockChat and no-op loggers with createMockChatInstance / createMockLogger, drop the now-unneeded casts, and adopt not.toHaveDispatched. Keeps fetch/HMAC and getList history stubs local. Signed-off-by: Ben Sabic --- packages/adapter-whatsapp/src/index.test.ts | 47 ++++----------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/packages/adapter-whatsapp/src/index.test.ts b/packages/adapter-whatsapp/src/index.test.ts index 069f85a0..67345627 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" From 65da0738a87da7d154165f195ee5ae47f852ca95 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:28:39 +1000 Subject: [PATCH 07/12] test(telegram): adopt shared @chat-adapter/tests factories and matchers Replace the local mockLogger with the shared singleton and delegate createMockChat to createMockChatInstance (thin wrapper preserving the "mybot" default), and adopt not.toHaveDispatched for the clean cases. Signed-off-by: Ben Sabic --- packages/adapter-telegram/src/index.test.ts | 41 ++++++--------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/packages/adapter-telegram/src/index.test.ts b/packages/adapter-telegram/src/index.test.ts index ddeed56c..8a4daa17 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); }); From f85910f656c3940b9160c8dd6db83661296cdea2 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:36:30 +1000 Subject: [PATCH 08/12] test(discord): adopt shared @chat-adapter/tests factories and matchers Remove the local mockLogger literals and the gateway mockChat object, replace ~20 inline `as unknown as ChatInstance` mocks with createMockChatInstance() (overrides where needed), and adopt not.toHaveDispatched for the clean case. discord.js mocks stay local. Signed-off-by: Ben Sabic --- packages/adapter-discord/src/gateway.test.ts | 27 +-- packages/adapter-discord/src/index.test.ts | 227 ++++++------------- 2 files changed, 67 insertions(+), 187 deletions(-) diff --git a/packages/adapter-discord/src/gateway.test.ts b/packages/adapter-discord/src/gateway.test.ts index 6f872227..c27d1afb 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 a40c332e..b850017d 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", From 5b1fd6fdd483190a68b06ff12ff51bc4d13ad083 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:40:53 +1000 Subject: [PATCH 09/12] test(twilio): adopt shared @chat-adapter/tests factory Replace the inline mockChat() helper with createMockChatInstance() and drop the now-unused ChatInstance import. Twilio fetch/credential mocks stay local. Signed-off-by: Ben Sabic --- packages/adapter-twilio/src/index.test.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/adapter-twilio/src/index.test.ts b/packages/adapter-twilio/src/index.test.ts index 29d67fba..f60b9774 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 () => From a47043cb59ade57b1bcf83eba1b1cc7dc8a08c4a Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:46:03 +1000 Subject: [PATCH 10/12] test(linear): adopt shared @chat-adapter/tests factories and matchers Remove the local createMockLogger/createMockState/createMockChatInstance, import the shared ones, convert 35 positional createMockChatInstance call sites to the options API, and adopt toHaveDispatched. LinearClient mocks and the Phase 1 connectWebhookContract descriptor stay intact. Signed-off-by: Ben Sabic --- packages/adapter-linear/src/index.test.ts | 206 +++++++++++----------- 1 file changed, 100 insertions(+), 106 deletions(-) diff --git a/packages/adapter-linear/src/index.test.ts b/packages/adapter-linear/src/index.test.ts index 25a899b1..c7b47a33 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"); From 4649f6f520a4ac5ca3f92cd5341204ecba26c428 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:46:45 +1000 Subject: [PATCH 11/12] test(github): adopt shared @chat-adapter/tests factories and matchers Replace the local mockLogger/createMockState and 17 inline mock-chat literals with the shared factories, and adopt not.toHaveDispatched. Octokit mocks and the Phase 1 connectWebhookContract descriptor stay intact. Signed-off-by: Ben Sabic --- packages/adapter-github/src/index.test.ts | 181 ++++------------------ 1 file changed, 32 insertions(+), 149 deletions(-) diff --git a/packages/adapter-github/src/index.test.ts b/packages/adapter-github/src/index.test.ts index 38aff08a..d73db7a6 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", From 02715c7478538be7e7925c838c598cdf2d3ffae6 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 3 Jul 2026 12:50:16 +1000 Subject: [PATCH 12/12] test(slack): adopt shared @chat-adapter/tests factories and matchers Remove the local mockLogger/createMockState/createMockChatInstance, import the shared ones, convert 100 positional createMockChatInstance call sites to the options API, and adopt toHaveDispatched for the clean cases. WebClient/socket-mode mocks and the Phase 1 connectWebhookContract descriptor stay intact. Signed-off-by: Ben Sabic --- packages/adapter-slack/src/index.test.ts | 325 +++++++++-------------- 1 file changed, 125 insertions(+), 200 deletions(-) diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index 529fef45..3c782282 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"); }); });