---
name: testing
argument-hint: [run <test-name>]
description: "The test-lifecycle skill — four modes. Plan-gate mode: after `boundary` passes, derives the tests a plan owes and writes the Test Obligations ledger into the plan (a row per behavior: layer, target file, oracle, fixtures); writes no test files. Placement mode: layered test strategy — which ownership strata the application has, where a test lives and what it may assert (on a rendered surface every user-observable promise lives in the surface's journey; component tests are residual); use for any testing-strategy, test-architecture, test-pyramid, or 'unit/integration/e2e?' question, even if 'layer' is never said. Gate mode: executes the verification gate — derives the behavioral checklist, drives the real surface (browser automation, the real CLI, HTTP; the CDP runner in `references/cdp-runner.md`, run as `/testing run <name>`), pins durable behaviors, discharges every ledger row. Run mode: executes a markdown UI test over Chrome DevTools Protocol on `/testing run <name>`, 'run my UI tests', 'run the browser tests'. Also the framework and suite conventions per stack. Triggers on 'test plan', 'testing strategy', 'test architecture', 'test layers', 'where should this test live', 'run the verification gate'."
---

# Testing — Ledger, Layers, Gate

One skill owns the three procedural moments of the test lifecycle. The standing constraints — the regimes, deferral, the gate's preconditions — are `.claude/rules/testing.md` and are never restated here. Pick the mode from the moment:

| Moment | Mode |
| --- | --- |
| The plan pass, after `boundary`'s amendments are folded | **Plan-gate** — derive what the plan owes |
| A strategy / placement / altitude question, or laying out a suite | **Placement** — derive the strata, place the tests |
| The build pass reaches its tail — the unit's code is written | **Gate** — drive, fix, pin, discharge |
| `/testing run <name>`, or a markdown UI test to run in the browser | **Run** — the CDP runner, [`references/cdp-runner.md`](references/cdp-runner.md) |

---

## Plan-gate mode — the Test Obligations ledger

You derive the tests an implementation plan owes and write them into the plan as a reviewable manifest — in the same pass that writes the plan, ordered second after `boundary`'s amendments are folded. A plan shown to the user already carries this ledger. **You never write a test file**: tests written before the code exists couple to an implementation that has not settled, and tests written inside the build loop tax every iteration. You write the ledger gate mode discharges, so the gate proves a checklist rather than an impression.

**The litmus is "which promise, at which altitude, proven by what?"** Each promise earns exactly one test at the cheapest layer that can prove it. Spend the reading on the *layer* and the *oracle*, not on prose.

### Contract sources

- **The plan's Proposed Changes** — each file, entity, or API change is a behavior that may owe a test.
- **The owning LLD** (`docs/features/{feature}.md`, via the ownership index) — every obligation names the LLD promise it proves. The LLD is referenced, never restated; the chain runs one way (LLD → ledger → tests and checklist), which is what keeps the artifacts from drifting.
- **[`references/frameworks.md`](references/frameworks.md)** — frameworks and canonical placement per stack. Target paths come from there, never invented; the repo's existing runner always wins.
- **`boundary`'s output** — a HIGH or CRITICAL finding is itself a behavior owed a regression test. Read the plan after boundary's amendments are folded.

### Layer assignment

Assign each behavior the **cheapest layer that can prove it**, then name the oracle. The ledger altitudes are `unit` / `component` / `integration` / `e2e`; § Bridge maps them to strata.

- **Integration is the default for server behavior** — a route or handler against a real in-memory datastore via the app instance. Server-side error paths, races, and idempotency belong there, with injected faults (network mock, fake timers, a forced constraint).
- **A journey is the default for a rendered surface.** Any promise a user could observe in a real browser — what a screen shows over real rows, what a click changes, where navigation lands and what the URL carries, what a refused or failed request shows — is owed at layer 8 as `extend journey <surface>`, **never** as a component test. The surface's own fault paths (a 500 on load, a 400 on submit) stay in the journey, injected at the app's own endpoint from the browser.
- **Below the browser, a rendered surface owes only** a pure derivation with real branches (layer 6), a regression pin for a state the journey world cannot reach (layer 7), the wire's error taxonomy (layer 5), and geometry that shipped wrong (7b) — § Placement discipline.

**The journey world is a precondition, not an excuse.** A rendered surface owes its journeys whether or not the repo's e2e config boots today. When it does not — no config, a world pointed at a deleted package, a job parked in CI — the plan's **first** row wires it (`e2e · <config> · the world builds, seeds, boots and one journey passes from a cold shell`), and every journey row names that row as a dependency. **Never emit `blocked` rows in place of journeys and route the promises down to component tests**: a broken world nothing obliges anyone to fix stays broken, and every UI promise lands in a simulated DOM for as long as it does.

Density is one spec file per surface and one `test()` per user journey, carrying as many assertions as the journey has observable steps — prefer **"extend journey `<surface>`"** over a new file.

### Run (plan-gate)

**Input:** a plan path (given, or the newest in `docs/history/plans/`), boundary-checked with amendments folded. **Skip on a boundary BLOCK.** For a non-plan diff the hunks stand in for Proposed Changes, but the obligations are paid **immediately** — only a plan has a ledger to defer into.

1. **Build the behavior list** from Proposed Changes, the LLD's promises, and HIGH/CRITICAL boundary findings. Mark compiler-guarded behaviors — they owe **no** test.
2. **Assign layer and oracle** per behavior; the cheapest proving layer wins.
3. **Map each to a target file** per `testing.md`; name fixtures, factories, and fault injection; use the repo-standard fake-data source.
4. **Route by surface**: a user-observable promise on a rendered surface → `e2e` (its surface's journey, with the world-wiring row first if the world does not boot); server behavior → `integration`; only what § Placement discipline keeps below the browser → `component` / `unit`.
5. **Emit the Test Obligations section into the plan** — an executor dispatched with only the plan sees nothing else — plus a one-line chat summary:

   ```
   ## Test Obligations

   | # | Behavior (LLD promise) | Layer | Target file | Oracle | Fixtures / faults | Outcome |
   | - | ---------------------- | ----- | ----------- | ------ | ----------------- | ------- |

   Coverage: {N} behaviors · {N} integration · {N} component · {N} unit · {N} e2e ({N} extend, {N} new)
   Completion checklist: docs/tests/{feature}.md  (derived at the gate, not now)
   ```

   State a behavior with no obligation too, with its reason — "none — compiler-guarded" reads as *checked*, an empty cell as *not analyzed*. A short manifest is valid; never pad it. **Leave `Outcome` empty — it is gate mode's cell**, and a plan cannot reach `Status: Complete` while any row's Outcome is empty.
6. **Emit the Model Obligations section when the plan touches a modelled layer** — a source path a `docs/modeling/{system}/bindings/*.md` names, the predicate the compliance hook computes. Read that system's `MODEL.md` (§ 8 promises, § 9 witnesses) and the binding for each touched layer; write one row per promise, witness group or scripted scenario the change can break, naming the binding and the pin that proves it at that layer. The model is regenerated first when the plan changes what it describes (`MODEL.md` first, never the spec against the code), so the rows carry the model's final names. Two rows are always owed: the scripted scenarios replayed as named tests, and the conformance replay of harvested traces through the system's `projection.py`:

   ```
   ## Model Obligations

   | # | Model element | Binding | Pin | Outcome |
   | - | ------------- | ------- | --- | ------- |
   ```

   The rows discharge at the gate with the test ledger's outcomes. A model row and a test row that prove the same thing are one test: the test row names the model row it pins.

---

## Placement mode — layered test strategy

Where a test lives and what it may assert: derive the strata from the application's real boundaries, then place each behavior by where its defect can originate. The procedure, the eight strata, the tree and the bridge to the ledger are [`references/placement.md`](references/placement.md) — read it in full whenever this mode runs.

---

## Gate mode — discharge the ledger

The tail of every build pass: the session opens it the moment the unit's code is written, without asking. Its constraints — preconditions, the inline verdict, tier, honest reporting — are `.claude/rules/testing.md` § The gate. **Drive the real thing first, then pin what must not regress** — driving finds the defects that matter to a user and shows which behaviors are worth a test's permanent cost.

**Standing order of the drive:**

- **On a rendered surface, browser driving LEADS the gate — it is not its final rung.** The order inverted deliberately: the defects that reach users through an interface are geometric and compositional, and a simulated DOM computes no layout, so a suite that is entirely green says nothing about whether anything is where it should be. **Reach the browser Chrome-CDP-first and Playwright second** — CDP drives a real browser interactively and needs no script written in advance, which is what discovery requires; Playwright is the fallback where CDP is unavailable and the tool of record for whatever must run unattended afterwards. Cheap oracles run in the same pass and are what the drive's findings get pinned into; they are no longer a gate to pass before looking. A surface with nothing rendered — a CLI, a service, a library — drives its own real binary or wire instead, and this clause does not apply.
- **A judged drive discovers; a scripted assertion guards.** Only the first finds a defect nobody has seen — a script asserting a control's value passes over a control drawn on top of its own label. Scripts are written after a judged drive names the class of defect, never generated ahead of one, and **a red script is never regenerated** — regeneration re-derives the assertion from the surface as it currently is, which turns a defect into the asserted truth with everything green.

1. **Write the checklist** — derive or update `docs/tests/{feature}.md` from the ledger and the LLD's promises (the `documentation` skill's `templates/behavioral-checklist.md`; one `**Assert**` per observable). **Derive it before driving** — before, it is the script; after, it is a report with nothing to trace tests back to. `docs/tests/{feature}.md` pairs with `docs/features/{feature}.md`; a checklist whose feature doc does not exist means one is owed — check here, when the checklist is named.

   **You maintain the checklist, not just its first draft.** Nothing else watches it: it owns no code and no `owns:` glob implicates it, so a surface can be rewritten under it and every hook stays silent. Before driving, reconcile the file against the surface you are about to drive:
   - **Retire** sections whose behavior no longer exists — delete them; a checklist is a truth document and its history lives in git.
   - **Add** sections for behavior shipped since the last run.
   - **Repair every `Pinned:` trace** — a trace naming a test that no longer exists reports coverage that cannot fail, which is worse than no trace. A section you cannot pin says `none yet` and names the test it owes.

   **Never soften an assertion to make the run green.** The checklist states what the feature promises, the run records what happened, and the backlog carries the difference — record the defect and send it to the backlog.
2. **Drive the real surface — on a rendered surface, visually and FIRST.** The defects that reach users through an interface — a slider thumb sitting on its own labels, a settings panel that grows the box someone types into — are invisible to a simulated DOM by construction; only judgement applied to real pixels catches them.
   - **Reach the browser Chrome-CDP-first, Playwright second.** CDP drives the user's own browser interactively and needs no script written in advance, which is what looking requires. Playwright is the fallback where CDP is unavailable (headless, CI, a runtime without it) and the tool of record for anything that must run unattended afterwards. Where the runtime offers neither, emit the browser-verification checklist for the user to run; their report is this step's verdict, and `Status: Complete` waits for it.
   - **Judgement leads, scripts follow — never the reverse.** A *judged* drive — an agent or a person reading the actual rendering against the design's intent — **finds** geometry and taste defects; a *scripted* assertion only **guards** a contract someone already discovered. A generated script asserting "the slider sets the value" passes over a thumb covering its labels.
   - **Run the cheap oracles in the same pass** — suites, the CLI binary, HTTP against the running app. They are what the drive's findings get pinned into (step 3), not a gate to pass before looking.
   - On a surface with no rendering — a CLI, a service, a library — drive its own real binary or real wire; the visual clause does not apply.
3. **Pin the durable behaviors — once, against the driven code** — as suite tests per `testing.md`, naming each test's stratum (§ Bridge).
   - **On a rendered surface the pin is the surface's journey first**: the drive just performed *is* the journey, so write it down as the test against the seeded world. Reach for a component test only where § Placement discipline's condition (c) holds.
   - **Write pins after the drive and the fixes, never beside the first attempt** — an earlier pin is rewritten on every iteration and proves only that its author and the code agree.
   - **Write the pins inline, as the agent that drove** — only it knows what the drive found and what each pin must therefore assert; a sub-agent briefed from the ledger row reproduces the row, not the finding.
   - **A pinned test must pass from a cold shell** — the canonical test command with no dev server hand-started and no seeded state beyond what the suite builds for itself; a server app is exercised in-process per `testing.md`'s stack table, **never** over a port. A test whose oracle is "fetch the server I happen to have running" is a driven check wearing a test's name, and goes red the moment the session ends.
   - **A suite whose subject is no longer reachable from the product's entry points is deleted with the code path**, never repointed to keep it green.
   - **A case that asserts nothing beyond "it did not throw" is not a pin** — give it an assertion on resulting state or delete it.

   A test is a permanent maintenance cost, bought deliberately; a driven check is evidence that proves the change works today and leaves nothing behind:

   | Earns a suite test | Verified by driving alone |
   | --- | --- |
   | A promise an LLD makes about the feature's contract | Copy and spacing that no defect has ever turned on — restated by a test, not protected by one |
   | A bug that occurred once — its regression test | A one-off migration or backfill's output |
   | **A layout defect that shipped** — its geometry pin, at 7b, measured in a real engine | Layout a judged drive has looked at and found correct — looking is the proof, and a pin buys nothing until something breaks |
   | An error path, race, or idempotency guarantee | Behavior a type or the compiler already makes unshippable |
   | A user-observable step of a surface — as an assertion in its journey | A same-branch table row: an `it.each` earns one row per branch or boundary, never one per literal |
4. **Run the affected suite and the affected surface's journeys from a cold shell** before discharging — a suite that needs the session's environment discharges nothing. The e2e config builds, seeds and boots its own world, so "needs the world running" is never a reason a journey did not run.
5. **Discharge every ledger row into exactly one outcome**: **satisfied** (test green, or driven check passed), **owes nothing** (with the reason — compiler-guarded, covered elsewhere), or **blocked** (missing infrastructure named, row converted to a backlog item).
   - **Re-read the ledger from the plan file on disk, never from conversation memory** — a context-held copy is how rows get missed.
   - **Write each row's outcome into its `Outcome` cell in the plan file** — exactly `satisfied`, `owes nothing — {reason}`, or `blocked → backlog`. The Stop wall and CI read that cell, and a row left unexamined blocks completion.
6. **Report results honestly, including failures.** Record which test now guards each checklist section (`Pinned:` traces) and each section's `AC:` line where the repo carries a keyed acceptance-criteria source, closing with the coverage count — `AC coverage: {n}/{m}; uncovered: {keys} → backlog`.

   **Record every section you drove in the changelog entry, not in the checklist** — one line per section: what was driven, PASS or FAIL, the environment, and the evidence the assertion turned on (the literal observed, the row version, the request body). A drive is a dated event and the checklist is a truth document that carries no dates (the `documentation` skill § The three natures). **A `FAIL` is recorded as a `FAIL` and stays**, with the backlog item it raised — a record of drives that never shows a failure cannot be told apart from one written by someone who already knew the answer. In the checklist itself, the `Pinned:` trace distinguishes a verified promise from a typed one; a section the drive could not reach says `not reachable — {what blocks it}` there, so an unexamined section and a guarded one never look alike.

   Freezing the plan and finishing the changelog entry are the root policy file's Completion steps, not this skill's.
