Skip to content

test(integration-tests): add Emulate.dev-backed tests for the Slack adapter#477

Merged
bensabic merged 8 commits into
mainfrom
tests/slack-emulator-integration
May 11, 2026
Merged

test(integration-tests): add Emulate.dev-backed tests for the Slack adapter#477
bensabic merged 8 commits into
mainfrom
tests/slack-emulator-integration

Conversation

@bensabic

Copy link
Copy Markdown
Contributor

Summary

Add a new integration-test suite that drives the Slack adapter against an in-process @emulators/slack server (Vercel Labs' Emulate.dev) instead of mocks. Tests assert on the emulator's stateful store (messages, reactions, installations) rather than on mock.calls, so they catch wire-format and contract issues that pure mocks miss.

  • New private devDeps in packages/integration-tests: @emulators/slack, @emulators/core, @hono/node-server. No published-package surface change.
  • New harness packages/integration-tests/src/slack-emulator-utils.ts boots the emulator on an ephemeral 127.0.0.1 port, seeds a deterministic team / channel / bot / human / OAuth app, and exposes a tiny re-signing forwarder so the emulator's outbound event_callback deliveries hit the SDK with valid x-slack-signature headers.
  • Five new test files (19 tests), wired to the adapter via its existing apiUrl + signingSecret config — zero source changes to packages/adapter-slack:
    • emulator-slack-auth.test.ts (3) — auth.test populates botUserId during initialize().
    • emulator-slack-post-message.test.ts (5) — plain text + threaded thread.post, edit, delete, markdown.
    • emulator-slack-reactions.test.ts (3) — addReaction / removeReaction / multi-user reactions.
    • emulator-slack-events.test.ts (4) — full inbound event_callback round-trip, including bot self-message filtering.
    • emulator-slack-oauth.test.ts (4) — handleOAuthCallback exchanges a real authorization code via oauth.v2.access and persists the installation.
flowchart LR
  subgraph Test["Vitest test process"]
    SDK[SlackAdapter + Chat]
    Forwarder["HTTP forwarder<br/>signs x-slack-signature"]
    Emu["@emulators/slack<br/>(in-process Hono)"]
  end

  SDK -->|"postMessage / auth.test / oauth.v2.access<br/>(apiUrl override)"| Emu
  Emu -->|"event_callback POST"| Forwarder
  Forwarder -->|"HMAC-signed webhook<br/>chat.webhooks.slack()"| SDK
Loading

Out of scope (unchanged from existing mock-based tests)

The emulator does not implement chat.postEphemeral, conversations.open, views.*, files.uploadV2, assistant.threads.*, or chatStream. Those flows continue to be covered by the existing tests in packages/integration-tests/src/slack.test.ts and packages/adapter-slack/src/index.test.ts.

Test plan

  • pnpm --filter @chat-adapter/integration-tests test — 414 tests pass across 35 files (including the 19 new emulator tests, ~470 ms when sharing a single emulator per file).
  • pnpm validate — green: knip clean, ultracite check clean, all 33 typecheck tasks pass, all 35 test packages pass, 19 build tasks pass.
  • CI safety verified: ephemeral ports (port: 0), loopback-only binds (127.0.0.1), deterministic teardown via httpServer.close(), no env vars, no external network egress.

Checklist

  • All commits are signed and verified
  • pnpm validate passes
  • Changeset added (or N/A — see CONTRIBUTING.md) — N/A: @chat-adapter/integration-tests is private: true and the change is test-only.
  • Documentation updated (or N/A) — packages/integration-tests/README.md describes the new emulator-* test category.

bensabic added 5 commits May 10, 2026 13:37
Add @emulators/slack, @emulators/core, and @hono/node-server as
devDependencies of the private integration-tests package. These power
the upcoming in-process Slack emulator harness used to drive the Slack
adapter against a stateful, Slack-shaped HTTP server instead of mocks.
Introduce slack-emulator-utils.ts, a test-only helper that boots the
@emulators/slack Hono app on an ephemeral 127.0.0.1 port via
@hono/node-server and pre-seeds a deterministic team / channel / bot /
human user / OAuth app. The Slack adapter is wired to it via its
existing apiUrl config; no source changes required.

Also exposes startSlackWebhookForwarder, a tiny in-process Node http
forwarder that re-signs the emulator's outbound event_callback
deliveries with x-slack-signature / x-slack-request-timestamp before
handing them to chat.webhooks.slack(...). The emulator's core
WebhookDispatcher only emits GitHub-style X-Hub-Signature-256 headers,
so this bridge is what makes inbound flows exercise the SDK's real
HMAC verification path.

The handle returns direct access to the emulator's Store and
WebhookDispatcher so tests can assert on persisted state instead of
mock call records.
… via emulator

Add three test files that drive the SlackAdapter's outbound WebClient
calls through the in-process emulator and assert on its stateful store
rather than on mock call records:

- emulator-slack-auth.test.ts (3 tests): auth.test populates botUserId
  during initialize(); explicit botUserId is respected; multi-workspace
  mode skips the call entirely.
- emulator-slack-post-message.test.ts (5 tests): plain text and threaded
  thread.post round-trip into the messages collection and are visible
  via conversations.replies; editMessage updates via chat.update;
  deleteMessage removes via chat.delete; markdown posts succeed.
- emulator-slack-reactions.test.ts (3 tests): addReaction /
  removeReaction round-trip via reactions.add / reactions.remove and
  show up via reactions.get; multi-user reactions accumulate correctly.

These exercise the full HTTP path against a Slack-shaped server,
catching wire-format and contract issues that pure mocks miss.
…all via emulator

Add two test files that drive end-to-end flows previously only
verifiable against real Slack:

- emulator-slack-events.test.ts (4 tests): a human posts to the
  emulator, which dispatches an event_callback to the local forwarder,
  which signs the body and hands it to chat.webhooks.slack(...). The
  SDK's onNewMention and onNewMessage handlers run with a live Thread
  and the bot's reply lands back in the emulator. Bot self-messages
  are correctly filtered. This is the only Slack adapter test in the
  repo that covers the full inbound-then-outbound round-trip without
  hand-crafted webhook payloads.
- emulator-slack-oauth.test.ts (4 tests): handleOAuthCallback
  exchanges a real authorization code via oauth.v2.access against the
  emulator's authorize/callback flow; the resulting installation is
  persisted in the state adapter; invalid codes and mismatched
  client_secrets are rejected; the freshly issued bot token works for
  subsequent chat.postMessage calls via withBotToken.
Add an "Emulator tests" entry to the package README so newcomers can
distinguish the new emulator-backed suite from the existing unit and
replay tests, and find the harness in slack-emulator-utils.ts.
@bensabic bensabic requested a review from a team as a code owner May 10, 2026 03:41
@vercel

vercel Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat-sdk-nextjs-chat Ready Ready Preview, Comment May 11, 2026 9:56pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
chat Ignored Ignored May 11, 2026 9:56pm

@socket-security

socket-security Bot commented May 10, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​emulators/​core@​0.5.0751008692100
Addednpm/​@​emulators/​slack@​0.5.0751009492100
Addednpm/​@​hono/​node-server@​2.0.210010010096100

View full report

@bensabic bensabic requested a review from ctate May 10, 2026 03:43
Comment thread packages/integration-tests/src/slack-emulator-utils.ts Outdated
`applyTokenSeed` (used during `reset()`) was granting only
["chat:write", "channels:read"] to seeded tokens, while the initial
`createCoreServer({ tokens })` call granted the full bot/human scope
sets. After the first `reset()` the bot token silently lost
`channels:history`, `users:read`, `reactions:read`, and
`reactions:write`, which would surface as flaky behaviour for any test
that relied on those scopes after a reset.

Unify both call sites on a single `buildTokenSeedEntries` helper so
fresh-boot and post-reset state always grant the same scopes. Add a
regression test in emulator-slack-auth.test.ts that triggers a manual
`emulator.reset()` and re-asserts that the bot token still resolves
via auth.test.
visyat
visyat previously approved these changes May 11, 2026

@visyat visyat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple small comments for cleanup/organization. Otherwise looks good.

Comment thread packages/integration-tests/src/emulator/slack/auth.test.ts
Comment thread packages/integration-tests/src/emulator-slack-auth.test.ts Outdated
Comment thread packages/integration-tests/src/emulator/slack/auth.test.ts
bensabic added 2 commits May 12, 2026 07:54
…ulator/slack/

Address review feedback (visyat) by moving the flat
`emulator-slack-*.test.ts` files into a per-adapter directory:

  packages/integration-tests/src/emulator/slack/
      utils.ts
      auth.test.ts
      events.test.ts
      oauth.test.ts
      post-message.test.ts
      reactions.test.ts

This scales cleanly as more adapter emulator suites land (e.g.
`emulator/github/`), instead of cluttering the top-level src tree with
adapter-prefixed file names.

Also hoist the duplicated `silentLogger` definition from each test
file into the shared `utils.ts`, removing five identical copies.

No behavior changes. All 20 emulator tests still pass.
… via emulator

Add `emulator/slack/multi-workspace.test.ts` exercising the path that
was previously only covered by replay/mock-based tests: the adapter
runs without a hardcoded `botToken`, multiple workspaces are persisted
in the state adapter via `adapter.setInstallation(teamId, ...)`, and
inbound `event_callback`s for either team are routed to the correct
per-tenant bot token end-to-end against the shared emulator.

To make this work, the helper grew two small additions:

- `addEmulatorWorkspace(emulator, seed)` — register an additional
  team + bot + channel + token on an already-booted emulator, so two
  tenants live side-by-side without needing two emulator instances.
- The inbound forwarder's `augmentEventEnvelope` now accepts an
  optional `resolveTeamId(envelope)` callback, defaulting to a constant
  teamId for the existing single-workspace tests. The multi-workspace
  test passes a resolver that looks up the dispatched event's channel
  in the emulator store and returns the owning team's id, so the SDK's
  per-team token resolver sees the right `team_id` on the envelope.

Three new tests: team-A routing, team-B routing, and `getInstallation`
returning null for unknown team ids. Follow-up from review feedback on
PR #477.
@bensabic bensabic merged commit 14b1434 into main May 11, 2026
17 checks passed
@bensabic bensabic deleted the tests/slack-emulator-integration branch May 11, 2026 22:29
bensabic added a commit that referenced this pull request May 12, 2026
…adapter (#479)

## Summary

Add an Emulate.dev-backed integration-test suite for the GitHub adapter,
mirroring the structure of the Slack work in #477. Tests drive the
adapter against an in-process
[`@emulators/github`](https://emulate.dev/docs/github) server and assert
on its stateful store (comments, reviews) rather than `mock.calls`,
catching wire-format and contract issues that pure Octokit mocks miss.

- New private devDeps in `packages/integration-tests`:
`@emulators/github`, `@emulators/core`, `@hono/node-server`, plus a
workspace dep on `@chat-adapter/github`. (`@emulators/core` and
`@hono/node-server` are also declared by #477; this PR is independent of
merge order.)
- New harness `packages/integration-tests/src/github-emulator-utils.ts`
boots the emulator on an ephemeral `127.0.0.1` port, seeds a
deterministic user / repo / issue / PR / starter review comment, and
exposes a near-passthrough HTTP forwarder. **No re-signing needed**
here. `@emulators/core`'s `WebhookDispatcher` already signs deliveries
with `X-Hub-Signature-256: sha256=<hex>` exactly as the GitHub adapter
expects. The harness also adds a small URL rewriter for Octokit's
`pulls.createReplyForReviewComment` shortcut endpoint, translating it
into the canonical review-comment POST that the emulator implements.
- Four new test files (12 tests), wired to the adapter via its existing
`apiUrl` + `webhookSecret` config **zero source changes** to
`packages/adapter-github`:
- `emulator-github-auth.test.ts` (2) `GET /user` populates `botUserId`
during `initialize()`.
- `emulator-github-comments.test.ts` (4) `thread.post` / `edit` /
`delete` on issue and PR-conversation threads.
- `emulator-github-reviews.test.ts` (3) review-comment replies routed
through `pulls.createReplyForReviewComment` with the right
\`in_reply_to_id\`, plus edit / delete.
- `emulator-github-events.test.ts` (3) full inbound `issue_comment` /
`pull_request_review_comment` round-trip, including bot self-message
filtering.

```mermaid
flowchart LR
  subgraph Test["Vitest test process"]
    SDK[GitHubAdapter + Chat]
    Forwarder["HTTP forwarder<br/>passthrough"]
    Emu["@emulators/github<br/>(in-process Hono)"]
  end

  SDK -->|"issues.createComment / pulls.* / GET /user<br/>(apiUrl override)"| Emu
  Emu -->|"X-Hub-Signature-256 + x-github-event"| Forwarder
  Forwarder -->|"chat.webhooks.github(request)"| SDK
```

### Out of scope (deliberate)

- **Reactions** `@emulators/github` does not implement the `/reactions`
endpoints used by the adapter. Reaction logic is still covered by the
existing mock-based tests in
`packages/adapter-github/src/index.test.ts`.
- **GitHub App auth** (JWT \u2192 installation token via `POST
/app/installations/:id/access_tokens`) the adapter and emulator both
support it, but PAT-mode was the agreed scope here.
- **Multi-tenant install flows** via `installation` webhook events.
- Branches/refs, releases, search, actions, checks not used by the
adapter.

## Test plan

- [x] `pnpm --filter @chat-adapter/integration-tests test` 407 tests
pass across 34 files (including the 12 new emulator-github tests, ~480
ms total).
- [x] `pnpm check` and `pnpm knip` clean.
- [x] CI safety verified: ephemeral ports (`port: 0`), loopback-only
binds (`127.0.0.1`), deterministic teardown via `httpServer.close()`, no
env vars, no external network egress.

## Checklist

- [x] All commits are signed and verified
- [x] \`pnpm validate\` passes
- [x] Changeset added (or N/A see [CONTRIBUTING.md](./CONTRIBUTING.md))
N/A: \`@chat-adapter/integration-tests\` is \`private: true\` and the
change is test-only.
- [x] Documentation updated (or N/A)
\`packages/integration-tests/README.md\` describes the new
emulator-github test category.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants