* feat(slack): enable DM support with per-thread IDs and implicit mentions DMs now use per-message threadTs (same as channels) instead of collapsing all DM messages into one thread. Each top-level DM starts a new conversation; thread replies continue the same conversation. DM messages automatically set isMention=true so bots process them without requiring @mention — natural for 1:1 conversations. * fix: use empty threadTs for top-level DMs, thread_ts for replies Top-level DM messages use empty threadTs to match openDM() subscriptions. DM thread replies use thread_ts for per-conversation isolation. Fixes integration test replay-dm.test.ts. * style: fix biome formatting * test: add e2e replay tests for direct DM implicit mention * style: fix extra blank line lint error * test: use real recorded DM fixtures for e2e tests
Replay Test Fixtures
Replay tests verify the Chat SDK handles real webhook payloads correctly by recording production interactions and replaying them in tests.
Quick Start: SHA-Based Recording Workflow
The recommended workflow ties recordings to git commits, making it easy to capture and convert production interactions into tests.
1. Deploy with recording enabled
# Set in Vercel environment variables (or .env.local for local dev)
RECORDING_ENABLED=true
REDIS_URL=redis://...
When deployed, recordings are automatically tagged with VERCEL_GIT_COMMIT_SHA:
session-{SHA}-{timestamp}-{random}
2. Interact with your bot
Perform the interactions you want to test:
- @mention the bot in Slack, Teams, or Google Chat
- Click buttons in cards (actions)
- Add emoji reactions
- Send follow-up messages
- Request DMs ("DM me")
3. Find recordings for your SHA
cd examples/nextjs-chat
# List all recording sessions
pnpm recording:list
# Find sessions for current commit
pnpm recording:list | grep $(git rev-parse HEAD | cut -c1-7)
# Output shows sessions with entry counts:
# session-abc123... (5 entries)
4. Export recordings
# Export a specific session (note: filter out pnpm output)
pnpm recording:export session-abc123 2>&1 | \
grep -v "^>" | grep -v "^\[dotenv" | grep -v "^$" > /tmp/recording.json
# Verify the export
cat /tmp/recording.json | jq 'length'
5. Analyze the recording
# Group webhooks by platform
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook")] | group_by(.platform) | .[] | {platform: .[0].platform, count: length}'
# View Slack webhook details
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook" and .platform == "slack") | .body | fromjson | .event | {type, text: .text[0:50], channel_type}]'
# View Google Chat webhook details
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook" and .platform == "gchat") | .body | fromjson | if .chat then {type: "direct", text: .chat.messagePayload.message.text[0:50]} else {type: "pubsub"} end]'
6. Extract webhook payloads into fixtures
# Extract Slack webhooks
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook" and .platform == "slack") | .body | fromjson]'
# Extract Google Chat webhooks
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook" and .platform == "gchat") | .body | fromjson]'
# Extract Teams webhooks
cat /tmp/recording.json | jq '[.[] | select(.type == "webhook" and .platform == "teams") | .body | fromjson]'
7. Create fixture file
Create a JSON file in the appropriate fixtures directory:
{
"botName": "My Bot",
"botUserId": "U123...",
"mention": {
/* first webhook - the @mention */
},
"followUp": {
/* follow-up message webhook */
}
}
8. Write the replay test
See existing tests for examples:
replay.test.ts- Basic mention and follow-up messagingreplay-actions-reactions.test.ts- Button clicks and emoji reactionsreplay-dm.test.ts- Direct message flows
Fixture Directory Structure
fixtures/replay/
├── README.md
├── slack.json # Basic Slack messaging
├── gchat.json # Basic Google Chat messaging
├── teams.json # Basic Teams messaging
├── actions-reactions/ # Button clicks and reactions
│ ├── slack.json
│ ├── gchat.json
│ └── teams.json
└── dm/ # Direct message flows
├── slack.json
├── gchat.json
└── teams.json
Fixture Formats
Basic messaging (slack.json, gchat.json, teams.json)
{
"botName": "My Bot",
"botUserId": "U123...",
"mention": {
/* webhook body for @mention */
},
"followUp": {
/* webhook body for follow-up message */
}
}
Actions & reactions (actions-reactions/*.json)
{
"botName": "My Bot",
"botUserId": "U123...",
"mention": {
/* webhook to subscribe the thread */
},
"action": {
/* button click webhook */
},
"reaction": {
/* emoji reaction webhook */
}
}
DM flows (dm/*.json)
{
"botName": "My Bot",
"botUserId": "U123...",
"dmChannelId": "D123...", // Slack only
"dmSpaceName": "spaces/...", // GChat only
"mention": {
/* initial @mention in channel */
},
"dmRequest": {
/* user says "DM me" in thread */
},
"dmMessage": {
/* user's message in DM */
}
}
Platform-Specific Webhook Formats
Google Chat
| Event | Format |
|---|---|
| Mention | Direct webhook with chat.messagePayload |
| Follow-up | Pub/Sub with message.data (base64) |
| Reaction | Pub/Sub with ce-type: "google.workspace.chat.reaction.v1.created" |
| Action | type: "CARD_CLICKED" event |
| DM Space | chat.messagePayload.space.type: "DM" |
Slack
| Event | Format |
|---|---|
| Mention | event_callback with event.type: "app_mention" |
| Follow-up | event_callback with event.type: "message" and thread_ts |
| Reaction | event_callback with event.type: "reaction_added" |
| Action | block_actions (URL-encoded form: payload=...) |
| DM | event_callback with event.channel_type: "im" |
Raw emoji format: Slack shortcode without colons (e.g., +1, heart)
Teams
| Event | Format |
|---|---|
| Mention | type: "message" with bot in entities array |
| Follow-up | type: "message" with same conversation.id |
| Reaction | type: "messageReaction" with reactionsAdded array |
| Action | type: "message" with value.actionId |
| DM | conversation.conversationType: "personal" |
Raw emoji format: Teams reaction type (e.g., like, heart)
Recording Implementation Details
The recorder (examples/nextjs-chat/src/lib/recorder.ts) stores entries in Redis:
- Key:
recording:{sessionId} - TTL: 24 hours
- Entry types:
webhook(incoming) andapi-call(outgoing)
Session ID format when VERCEL_GIT_COMMIT_SHA is set:
session-{SHA}-{ISO timestamp}-{random 6 chars}
This makes it easy to find all recordings from a specific deployment.
Troubleshooting
Recording export shows log output instead of JSON
The pnpm command outputs logging to stdout. Filter it out:
pnpm recording:export <session-id> 2>&1 | grep -v "^>" | grep -v "^\[dotenv" | grep -v "^$"
DM tests failing with subscription issues
DM threads need explicit subscription. When using openDM(), remember to call subscribe():
const dmThread = await chat.openDM(message.author);
await dmThread.subscribe();
await dmThread.post("Hello!");
Google Chat Pub/Sub messages have base64 data
Decode the message.data field:
echo '<base64-data>' | base64 -d | jq .
Teams DM not working
Teams DM functionality requires additional Azure Bot configuration. See SETUP.md for details.