End-to-end scenario testing
Verify that a running application does what it claims, by driving its real
interface the way a user would. The unit of work is a scenario card: a
short markdown test written for an agent to execute — not a Playwright/expect
script. Cards are high-level enough that a small UI shuffle doesn't invalidate
them, but precise enough that two agents running the same card reach the same
verdict.
A green unit test proves the wiring in isolation. A scenario proves the wiring
as assembled and rendered. They catch different bugs — write the card even
when the unit tests pass.
When to use this
- A feature touches a user-facing surface (button, palette command, status
indicator, keybinding, rendered message) and you want proof it works live.
- The user asks to "test it end to end" / "prove the UI works" / "run a scenario."
- You changed a layer (projection, capability gate, renderer) whose effect is
only observable in the assembled UI.
Don't use it for logic with no UI surface (unit-test that), or when a
production gate makes the live path unreachable (see Over-specification below).
The card format
One card = one .md file. Keep these sections; collapse any to one line when
the scenario is simple. Don't pad.
# <area>-<behavior>: one-line title
**What this covers**: the feature + the specific commits/IDs it exercises.
If something else breaks this, it should be caught here.
## Pre-state
What must be true before starting: a freshly built instance running, auth/creds
in place, a clean workdir. Give the exact commands to reach it.
## Steps
Numbered actions described by **intent**, each with the concrete command or
tool call and a real UI label (prefer labels the user sees over brittle
selectors like `#nav > li:nth-child(3)`).
## Expected
For each step, what you should observe — and the **falsification condition**:
"if you see X instead, the test fails." Silence is not success.
## Cleanup
Idempotent teardown so reruns are hermetic. Never touch state you didn't create.
## Sharp edges
Footguns, timing/ordering caveats, nondeterminism noted while recording.
Running a card
- Build fresh from the code under test. The single most common mistake is
testing a stale binary. Rebuild every layer your change touches (server,
client, embedded assets) and confirm the running instance is the new one,
not a process someone left up yesterday.
- Isolate. Run in a hermetic workdir. If the app holds a host-level
singleton (a lock, a fixed port, a shared state dir), point the test
instance at its own copy — e.g. override
$HOME/state-dir/port — so it can
neither collide with nor pollute (nor be polluted by) a real instance.
Symlink shared read-only inputs (creds, tokens); keep mutable state separate.
- Drive the surface (recipes below).
- Assert against the authoritative record, not just the pixels. The UI can
lie or lag; the on-disk state / log / database is ground truth. Cross-check
the rendered claim against it when an assertion is ambiguous.
- Capture evidence — a screenshot, the captured pane, the on-disk artifact.
- Clean up — shut down what you spawned, remove scratch dirs, leave
pre-existing instances running and untouched.
Driving a web UI (browser)
Use a Chrome/CDP browser tool. After authenticated navigation, drive the page
through eval against the app's own JS entry points rather than synthesizing
clicks where possible — it's more robust to layout change.
- Optimistic-vs-settled assertions: fire the action but don't await it,
take a synchronous DOM snapshot (the pending placeholder is there now),
then await and snapshot again. Without the no-await capture you can't tell
"rendered then reconciled" from "never rendered."
- Return a plain string from
eval (join your findings with \n); some
bridges stringify a returned object as [object Object].
- Inspect internal state via the app's singleton (
window.<App>?.state, etc.)
when the DOM is ambiguous.
Driving a CLI / TUI (tmux)
Each scenario gets its own named tmux session (cleanup needs a deterministic
name). Fix the size for deterministic capture; prefer the app's plain-text/inline
mode if it has one.
tmux new-session -d -s <name> -x 200 -y 50 "<cmd> 2>/tmp/<name>-stderr.log"
tmux send-keys -t <name> -l "literal text" # -l = no key-name parsing (paths, slashes)
tmux send-keys -t <name> Enter
tmux capture-pane -t <name> -p # -p = plain text; add -e only for styling
- Always
-l for user-typed strings; without it /foo/bar parses as escapes.
- Poll
capture-pane for a state string; grep the glyph/word, not the color.
- Redirect stderr to a file — panics and debug probes land there, not the pane.
Hard-won principles
- Falsification, always. Every assertion states what failure looks like. A
step that can't fail proves nothing. When watching for an outcome, make sure
your check would fire on the failure path, not just the happy path.
- Verify the right surface. The same concept often exists at several
layers (an internal capability vs. the REST projection of it; a model field
vs. the rendered chip). Confirm your assertion reads the surface that actually
carries the signal — a "missing" value is often present one layer over.
- Present but not visible ≠ absent. Scrollable bodies, virtualized lists,
and auto-scroll-to-bottom routinely push a real element out of the capture
window. Before concluding something didn't render, scroll/expand to where it
should be. Confirm via a sibling read (a status command reading the same
state) when the visual is hard to capture.
- Executing the card tests the card. Expect to find bugs in your own
scenario — a wrong selector, a wrong layer, an assertion the UI can't show.
Fix the card as you go; a card that "passes" because its check was vacuous is
worse than none.
- Over-specification trap. A card can describe a path that production gating
prevents (e.g. a keybind that's a no-op in the current mode). Confirm the gate
in the source rather than fighting it through the UI; verify the underlying
behavior with a unit test and note the gate in the card.
- Cleanup is part of the test. A half-shutdown fleet makes the next run's
polling return false positives. Make teardown idempotent and scoped to what
you created.
Finishing
Report each assertion as pass/fail with the concrete observation (the rendered
text, the on-disk value), not "looks good." If a card fails, capture the
evidence and either fix the bug or file it; don't soften the verdict.
1---2name: e2e-scenario-testing3description: Use when verifying a running application end-to-end through its real interface — a web UI, a CLI, or a TUI — by writing and executing agent-run "scenario cards" against a freshly built instance with falsifiable assertions. Trigger on "test it end to end", "prove the UI actually works", "write/run a scenario", or after a change touches a user-facing surface that unit tests can't fully cover. Not for unit tests, pure code review, or API-only checks.4---56# End-to-end scenario testing78Verify that a *running* application does what it claims, by driving its real9interface the way a user would. The unit of work is a **scenario card**: a10short markdown test written for an agent to execute — not a Playwright/expect11script. Cards are high-level enough that a small UI shuffle doesn't invalidate12them, but precise enough that two agents running the same card reach the same13verdict.1415A green unit test proves the wiring in isolation. A scenario proves the wiring16*as assembled and rendered*. They catch different bugs — write the card even17when the unit tests pass.1819## When to use this2021- A feature touches a user-facing surface (button, palette command, status22 indicator, keybinding, rendered message) and you want proof it works live.23- The user asks to "test it end to end" / "prove the UI works" / "run a scenario."24- You changed a layer (projection, capability gate, renderer) whose effect is25 only observable in the assembled UI.2627Don't use it for logic with no UI surface (unit-test that), or when a28production gate makes the live path unreachable (see *Over-specification* below).2930## The card format3132One card = one `.md` file. Keep these sections; collapse any to one line when33the scenario is simple. Don't pad.3435```markdown36# <area>-<behavior>: one-line title3738**What this covers**: the feature + the specific commits/IDs it exercises.39If something else breaks this, it should be caught here.4041## Pre-state42What must be true before starting: a freshly built instance running, auth/creds43in place, a clean workdir. Give the exact commands to reach it.4445## Steps46Numbered actions described by **intent**, each with the concrete command or47tool call and a real UI label (prefer labels the user sees over brittle48selectors like `#nav > li:nth-child(3)`).4950## Expected51For each step, what you should observe — and the **falsification condition**:52"if you see X instead, the test fails." Silence is not success.5354## Cleanup55Idempotent teardown so reruns are hermetic. Never touch state you didn't create.5657## Sharp edges58Footguns, timing/ordering caveats, nondeterminism noted while recording.59```6061## Running a card62631. **Build fresh from the code under test.** The single most common mistake is64 testing a stale binary. Rebuild every layer your change touches (server,65 client, embedded assets) and confirm the running instance is the new one,66 not a process someone left up yesterday.672. **Isolate.** Run in a hermetic workdir. If the app holds a host-level68 singleton (a lock, a fixed port, a shared state dir), point the test69 instance at its own copy — e.g. override `$HOME`/state-dir/port — so it can70 neither collide with nor pollute (nor be polluted by) a real instance.71 Symlink shared read-only inputs (creds, tokens); keep mutable state separate.723. **Drive the surface** (recipes below).734. **Assert against the authoritative record, not just the pixels.** The UI can74 lie or lag; the on-disk state / log / database is ground truth. Cross-check75 the rendered claim against it when an assertion is ambiguous.765. **Capture evidence** — a screenshot, the captured pane, the on-disk artifact.776. **Clean up** — shut down what you spawned, remove scratch dirs, leave78 pre-existing instances running and untouched.7980## Driving a web UI (browser)8182Use a Chrome/CDP browser tool. After authenticated navigation, drive the page83through `eval` against the app's own JS entry points rather than synthesizing84clicks where possible — it's more robust to layout change.8586- **Optimistic-vs-settled** assertions: fire the action but *don't await it*,87 take a synchronous DOM snapshot (the pending placeholder is there *now*),88 then await and snapshot again. Without the no-await capture you can't tell89 "rendered then reconciled" from "never rendered."90- Return a **plain string** from `eval` (join your findings with `\n`); some91 bridges stringify a returned object as `[object Object]`.92- Inspect internal state via the app's singleton (`window.<App>?.state`, etc.)93 when the DOM is ambiguous.9495## Driving a CLI / TUI (tmux)9697Each scenario gets its own named tmux session (cleanup needs a deterministic98name). Fix the size for deterministic capture; prefer the app's plain-text/inline99mode if it has one.100101```bash102tmux new-session -d -s <name> -x 200 -y 50 "<cmd> 2>/tmp/<name>-stderr.log"103tmux send-keys -t <name> -l "literal text" # -l = no key-name parsing (paths, slashes)104tmux send-keys -t <name> Enter105tmux capture-pane -t <name> -p # -p = plain text; add -e only for styling106```107108- Always `-l` for user-typed strings; without it `/foo/bar` parses as escapes.109- Poll `capture-pane` for a state string; grep the **glyph/word**, not the color.110- Redirect stderr to a file — panics and debug probes land there, not the pane.111112## Hard-won principles113114- **Falsification, always.** Every assertion states what failure looks like. A115 step that can't fail proves nothing. When watching for an outcome, make sure116 your check would fire on the failure path, not just the happy path.117- **Verify the *right* surface.** The same concept often exists at several118 layers (an internal capability vs. the REST projection of it; a model field119 vs. the rendered chip). Confirm your assertion reads the surface that actually120 carries the signal — a "missing" value is often present one layer over.121- **Present but not visible ≠ absent.** Scrollable bodies, virtualized lists,122 and auto-scroll-to-bottom routinely push a real element out of the capture123 window. Before concluding something didn't render, scroll/expand to where it124 should be. Confirm via a sibling read (a status command reading the same125 state) when the visual is hard to capture.126- **Executing the card tests the card.** Expect to find bugs in your own127 scenario — a wrong selector, a wrong layer, an assertion the UI can't show.128 Fix the card as you go; a card that "passes" because its check was vacuous is129 worse than none.130- **Over-specification trap.** A card can describe a path that production gating131 prevents (e.g. a keybind that's a no-op in the current mode). Confirm the gate132 in the source rather than fighting it through the UI; verify the underlying133 behavior with a unit test and note the gate in the card.134- **Cleanup is part of the test.** A half-shutdown fleet makes the next run's135 polling return false positives. Make teardown idempotent and scoped to what136 you created.137138## Finishing139140Report each assertion as pass/fail with the concrete observation (the rendered141text, the on-disk value), not "looks good." If a card fails, capture the142evidence and either fix the bug or file it; don't soften the verdict.