Skip to content
Back to skills

Slack Chat Validation

ASecurity

Run the live Slack chat-integration validation suite — a real Slack account messaging a real bot, driving a real app instance and a real container, asserting on what actually lands in Slack. Use before and after any change to src/shared/lib/chat-integrations/, to the user-input request path that feeds it, or to the "can't do this in chat" notice.

  • 24 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
businessrustgobashsqlnodedockergitapi

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add SkillfulAgents/SuperAgent --skill slack-chat-validation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Slack Chat Validation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Slack Chat Validation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/skillfulagents-slack-chat-validation/badge)](https://www.skillsdirectory.com/skills/skillfulagents-slack-chat-validation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: slack-chat-validation
description: Run the live Slack chat-integration validation suite — a real Slack account messaging a real bot, driving a real app instance and a real container, asserting on what actually lands in Slack. Use before and after any change to src/shared/lib/chat-integrations/, to the user-input request path that feeds it, or to the "can't do this in chat" notice.
---

# Slack chat-integration validation

## Why this exists

Every other test around `src/shared/lib/chat-integrations/` is a **contract test**:
it hands a connector an event object and asserts what the connector does with
it. That pins the connector's behaviour and nothing else. If a refactor stops
the host from *emitting* those events — or emits them for a different session,
or stops registering the underlying request — every one of those tests stays
green and the feature is dead.

This suite closes that gap. Nothing on the path is mocked:

```
a real human Slack account
  → real Slack (Socket Mode)
    → a real app instance booted against an isolated data dir
      → a real agent container running a real turn
        → back to Slack
          → assertions read the actual message, Block Kit payload included
```

Run it **before** starting a chat-integration change to record a green
baseline, and **after** to prove nothing regressed.

## Prerequisites

Checked automatically by `preflight.mjs`; run that first when anything looks off.

1. **A source install with exactly one Slack integration.** By default
   `~/Library/Application Support/Superagent-dev`. Its bot and app tokens are
   read from the integration row; they are never printed.
2. **A second Slack identity that can post as somebody other than the bot.**
   Resolved from `connected_accounts` in the source install, then in the
   packaged install (`…/Superagent`). Two shapes work, and which one is found
   decides the surface:
   - **human (user) token** → the suite runs in a **DM with the bot**. Preferred:
     it's how the feature is really used, and only a human identity can open a
     `D…` channel.
   - **another app's bot token** → the suite runs in a **shared public channel**.
     Slack won't let one app post into another app's DM. The sender joins a
     public channel the bot is already in if it isn't in one.

   Composio-managed connections usually **do not** expose a raw token (Composio
   returns a redacted one). That is expected: the harness falls back to
   executing Slack calls through Composio's proxy, and a token Slack rejects
   with `invalid_auth` is treated as "try the proxy", not "dead connection".
   If no identity resolves at all, reconnect Slack in the app's
   connected-accounts UI — the harness picks a human token up automatically on
   the next run, no code change needed.
3. **Docker running**, with the agent image named in the source install's
   `settings.container.agentImage` (normally `superagent-container:latest`,
   built by `npm run build:container`).
4. **No other Superagent instance holding the same Slack app.** Slack
   load-balances events across Socket Mode connections, so a second instance
   makes the suite go nondeterministically quiet. Preflight flags a running
   packaged/dev app whose data dir has an active Slack integration.
5. **A free container publish range.** See the port hazard below.

## Running it

```bash
node e2e/live/slack-chat/preflight.mjs        # ~10s, proves the environment
node e2e/live/slack-chat/run.mjs              # full suite, web host (~5 min)
node e2e/live/slack-chat/run.mjs --host=electron   # desktop host
node e2e/live/slack-chat/run.mjs --no-public-url   # cloud host, no HOST_PUBLIC_URL
```

Useful flags:

| Flag | Effect |
|---|---|
| `--only=<tag or ids>` | Run a subset: `--only=unsupported`, `--only=question`, `--only=inbound-turn,question-card` |
| `--host=web\|electron` | Which host shape to boot (default `web`) |
| `--no-public-url` | Boot the cloud host with `HOST_PUBLIC_URL` unset |
| `--public-url=<url>` | Override the cloud base (default `https://validation.example.test`) |
| `--boot-only` | Seed + boot + connect, then stop. Environment check without spending turns |
| `--keep-app` | Leave the app running afterwards for manual poking |
| `--no-retry` | Disable the one automatic retry per check |
| `--with-computer-use` | Include the computer-use check — **drives the real machine** |
| `--channel=#name` | Pin the channel when the surface is a channel |
| `--container-base-port=N` | Move the container publish range (default 5300) |
| `--source=<dir>` / `--sender-source=<dir>` | Point at other installs |

Each run writes `report.json` and the full app log to a temp run dir, printed
at the end.

### A full pass before a chat-integration change

```bash
node e2e/live/slack-chat/run.mjs                   # 12/12
node e2e/live/slack-chat/run.mjs --host=electron   # 12/12 — desktop links
node e2e/live/slack-chat/run.mjs --no-public-url --only=unsupported   # link omitted
```

## What it isolates (and what it will never touch)

`seed.mjs` builds `~/Downloads/slack-chat-validation` from the source install:
`settings.json` verbatim, a fresh single-agent workspace, and a copy of the
SQLite file pruned to **one** chat integration retargeted at the slug
`slack-validation-agent`.

That slug is the isolation boundary. The only container the suite ever creates,
stops, or removes is `superagent-slack-validation-agent`. Scheduled tasks,
webhook triggers, other integrations, and every other agent's rows are emptied
in the copy, so the seeded install can't wake anything on its own. **The source
install is only ever read.**

## What each check protects

| Check | Protects |
|---|---|
| `inbound-turn` | An inbound Slack message creates a chat session bound to a real container session whose transcript holds the turn |
| `question-card` | `AskUserQuestion` renders as Block Kit with one button per option, `answer_<n>` action ids, and values that carry the request, question and answer |
| `question-answered-in-app` | The card posted to Slack and the request the **app** answers are the same registry entry: answering through the app's decision route settles it and the continuation lands back in Slack |
| `question-freetext-reply` | A plain-text reply to an open single-question card settles it and the agent responds — and the report says **which** path ran (answer-in-turn vs cancel → fresh turn) |
| `question-cancelled-by-unrelated-message` | An unrelated message cancels the parked question instead of deadlocking behind it, and the fresh turn runs |
| `question-multi-is-not-consumed-by-text` | A multi-question ask posts one card per question, and text does **not** answer it (an answer would be attributed to the wrong question) |
| `file-delivery-notice` | `deliver_file` reaches the conversation as real uploaded bytes with the description as caption (text fallback accepted and reported) |
| `unsupported-*` (7) | Per kind: the exact refusal wording, the **exact host-aware link tail**, no interactive card, and the request staying parked in the registry |

`unsupported-capability-review` is the newest row and the one with the least
margin: `workflows: 'review'` is the shipped default, so asking the agent to run
a workflow parks a capability review — which chat was silent about entirely
before the connectors moved onto the unified request wire. It needs the agent to
actually reach for the `Workflow` tool, so it is the row most likely to need its
retry.

`question-freetext-reply` reports its path rather than asserting one, because
the two are connector-dependent: the free-text **answer** path needs
`answerOpenQuestionWithText`, which only the Telegram connector implements. On
Slack the same message legitimately takes the **cancel** path. Both settle the
request and both get a reply, so a check that only asserted "the agent
answered" would stay green if the answer path broke outright — which is what
the previous version of this check did.

The `unsupported-*` checks are the regression test for the host-aware notice
(PR #563). They rebuild the expected tail from the host shape and assert the
message **ends with it**, so all three rows of that matrix are distinguished:

| Host | Expected tail |
|---|---|
| `--host=electron` | ` Open Gamut on your desktop to continue: superagent-dev://agent/<slug>/sessions/<id>` |
| `--host=web` + `HOST_PUBLIC_URL` | ` Open Gamut to continue: <base>/agents/<slug>/sessions/<id>` |
| `--host=web --no-public-url` | ` Open Gamut to continue.` (no link — never a broken URL) |

The `<id>` is asserted to be the conversation's **actual** session, which is
what stops a queued notice from linking a rotated-away session.

## Known limits — read before trusting a green run

- **Button taps are not automated.** Slack has no API to simulate clicking a
  Block Kit button; interactions only arrive from a real client over Socket
  Mode. The suite asserts the card is posted with the right buttons and
  `action_id`s, and covers a real decision via `question-answered-in-app` (the
  app's decision route settles the same registry entry the Slack card came
  from). The `action_id` → decision round trip still needs a human tap.
- **The already-handled gate is not live-reachable on Slack.** A press on a card
  whose request was settled elsewhere must be refused rather than buffered in
  the container. Reaching it needs an interactive response, which on Slack means
  a button tap — and the free-text branch does not produce one, because Slack
  has no `answerOpenQuestionWithText`. That gate is covered instead by the
  wire-contract suite in `chat-integration-e2e.test.ts`, which drives the real
  persister and the real registry.
- **`browser_input` is not asserted as parked** (`requiresParked: false`).
  `mcp__user-input__request_browser_input` is only in the web-browser
  subagent's tool list, so the main agent reaches it indirectly and the
  registration can settle with the subagent. The notice is still asserted.
- **Computer use is opt-in** (`--with-computer-use`) because triggering it
  drives the real machine.
- **`proxy_review` cards are not covered.** They arrive through the global
  notification path as a synthetic `question_request`, and triggering one needs
  a proxied API call that requires review.
- **One retry per check by default.** Each check drives a live model, and a
  model that wanders off is not the product breaking. A real regression fails
  both attempts. `--no-retry` disables it.

## Environment hazards these runs actually hit

- **Container publish range.** A packaged install's Lima runner forwards its
  container ports on `127.0.0.1`, and a loopback-specific bind **shadows**
  Docker's `0.0.0.0` publish. With the default base (4000) the validation app's
  host→container calls silently land in the *other* install's container and
  come back `401 Unauthorized` — which reads exactly like a broken build.
  The harness pins `SUPERAGENT_BASE_PORT=5300` (away from 4000 and from
  `dev:electron`'s 5000) and preflight checks the range is free.
- **`better-sqlite3` ABI.** The web and Electron hosts need different builds, so
  whichever ran last leaves the other broken. `startApp` rebuilds for the host
  about to run.
- **Electron outliving its wrapper.** `electron-vite dev` forks Electron;
  killing the wrapper alone leaves the app holding the Socket Mode session and
  writing into the seed dir (the next seed then fails with `ENOTEMPTY`). The
  harness kills the process group and then pkills any Electron whose app path is
  this repo, and the seed retries its clear.
- **Electron ignores `PORT`.** Main binds its own API from 47891 upward, so the
  harness reads the port back from the log line rather than assuming it.

## Extending it

Add an entry to `CHECKS` in `e2e/live/slack-chat/lib/checks.mjs`:

```js
{
  id: 'stable-kebab-id',
  title: 'reads as the behaviour being protected',
  tags: ['group'],            // selectable via --only
  async run(ctx) { /* ... */ return 'short detail for the report' },
  async cleanup(ctx) { /* leave the session idle */ },
}
```

`ctx` gives you `conv` (say / awaitBot / expectNoBot, watermarked), `api`
(sessions, messages, `pendingRequests` — the Phase 6 registry snapshot),
`agentSlug`, `integrationId`, and `hostShape`.

Two rules worth keeping:

1. **Predicates must be specific enough to fail.** An early version matched the
   delivered file on its filename alone and passed on the `🔧 Write` tool-call
   echo — a green check that tested nothing. If the agent echoes your marker for
   an unrelated reason, the check is not doing its job.
2. **Watch the registry from before the trigger**, not after the Slack message
   lands (`watchRegistry`). A request can register and settle inside one poll
   interval, and polling afterwards reports "never registered" for something
   that did.

## Files

```
e2e/live/slack-chat/
  preflight.mjs       environment check, no side effects beyond joining a channel
  run.mjs             the runner: seed → boot → checks → report
  lib/data-dir.mjs    read-only access to an install's settings + SQLite
  lib/slack.mjs       bot and sender identities, token vs Composio-proxy calling
  lib/surface.mjs     DM vs shared channel, and how the sender gets in
  lib/seed.mjs        the isolated data dir
  lib/app.mjs         booting web/electron, port discovery, API helpers
  lib/conversation.mjs watermarked send/await against the conversation
  lib/checks.mjs      the assertions
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.