CHANNEL_INTEGRATION_TESTS
This guide explains how to add chat platform integration tests for agent channel integrations. The current suite covers three layers:
$ npx -y skills add n8n-io/n8n --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
This guide explains how to add chat platform integration tests for agent channel integrations. The current suite covers three layers:
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` |
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.
- **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.
Test Layout
src/modules/agents/integrations/
__tests__/channel-integration-contract.test.ts
__tests__/fixtures/<platform>/
__tests__/helpers/<platform>/replay-test-context.ts
__tests__/helpers/<platform>/synthetic-fixtures.ts
__tests__/helpers/replay-test-helpers.ts # shared: createReplayContextSetup, installFetchStub, …
platforms/__tests__/<platform>/recorded-integration.test.ts
platforms/__tests__/<platform>/synthetic-integration.test.ts
Each `<platform>/replay-test-context.ts` builds the real adapter + real `Chat`, installs the network interceptor, wires `AgentChatBridge` via `createReplayContextSetup`, and exposes `sendWebhook`, `latestContext`, `lastPost`, `apiCalls`, and `shutdown` (which restores the interceptor).
Shared Contract Tests
Use `runSharedChannelIntegrationContract()` when a scenario should behave the same across platforms. It verifies that an integration:
- Routes a mention or DM to `executeForChatPublished()`.
- Subscribes the thread and routes follow-up messages.
- Persists latest message context for the integration context tool.
- Responds through the integration action executor into the latest thread.
- Ignores messages authored by the connected bot.
> **Linear is intentionally not in the shared contract.** The real `@chat-adapter/linear` only treats > agent-session events as mentions (a bare comment has `isMention = false`), and agent-session vs > comment threads don't share an id — so the comment-as-mention + subscribe/follow-up contract > doesn't model real Linear behavior. Linear's real flow is covered by its recorded agent-session > test (`platforms/__tests__/linear/recorded-integration.test.ts`).
Assert the real adapter's actual output, not a simplified shape. For example, the message-context `channelId` is platform-prefixed (`telegram:123456`, `slack:C_SUPPORT`), and Slack agent replies are recorded with a `markdown_text` body.
Synthetic Integration Tests
Use synthetic tests for cases that are hard to capture reliably or need narrow edge-case co
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` |
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.
- **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.
Test Layout
src/modules/agents/integrations/ __tests__/channel-integration-contract.test.ts __tests__/fixtures/<platform>/ __tests__/helpers/<platform>/replay-test-context.ts __tests__/helpers/<platform>/synthetic-fixtures.ts __tests__/helpers/replay-test-helpers.ts # shared: createReplayContextSetup, installFetchStub, … platforms/__tests__/<platform>/recorded-integration.test.ts platforms/__tests__/<platform>/synthetic-integration.test.ts
Each `<platform>/replay-test-context.ts` builds the real adapter + real `Chat`, installs the network interceptor, wires `AgentChatBridge` via `createReplayContextSetup`, and exposes `sendWebhook`, `latestContext`, `lastPost`, `apiCalls`, and `shutdown` (which restores the interceptor).
Shared Contract Tests
Use `runSharedChannelIntegrationContract()` when a scenario should behave the same across platforms. It verifies that an integration:
- Routes a mention or DM to `executeForChatPublished()`.
- Subscribes the thread and routes follow-up messages.
- Persists latest message context for the integration context tool.
- Responds through the integration action executor into the latest thread.
- Ignores messages authored by the connected bot.
> **Linear is intentionally not in the shared contract.** The real `@chat-adapter/linear` only treats > agent-session events as mentions (a bare comment has `isMention = false`), and agent-session vs > comment threads don't share an id — so the comment-as-mention + subscribe/follow-up contract > doesn't model real Linear behavior. Linear's real flow is covered by its recorded agent-session > test (`platforms/__tests__/linear/recorded-integration.test.ts`).
Assert the real adapter's actual output, not a simplified shape. For example, the message-context `channelId` is platform-prefixed (`telegram:123456`, `slack:C_SUPPORT`), and Slack agent replies are recorded with a `markdown_text` body.
Synthetic Integration Tests
Use synthetic tests for cases that are hard to capture reliably or need narrow edge-case co
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.
Repo: n8n-io/n8n
Other agents on n8n.
- developer
Use this agent for any n8n development task - frontend (Vue 3), backend (Node.js/TypeScript), workflow engine, node creation, or full-stack features. The agent automatically applies n8n conventions and best practices. Examples: <example>user: 'Add a new button to the workflow
Open agent - linear-issue-triager
Use this agent proactively when a Linear issue is created, updated, or needs comprehensive analysis. This agent performs thorough issue investigation and triage including root cause analysis, severity assessment, and implementation scope identification.
Open agent - prompt-caching
`Agent.promptCaching()` generates provider-specific `providerOptions` for Anthropic and OpenAI so callers don't have to hand-write the Vercel AI SDK's raw `cacheControl` / `promptCacheKey` shapes. It layers on top of the existing `providerOptions` escape hatch — nothing here is
Open agent

