import { Adapter, ChatInstance, Logger, StateAdapter, MessageData, Message } from 'chat'; import { ChatHandler } from './matchers.js'; export { matchers } from './matchers.js'; import '@vitest/expect'; /** * Webhook verifier shape shared by Vercel Connect-capable adapters: it * receives the incoming request and its raw body; a truthy result accepts the * request, while a thrown error or a falsy result rejects it (the adapter * responds `401`). Used in place of the platform's native signature/secret * check when webhooks arrive via Vercel Connect trigger forwarding. */ type ConnectWebhookVerifier = (request: Request, body: string) => Promise | unknown; /** * Per-adapter hooks the {@link connectWebhookContract} runner needs. * * The contract only exercises inbound verification, so `createAdapter` should * stub any outbound token (e.g. `accessToken: () => Promise.resolve("t")`) and * pre-seed whatever the adapter needs to dispatch without hitting the network. */ interface ConnectWebhookContractDescriptor { /** * Build the adapter in Vercel Connect mode using the supplied verifier and * no native webhook secret. */ createAdapter(options: { webhookVerifier: ConnectWebhookVerifier; }): Adapter; /** * Optionally build the adapter with BOTH a native secret and a verifier, to * assert the verifier takes precedence. Omit if there is no secret mode. */ createAdapterWithSecretAndVerifier?(options: { webhookVerifier: ConnectWebhookVerifier; }): Adapter; /** Mock chat instance to initialize against. Defaults to `createMockChatInstance()`. */ createChat?(): ChatInstance; /** * Build an inbound webhook request the adapter should accept once verified * (no native signature header — the verifier is the only gate). */ makeWebhookRequest(): Promise | Request; /** Label for the describe block, typically the adapter name. */ name: string; } /** * Shared Vitest suite for an adapter's Vercel Connect webhook verification. * * Call it at the top level of an adapter's test file with a descriptor that * knows how to build the adapter in Connect mode and craft an inbound webhook. * It asserts the behavior every Connect-capable adapter shares: a * `webhookVerifier` replaces the native signature/secret check and gates * inbound requests — accept (`200`) on a truthy result, reject (`401`) on a * thrown error or a falsy result — and is invoked with the request and raw body. * * ```ts * connectWebhookContract({ * name: "github", * createAdapter: ({ webhookVerifier }) => * new GitHubAdapter({ installationToken: "t", webhookVerifier, botUserId: 1, logger }), * makeWebhookRequest: () => * makeWebhookRequest(JSON.stringify(payload), "issue_comment"), * }); * ``` */ declare function connectWebhookContract(descriptor: ConnectWebhookContractDescriptor): void; /** * Mock logger that captures all log calls via `vi.fn()`. * Re-used across `child()` so assertions on a parent surface child calls too. */ declare const mockLogger: Logger; /** * Create a fresh mock logger with its own `vi.fn()` instances. * Use when a test needs isolation from other tests sharing `mockLogger`. */ declare function createMockLogger(): Logger; /** * Create a mock `Adapter` for testing. Every method is a `vi.fn()` returning * a sensible default. Pass `overrides` to replace specific methods or fields. * * @param name Adapter name (e.g. `"slack"`, `"teams"`). Used in the default * `encodeThreadId`/`decodeThreadId` implementations and in derived defaults. * @param overrides Partial overrides merged on top of the defaults. */ declare function createMockAdapter(name?: string, overrides?: Partial): Adapter; /** * Mock state adapter with working in-memory storage. * `cache` exposes the underlying map for direct inspection in tests. */ interface MockStateAdapter extends StateAdapter { cache: Map; } /** * Create a `StateAdapter` backed by in-memory `Map`s — subscriptions, locks, * key/value cache, lists, and queues all work end-to-end. * * Each call returns isolated state, so tests don't leak between runs. */ declare function createMockState(): MockStateAdapter; /** * Options for `createMockChatInstance`. */ interface MockChatInstanceOptions { /** Logger. Defaults to `mockLogger`. */ logger?: Logger; /** Partial overrides applied last (e.g. to swap a single processor). */ overrides?: Partial; /** State adapter to back the instance. Defaults to a fresh `createMockState()`. */ state?: StateAdapter; /** Bot user name returned by `getUserName()`. Defaults to `"test-bot"`. */ userName?: string; } /** * Create a mock `ChatInstance` for adapter authors who need to verify their * adapter dispatches incoming events through the right `process*` hook. * * Every processor is a `vi.fn()`; `getState()`/`getUserName()`/`getLogger()` * are wired up to the supplied (or default) state and logger. */ declare function createMockChatInstance(options?: MockChatInstanceOptions): ChatInstance; /** * Build a `Message` for tests. `text` is parsed into the formatted AST so * assertions on `message.formatted` work without manual setup. */ declare function createTestMessage(id: string, text: string, overrides?: Partial): Message; /** * Per-adapter hooks the {@link selfMessageContract} runner needs. * * The contract also covers the webhook-dispatch happy path: a message from * another user must route to the adapter's dispatch handler, while a message * the bot itself authored must be ignored (no dispatch) so the bot never * replies to itself. */ interface SelfMessageContractDescriptor { /** Handler a non-self message should reach. Defaults to `"processMessage"`. */ dispatchHandler?: ChatHandler; /** Build an inbound webhook authored by another user (should dispatch). */ makeOtherMessageRequest: () => Promise | Request; /** Build an inbound webhook authored by the bot itself (should be ignored). */ makeSelfMessageRequest: () => Promise | Request; /** Label for the describe block, typically the adapter name. */ name: string; /** * Build the adapter (initialized, with a known bot identity so self-detection * works) and the mock chat it dispatches into. Return both so the contract * can assert on dispatch. Use a fresh mock chat per call. */ setup: () => Promise<{ adapter: Adapter; chat: ChatInstance; }> | { adapter: Adapter; chat: ChatInstance; }; } /** * Shared Vitest suite asserting an adapter ignores its own messages. * * Requires the `@chat-adapter/tests` matchers to be registered (via * `setupFiles: ["@chat-adapter/tests/setup"]`). A message from another user * dispatches through `dispatchHandler` (default `processMessage`) and returns * `200`; a message authored by the bot returns `200` and dispatches through * none of the chat's handlers. */ declare function selfMessageContract(descriptor: SelfMessageContractDescriptor): void; /** * A decoded thread descriptor paired with the adapter that produced it. The * `encoded` field is optional: when present the contract also pins the exact * encoded string, otherwise it only asserts the decode(encode(x)) round-trip. */ interface ThreadIdCase { decoded: TDecoded; encoded?: string; } /** * Per-adapter hooks the {@link threadIdContract} runner needs. * * `encode`/`decode` should delegate to the adapter's `encodeThreadId` / * `decodeThreadId` (wrap in arrows so `this` binds correctly). */ interface ThreadIdContractDescriptor { /** Sample descriptors to round-trip. */ cases: readonly ThreadIdCase[]; /** Decode a thread-id string back into a descriptor. */ decode: (id: string) => TDecoded; /** Encode a decoded thread descriptor into a thread-id string. */ encode: (decoded: TDecoded) => string; /** * Optional DM detection check: an `isDM` function plus a DM thread id * (expected `true`) and a non-DM thread id (expected `false`). */ isDM?: { fn: (id: string) => boolean; dmThreadId: string; nonDmThreadId: string; }; /** Label + expected thread-id prefix (the `{adapter}` in `{adapter}:...`). */ name: string; } /** * Shared Vitest suite for an adapter's thread-id codec. * * Asserts every case round-trips (`decode(encode(x))` deep-equals `x`), encoded * ids carry the `{name}:` prefix, any pinned `encoded` strings match exactly, * and (optionally) `isDM` distinguishes DM from non-DM threads. Call it at the * top level of an adapter's test file. * * ```ts * threadIdContract({ * name: "github", * encode: (d) => adapter.encodeThreadId(d), * decode: (id) => adapter.decodeThreadId(id), * cases: [{ decoded: { owner: "acme", repo: "app", prNumber: 1, type: "pr" } }], * }); * ``` */ declare function threadIdContract(descriptor: ThreadIdContractDescriptor): void; export { ChatHandler, type ConnectWebhookContractDescriptor, type ConnectWebhookVerifier, type MockChatInstanceOptions, type MockStateAdapter, type SelfMessageContractDescriptor, type ThreadIdCase, type ThreadIdContractDescriptor, connectWebhookContract, createMockAdapter, createMockChatInstance, createMockLogger, createMockState, createTestMessage, mockLogger, selfMessageContract, threadIdContract };