惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
T
The Blog of Author Tim Ferriss
H
Help Net Security
博客园 - 叶小钗
云风的 BLOG
云风的 BLOG
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
D
DataBreaches.Net
博客园 - 聂微东
A
About on SuperTechFans
大猫的无限游戏
大猫的无限游戏
P
Proofpoint News Feed
Martin Fowler
Martin Fowler
博客园 - 【当耐特】
S
SegmentFault 最新的问题
Blog — PlanetScale
Blog — PlanetScale
酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
I
InfoQ
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
B
Blog
Engineering at Meta
Engineering at Meta
V
V2EX
Hugging Face - Blog
Hugging Face - Blog

Chat SDK Documentation

History | Chat SDK History | Chat SDK List a vendor-official adapter | Chat SDK Approvals | Chat SDK Vercel Connect | Chat SDK Teams Low-Level APIs | Chat SDK CLI | Chat SDK Platform Adapters | Chat SDK State Adapters | Chat SDK Cards | Chat SDK Getting Started | Chat SDK Introduction | Chat SDK Modals | Chat SDK Slack Low-Level APIs | Chat SDK Streaming | Chat SDK Testing | Chat SDK Overview | Chat SDK toAiMessages | Chat SDK Cards | Chat SDK Overview | Chat SDK Markdown | Chat SDK Modals | Chat SDK AI SDK Tools | Chat SDK Types | Chat SDK Message Subject | Chat SDK Conversation History | Chat SDK Transcripts | Chat SDK Slack bot with Next.js and Redis Actions | Chat SDK Direct Messages | Chat SDK
Testing adapters | Chat SDK
Vercel · 2026-04-06 · via Chat SDK Documentation

Write unit tests, integration tests, and replay tests for community Chat SDK adapters.

Chat SDK adapters are the trust boundary between your application and a platform. High test coverage is expected: unit tests for every public method, and integration tests that wire up the full ChatAdapter → handler pipeline.

All adapters in this repo use vitest with @vitest/coverage-v8. Community adapters should follow the same convention.

This page covers the hand-rolled patterns used inside this repo's packages/. If you're testing a bot or a custom adapter as a consumer of Chat SDK, use @chat-adapter/tests — it ships factories and Vitest matchers that cover most of these patterns in a few lines.

Factory function

Verify that the factory validates config, reads environment variables, and sets defaults.

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createMatrixAdapter } from "./factory";

describe("createMatrixAdapter", () => {
  const originalEnv = process.env;

  beforeEach(() => {
    process.env = { ...originalEnv };
  });

  afterEach(() => {
    process.env = originalEnv;
  });

  it("creates adapter from explicit config", () => {
    const adapter = createMatrixAdapter({
      homeserverUrl: "https://matrix.example.com",
      accessToken: "syt_test_token",
    });

    expect(adapter.name).toBe("matrix");
  });

  it("reads environment variables as fallback", () => {
    process.env.MATRIX_HOMESERVER_URL = "https://matrix.example.com";
    process.env.MATRIX_ACCESS_TOKEN = "syt_test_token";

    const adapter = createMatrixAdapter();
    expect(adapter.name).toBe("matrix");
  });

  it("throws when homeserver URL is missing", () => {
    expect(() => createMatrixAdapter({ accessToken: "tok" } as never))
      .toThrow("homeserver URL");
  });

  it("throws when access token is missing", () => {
    expect(() =>
      createMatrixAdapter({
        homeserverUrl: "https://matrix.example.com",
      } as never)
    ).toThrow("access token");
  });
});

Thread ID encode/decode

Verify that encodeThreadId and decodeThreadId roundtrip consistently and reject invalid formats.

import { describe, it, expect } from "vitest";
import { MatrixAdapter } from "./adapter";

const adapter = new MatrixAdapter({
  homeserverUrl: "https://matrix.example.com",
  accessToken: "syt_test_token",
});

describe("thread ID encoding", () => {
  it("roundtrips room-only thread ID", () => {
    const data = { roomId: "!abc123:matrix.org" };
    const encoded = adapter.encodeThreadId(data);
    const decoded = adapter.decodeThreadId(encoded);

    expect(decoded.roomId).toBe(data.roomId);
    expect(encoded).toMatch(/^matrix:/);
  });

  it("roundtrips thread ID with event", () => {
    const data = {
      roomId: "!abc123:matrix.org",
      eventId: "$event456",
    };
    const encoded = adapter.encodeThreadId(data);
    const decoded = adapter.decodeThreadId(encoded);

    expect(decoded.roomId).toBe(data.roomId);
    expect(decoded.eventId).toBe(data.eventId);
  });

  it("throws on invalid format", () => {
    expect(() => adapter.decodeThreadId("invalid")).toThrow();
    expect(() => adapter.decodeThreadId("slack:C123:ts")).toThrow();
  });
});

Webhook signature verification

Test the three key scenarios: missing headers, invalid signature, and valid signature.

import { describe, it, expect } from "vitest";
import { MatrixAdapter } from "./adapter";

const adapter = new MatrixAdapter({
  homeserverUrl: "https://matrix.example.com",
  accessToken: "syt_test_token",
});

describe("handleWebhook", () => {
  it("returns 401 when signature header is missing", async () => {
    const request = new Request("https://example.com/webhook", {
      method: "POST",
      body: "{}",
    });

    const response = await adapter.handleWebhook(request);
    expect(response.status).toBe(401);
  });

  it("returns 401 when signature is invalid", async () => {
    const request = new Request("https://example.com/webhook", {
      method: "POST",
      headers: { "x-matrix-signature": "invalid" },
      body: "{}",
    });

    const response = await adapter.handleWebhook(request);
    expect(response.status).toBe(401);
  });

  it("returns 200 for valid signed request", async () => {
    const body = JSON.stringify({ type: "m.room.message" });
    const request = createSignedRequest(body); // Your signing helper

    const response = await adapter.handleWebhook(request);
    expect(response.status).toBe(200);
  });
});

Message parsing

Cover the main message types: plain text, bot messages, DMs, edited messages, attachments, and formatted text.

import { describe, it, expect } from "vitest";
import { MatrixAdapter } from "./adapter";

const adapter = new MatrixAdapter({
  homeserverUrl: "https://matrix.example.com",
  accessToken: "syt_test_token",
});

describe("parseMessage", () => {
  it("parses a plain text message", () => {
    const raw = {
      event_id: "$evt1",
      room_id: "!room1:matrix.org",
      body: "Hello world",
      sender: "@alice:matrix.org",
      sender_display_name: "Alice",
      origin_server_ts: 1700000000000,
    };

    const message = adapter.parseMessage(raw);
    expect(message.text).toBe("Hello world");
    expect(message.author.userId).toBe("@alice:matrix.org");
    expect(message.author.isBot).toBe(false);
  });

  it("detects bot messages", () => {
    const raw = {
      event_id: "$evt2",
      room_id: "!room1:matrix.org",
      body: "Automated response",
      sender: "@bot:matrix.org",
      origin_server_ts: 1700000000000,
    };

    const message = adapter.parseMessage(raw);
    expect(message.author.isBot).toBe(true);
  });
});

Format converter

Test toAst() and fromAst() for each node type your platform supports, plus renderPostable() for all message variants.

import { describe, it, expect } from "vitest";
import { MatrixFormatConverter } from "./format-converter";

const converter = new MatrixFormatConverter();

describe("MatrixFormatConverter", () => {
  describe("toAst", () => {
    it("parses plain text", () => {
      const ast = converter.toAst("Hello world");
      expect(ast.type).toBe("root");
    });

    it("parses bold text", () => {
      const ast = converter.toAst("**bold**");
      // Verify the AST contains a strong node
      const paragraph = ast.children[0];
      expect(paragraph.children[0].type).toBe("strong");
    });
  });

  describe("fromAst", () => {
    it("renders bold text", () => {
      const ast = converter.toAst("**bold**");
      const result = converter.fromAst(ast);
      expect(result).toContain("**bold**");
    });
  });

  describe("renderPostable", () => {
    it("renders plain text", () => {
      const result = converter.renderPostable({ text: "Hello" });
      expect(result).toBe("Hello");
    });

    it("renders card fallback", () => {
      const result = converter.renderPostable({
        card: {
          type: "card",
          title: "Test Card",
          children: [],
        },
      });
      expect(result).toContain("Test Card");
    });
  });
});

Integration tests wire your adapter into a full Chat instance and verify end-to-end message handling.

Test context factory

Create a factory that sets up the full test environment:

import { Chat, type Message, type Thread, type ActionEvent, type ReactionEvent } from "chat";
import { createMemoryState } from "@chat-adapter/state-memory";
import { createMatrixAdapter } from "./factory";

interface CapturedMessages {
  mentionMessage: Message | null;
  mentionThread: Thread | null;
  followUpMessage: Message | null;
  followUpThread: Thread | null;
}

interface WaitUntilTracker {
  waitUntil: (task: Promise<unknown>) => void;
  waitForAll: () => Promise<void>;
}

function createWaitUntilTracker(): WaitUntilTracker {
  const tasks: Promise<unknown>[] = [];

  return {
    waitUntil: (task) => {
      tasks.push(task);
    },
    waitForAll: async () => {
      await Promise.all(tasks);
      tasks.length = 0;
    },
  };
}

export function createMatrixTestContext(handlers: {
  onMention?: (thread: Thread, message: Message) => void | Promise<void>;
  onSubscribed?: (thread: Thread, message: Message) => void | Promise<void>;
  onAction?: (event: ActionEvent) => void | Promise<void>;
  onReaction?: (event: ReactionEvent) => void | Promise<void>;
}) {
  const adapter = createMatrixAdapter({
    homeserverUrl: "https://matrix.example.com",
    accessToken: "syt_test_token",
  });

  const state = createMemoryState();
  const chat = new Chat({
    userName: "matrix-bot",
    adapters: { matrix: adapter },
    state,
    logger: "error",
  });

  const captured: CapturedMessages = {
    mentionMessage: null,
    mentionThread: null,
    followUpMessage: null,
    followUpThread: null,
  };

  if (handlers.onMention) {
    const handler = handlers.onMention;
    chat.onNewMention(async (thread, message) => {
      captured.mentionMessage = message;
      captured.mentionThread = thread;
      await handler(thread, message);
    });
  }

  if (handlers.onSubscribed) {
    const handler = handlers.onSubscribed;
    chat.onSubscribedMessage(async (thread, message) => {
      captured.followUpMessage = message;
      captured.followUpThread = thread;
      await handler(thread, message);
    });
  }

  if (handlers.onAction) {
    chat.onAction(handlers.onAction);
  }

  if (handlers.onReaction) {
    chat.onReaction(handlers.onReaction);
  }

  const tracker = createWaitUntilTracker();

  return {
    chat,
    adapter,
    state,
    tracker,
    captured,
    sendWebhook: async (fixture: unknown) => {
      const request = createSignedMatrixRequest(fixture); // Your signing helper
      await chat.webhooks.matrix(request, {
        waitUntil: tracker.waitUntil,
      });
      await tracker.waitForAll();
    },
  };
}

function createSignedMatrixRequest(payload: unknown): Request {
  const body = JSON.stringify(payload);
  // Add platform-specific signature headers
  return new Request("https://example.com/webhook/matrix", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-matrix-signature": computeSignature(body),
    },
    body,
  });
}

function computeSignature(body: string): string {
  // Platform-specific signature computation
  return "valid-signature";
}

Writing integration tests

Use the test context to verify the full message flow:

import { describe, it, expect } from "vitest";
import { createMatrixTestContext } from "./test-utils";

describe("Matrix adapter integration", () => {
  it("handles mention → subscribe → follow-up flow", async () => {
    const ctx = createMatrixTestContext({
      onMention: async (thread) => {
        await thread.subscribe();
        await thread.post("Got it!");
      },
      onSubscribed: async (thread, message) => {
        await thread.post(`Echo: ${message.text}`);
      },
    });

    // Send a mention
    await ctx.sendWebhook({
      type: "m.room.message",
      room_id: "!room1:matrix.org",
      event_id: "$evt1",
      body: "@bot hello",
      sender: "@alice:matrix.org",
      origin_server_ts: Date.now(),
    });

    expect(ctx.captured.mentionMessage).not.toBeNull();
    expect(ctx.captured.mentionMessage?.text).toBe("@bot hello");

    // Send a follow-up in the same thread
    await ctx.sendWebhook({
      type: "m.room.message",
      room_id: "!room1:matrix.org",
      thread_root_id: "$evt1",
      event_id: "$evt2",
      body: "follow up",
      sender: "@alice:matrix.org",
      origin_server_ts: Date.now(),
    });

    expect(ctx.captured.followUpMessage).not.toBeNull();
    expect(ctx.captured.followUpMessage?.text).toBe("follow up");
  });
});

Cover these flows in your integration tests:

FlowWhat to verify
MentionBot detects @mention, handler fires, mentionMessage is captured
Subscribe + follow-upAfter thread.subscribe(), subsequent messages trigger onSubscribedMessage
ActionsButton clicks fire onAction with correct action ID and user info
ReactionsEmoji reactions fire onReaction with correct emoji and message ID
Self-message filteringMessages from the bot itself are ignored
DM flowDirect messages are detected and routed correctly

For production debugging, Chat SDK supports recording webhook interactions and replaying them as test fixtures.

  1. Enable recording — Set RECORDING_ENABLED=true in your deployed environment. Recordings are tagged with the current git SHA.

  2. Interact with the bot — Send messages, click buttons, add reactions — each interaction is recorded.

  3. Export recordings

pnpm recording:list
pnpm recording:export session-<id>
  1. Create test fixtures — Extract webhook payloads from the exported recording and save them as JSON fixtures:
{
  "botName": "matrix-bot",
  "botUserId": "@bot:matrix.org",
  "mention": { "type": "m.room.message", "body": "@bot help", "..." : "..." },
  "followUp": { "type": "m.room.message", "body": "thanks", "..." : "..." }
}
  1. Write replay tests — Use the fixtures in your test context:
import { describe, it, expect } from "vitest";
import fixture from "../fixtures/replay/matrix-mention.json";
import { createMatrixTestContext } from "./test-utils";

describe("replay: mention flow", () => {
  it("handles recorded mention interaction", async () => {
    const ctx = createMatrixTestContext({
      onMention: async (thread) => {
        await thread.subscribe();
      },
    });

    await ctx.sendWebhook(fixture.mention);
    expect(ctx.captured.mentionMessage).not.toBeNull();

    await ctx.sendWebhook(fixture.followUp);
    expect(ctx.captured.followUpMessage).not.toBeNull();
  });
});