CHANNEL_INTEGRATION_TESTS
This guide explains how to add chat platform integration tests for agent channel integrations. The current suite covers three layers:
Claude Code
> /plugin marketplace add n8n-io/n8n> /plugin install n8n@n8n
Cursor
Cursor also loads subagents from .claude/agents, so copy this file there to use it in Cursor.
Agent definition
CHANNEL_INTEGRATION_TESTS.mdChannel Integration Tests
This guide explains how to add chat platform integration tests for agent channel integrations. The current suite covers three layers:
- Shared contract tests for behavior every channel integration must support.
- Synthetic platform tests for hand-built edge cases.
- Recorded replay tests for real webhook payloads captured from a local run.
These tests validate n8n's integration logic: routing inbound messages to the agent, preserving message context, executing integration actions, resuming suspended tool calls, and avoiding self-trigger loops.
Design: real adapters, no fakes
These tests run the **real** `@chat-adapter/*` adapters and the real `chat` SDK. There are no fake or in-memory adapters. The packages are ESM-only; in production they are loaded via `esm-loader`'s `new Function()` indirection to survive the CJS transform, but **that indirection cannot run under Vitest** — so the test helpers import them directly (`await import('@chat-adapter/telegram')`), which Vitest loads natively. Any test that drives a code path which itself calls `esm-loader` (e.g. the real `ComponentMapper` for rich cards) must `vi.mock('../esm-loader', …)` to redirect the loaders to native dynamic imports — see the telegram tests for the pattern.
The only thing faked is the **external platform HTTP at the network boundary** — you cannot call live Telegram/Slack/Linear from CI. Everything else (adapter, `AgentChatBridge`, integration implementation, action executor, message-context service) is real.
Answering the network boundary
Each platform adapter uses a different HTTP client, so the interception mechanism differs:
| Platform | Adapter HTTP client | Interception | Helper | |----------|---------------------|--------------|--------| | Telegram | native `fetch` | replace `globalThis.fetch` | `installFetchStub` (replay-test-helpers) | | Slack | `@slack/web-api` (axios) | `nock` at the HTTP layer | inline in slack `replay-test-context` | | Linear | `@linear/sdk` (GraphQL over fetch) | replace `globalThis.fetch` | `installFetchStub` | | WhatsApp | native `fetch` | replace `globalThis.fetch` | `installWhatsAppApiStub` (whatsapp `replay-test-context`) | | Teams | `@microsoft/teams.*` (axios) + `jwks-rsa` (node `https`) | `nock` at the HTTP layer | inline in teams `replay-test-context` |
Responses are answered from two sources, in order of preference:
1. **Recorded data** where it matters — the captured webhook payload is replayed into the real adapter as the inbound event, and recorded outbound bodies inform assertions. 2. **Minimal stubs** for incidental calls the recordings don't need to pin down — identity bootstrap (`getMe`, `auth.test`, Linear `viewer`), streaming lifecycle, and entity look-ups.
The assertions check what the adapter **sends** (the outbound request body), not what it receives, so response stubs only need to be valid enough for the real adapter to proceed.
Platform notes
- **Telegram** — `getMe` returns the bot fixture so the adapter learns its identity; `sendMessage`
returns a minimal message. `reply_markup` is sent as a JSON object (not a stringified blob), so read inline-keyboard callback data via `getTelegramInlineCallbackData`.
- **Slack** — agent replies go through Slack's assistant **streaming** API
(`chat.startStream` → `appendStream` → `stopStream`), not `chat.postMessage`. The nock handler reconstructs the streamed text and records it as a synthetic `chat.postMessage` so assertions can treat the reply as one outbound post. `webhookVerifier: () => true` bypasses signature checks (the fixtures carry sanitized signatures); passing `botUserId` skips the `auth.test` lookup.
- **Teams** — the only platform whose inbound requests must carry a **real signed token**.
`createTeamsAdapter` builds a `@microsoft/teams.apps` `App`, which validates every activity's Bot Framework JWT, and the adapter exposes no bypass: there is no `webhookVerifier` like Slack's, and `TeamsAdapterConfig` cannot pass the Teams SDK's `skipAuth` through. So the helper generates an RSA keypair per run, signs an activity token (issuer `https://api.botframework.com`, audience the app ID, a `serviceurl` claim matching the activity), and serves the matching public JWK from a nocked `https://login.botframework.com/v1/.well-known/keys`. The adapter's own validation then runs for real — `sendUnauthenticatedWebhook` asserts it still rejects. Two non-obvious stubs are also required: the client-credentials token endpoint is **tenant**-scoped, not `botframework.com`-scoped, and the SDK decodes the access token it gets back, so that stub must return a real JWT rather than an opaque string.
- **Linear** — webhooks are HMAC-signed (`linear-signature`) and timestamp-checked, so the helper
refreshes `webhookTimestamp` and signs the body. `@linear/sdk` strictly deserializes typed entities and lazily fetches relationships, so the GraphQL stub returns fully-shaped entities (e.g. a `Comment` needs `reactions: []`; an `AgentActivity` references `agentSession`/`sourceComment` by id). Linear's "mention" is an **agent-session** event, not a comment — see the contract note below.
- **WhatsApp** — inbound media (`fetchData`) triggers `downloadMedia`'s two plain GETs (a metadata
lookup, then the CDN url it returns), neither of which carries a JSON body — the stub branches on `httpMethod === 'GET'` before the send-message logic, which assumes a POST. Testing that media-download path meaningfully needs *real* bytes: `resolveInboundMimeType` sniffs magic bytes before trusting a declared type, so `StubResponse` supports an optional `rawResponseBody` (raw bytes, no `JSON.stringify`) alongside the normal JSON `responseBody` — see `WHATSAPP_MEDIA_CONTENT` in whatsapp's `replay-test-context` for genuine minimal file content per media kind. Attachments also need an `attachmentService` passed to `createReplayContextSetup` (opt-in; `createMock
Read more
Channel Integration Tests
This guide explains how to add chat platform integration tests for agent channel integrations. The current suite covers three layers:
- Shared contract tests for behavior every channel integration must support.
- Synthetic platform tests for hand-built edge cases.
- Recorded replay tests for real webhook payloads captured from a local run.
These tests validate n8n's integration logic: routing inbound messages to the agent, preserving message context, executing integration actions, resuming suspended tool calls, and avoiding self-trigger loops.
Design: real adapters, no fakes
These tests run the **real** `@chat-adapter/*` adapters and the real `chat` SDK. There are no fake or in-memory adapters. The packages are ESM-only; in production they are loaded via `esm-loader`'s `new Function()` indirection to survive the CJS transform, but **that indirection cannot run under Vitest** — so the test helpers import them directly (`await import('@chat-adapter/telegram')`), which Vitest loads natively. Any test that drives a code path which itself calls `esm-loader` (e.g. the real `ComponentMapper` for rich cards) must `vi.mock('../esm-loader', …)` to redirect the loaders to native dynamic imports — see the telegram tests for the pattern.
The only thing faked is the **external platform HTTP at the network boundary** — you cannot call live Telegram/Slack/Linear from CI. Everything else (adapter, `AgentChatBridge`, integration implementation, action executor, message-context service) is real.
Answering the network boundary
Each platform adapter uses a different HTTP client, so the interception mechanism differs:
| Platform | Adapter HTTP client | Interception | Helper | |----------|---------------------|--------------|--------| | Telegram | native `fetch` | replace `globalThis.fetch` | `installFetchStub` (replay-test-helpers) | | Slack | `@slack/web-api` (axios) | `nock` at the HTTP layer | inline in slack `replay-test-context` | | Linear | `@linear/sdk` (GraphQL over fetch) | replace `globalThis.fetch` | `installFetchStub` | | WhatsApp | native `fetch` | replace `globalThis.fetch` | `installWhatsAppApiStub` (whatsapp `replay-test-context`) | | Teams | `@microsoft/teams.*` (axios) + `jwks-rsa` (node `https`) | `nock` at the HTTP layer | inline in teams `replay-test-context` |
Responses are answered from two sources, in order of preference:
1. **Recorded data** where it matters — the captured webhook payload is replayed into the real adapter as the inbound event, and recorded outbound bodies inform assertions. 2. **Minimal stubs** for incidental calls the recordings don't need to pin down — identity bootstrap (`getMe`, `auth.test`, Linear `viewer`), streaming lifecycle, and entity look-ups.
The assertions check what the adapter **sends** (the outbound request body), not what it receives, so response stubs only need to be valid enough for the real adapter to proceed.
Platform notes
- **Telegram** — `getMe` returns the bot fixture so the adapter learns its identity; `sendMessage`
returns a minimal message. `reply_markup` is sent as a JSON object (not a stringified blob), so read inline-keyboard callback data via `getTelegramInlineCallbackData`.
- **Slack** — agent replies go through Slack's assistant **streaming** API
(`chat.startStream` → `appendStream` → `stopStream`), not `chat.postMessage`. The nock handler reconstructs the streamed text and records it as a synthetic `chat.postMessage` so assertions can treat the reply as one outbound post. `webhookVerifier: () => true` bypasses signature checks (the fixtures carry sanitized signatures); passing `botUserId` skips the `auth.test` lookup.
- **Teams** — the only platform whose inbound requests must carry a **real signed token**.
`createTeamsAdapter` builds a `@microsoft/teams.apps` `App`, which validates every activity's Bot Framework JWT, and the adapter exposes no bypass: there is no `webhookVerifier` like Slack's, and `TeamsAdapterConfig` cannot pass the Teams SDK's `skipAuth` through. So the helper generates an RSA keypair per run, signs an activity token (issuer `https://api.botframework.com`, audience the app ID, a `serviceurl` claim matching the activity), and serves the matching public JWK from a nocked `https://login.botframework.com/v1/.well-known/keys`. The adapter's own validation then runs for real — `sendUnauthenticatedWebhook` asserts it still rejects. Two non-obvious stubs are also required: the client-credentials token endpoint is **tenant**-scoped, not `botframework.com`-scoped, and the SDK decodes the access token it gets back, so that stub must return a real JWT rather than an opaque string.
- **Linear** — webhooks are HMAC-signed (`linear-signature`) and timestamp-checked, so the helper
refreshes `webhookTimestamp` and signs the body. `@linear/sdk` strictly deserializes typed entities and lazily fetches relationships, so the GraphQL stub returns fully-shaped entities (e.g. a `Comment` needs `reactions: []`; an `AgentActivity` references `agentSession`/`sourceComment` by id). Linear's "mention" is an **agent-session** event, not a comment — see the contract note below.
- **WhatsApp** — inbound media (`fetchData`) triggers `downloadMedia`'s two plain GETs (a metadata
lookup, then the CDN url it returns), neither of which carries a JSON body — the stub branches on `httpMethod === 'GET'` before the send-message logic, which assumes a POST. Testing that media-download path meaningfully needs *real* bytes: `resolveInboundMimeType` sniffs magic bytes before trusting a declared type, so `StubResponse` supports an optional `rawResponseBody` (raw bytes, no `JSON.stringify`) alongside the normal JSON `responseBody` — see `WHATSAPP_MEDIA_CONTENT` in whatsapp's `replay-test-context` for genuine minimal file content per media kind. Attachments also need an `attachmentService` passed to `createReplayContextSetup` (opt-in; `createMock
Fair-code platform to build and deploy AI agents and workflows. Combine a visual canvas with custom code, run it self-hosted or in the cloud, and connect to 1500+ integrations. AI automation you can trust with real work, from prototype to production.
Other agents on n8n.
More agents from other plugins.
aris-fanout-leaf
AgentRead-only leaf worker for one bounded ARIS fan-out shard. Never delegates, writes files, or renders verdicts.
branch-diff-reviewer
AgentUse this agent when you want a comprehensive code review of changes in the current branch compared to main. This includes…
code-reviewer
AgentExpert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing…
Questions about CHANNEL_INTEGRATION_TESTS.
- Which coding agents can use CHANNEL_INTEGRATION_TESTS?
- CHANNEL_INTEGRATION_TESTS installs in Claude Code and Cursor: each loads subagents from .claude/agents, so copy the file there. Flowy's auto-invocation runs in Claude Code only.

