# Testing & Verification

**When** work is proven and **which technology** proves it — the standing constraints that bind on every turn. The full lifecycle procedure — deriving a plan's Test Obligations, placing tests in strata, executing the verification gate — is the `testing` skill, invoked at those moments; this rule never restates it.

## Two regimes

**Deferral is a property of plans, not of code.** A plan carries a Test Obligations ledger and a gate that discharges it; nothing else does, so nothing else may defer.

| Regime | During the work | Proven |
| --- | --- | --- |
| **Plan-driven build** | No new test files. Code only — the loop stays fast | At the verification gate, before `Status: Complete` |
| **Bug fix** — a defect in shipped behavior | Fix it, drive it, then write its regression test — in the same unit of work | Before the unit ends — the pin is written once, against the fixed code |
| **Ad-hoc / non-plan change** | Make the change, drive it, then write its test **when it alters promised behavior** — layout, copy, and config tweaks owe none | Before the unit ends, same discipline as a bug fix |

- **A bug fix cannot defer past its own unit of work — but its pin comes after the fix is driven, never beside the first attempt.** A test written while the fix is still moving is rewritten on every iteration, and written by the fix's author beside the fix it proves only that the two agree. Write it once, against the fixed code; where the defect was subtle, run it once against the pre-fix code (stash the fix) so the pin is shown to bite — that is one command, not an iteration.
- **Ad-hoc work cannot defer — there is no ledger to defer into.** A non-plan change that alters no promised behavior — a pure refactor, a rename — owes no test.

## During the loop

Rules for the plan regime between the plan gate and the verification gate:

- **No new test files** — obligations wait in the plan's ledger (written at the plan gate by the `testing` skill).
- **Existing tests broken by the plan's intended behavior change are updated to the new contract before any mid-plan commit** — the shared branch never carries a red suite, and an update is never a weakening (§ Discipline).
- **A defect in the plan's own in-flux code is the loop iterating** — no test, no ledger row. **A pre-existing bug in shipped behavior is the bug-fix regime even mid-plan**: fix it now and pin it at the build pass's pin step, with the unit's other pins — or send it to the backlog via Out-of-Scope Discovery. Never fix silently.
- **The ledger is amendable while the plan is open** — the gate discharges whatever it holds at gate time.
- **A mid-plan commit is a checkpoint, not a verification claim.**

## The gate

**The tail of every build pass, never a separate ask.** When the unit's code is written the gate runs — drive the real surface, fix what driving finds, pin, discharge — and the session does not stop to ask whether it should. What the user decides is the freeze and the commit: "ship it", "commit", "looks good" close a plan whose gate has already run; they never open one. Never flip `Status: Complete` without the gate. A plan with outstanding obligations stays open, whatever the code does.

- **Inside the build pass the order is fixed — code → drive → fix → pin.** Tests written while the code is still moving are rewritten on every iteration, and written by the code's author beside the code they pass by construction; a judged drive is what finds the defects (below). Pinning is the last step and happens once, against code the drive has already shown to be right. The procedure — checklist, drive-then-pin ladder, row discharge, what earns a durable test — is the `testing` skill (gate mode).

- **Precondition: the ledger exists.** A plan with no Test Obligations section is blocked at the gate — run the `testing` skill (plan-gate mode) on it first; an empty ledger passes vacuously and the gap surfaces only after the file is frozen.
- **The verdict is never delegated** (the root policy file § Delegation), and where browser work is main-session-only (its § Runtime Notes), the driving is too. **The pins are written inline, by the agent that drove the surface** — the drive is what tells it which behaviors earned a test and what each one must assert, and a sub-agent briefed from the ledger alone holds neither. Nothing in the gate is dispatched: the checklist, the driving, the pinning, the cold-shell suite run and every discharge stay with one agent, so the state of the gate is never somewhere nobody is looking.
- **The gate needs no stronger tier than the session default** — a standard-tier session suffices whatever tier built the plan; the plan and its ledger carry all the state the gate needs, so gating in a fresh session loses nothing.
- **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.
- **Report what happened, including failures.** A gate that reports green while a suite is red is worse than no gate; state the failing test and its output.
- **`cmd > log; echo "EXIT=$?"` reports the echo's status, never the command's** — a SIGTERM'd suite surfaces as "exit code 0", which is the worst direction for a gate to be wrong in. Read the real status from the log, or drop the trailing echo. Adjacent trap: a runner that never emits a summary is a dying **worker**, not a slow test — `--testTimeout` bounds neither, so bound the run and read the log.
- **Abandonment does not erase debt.** Before `Status: Abandoned` freezes a plan whose code was committed, convert its undischarged rows into backlog items; uncommitted code owes nothing.

## Frameworks by stack

Framework choice is **never** ad hoc and never the user's problem mid-task: detect the stack, use its row; if the repo already has a runner, that runner wins — **never** introduce a second framework beside it.

| Stack | Unit / integration | Component | Rendered (7b) | Judged drive | E2E | HTTP mocking | Don't use |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Node + Vite / React (web) | `vitest` | `@testing-library/react` (+ `@testing-library/user-event`, `happy-dom`) | `vitest` browser mode, Playwright provider | Chrome CDP, else `@playwright/test` headed | `@playwright/test` | `msw` | `jest`, `mocha`, `jasmine`, `cypress`, `enzyme` |
| Express APIs | `vitest` | — | — (nothing rendered) | the real wire — `curl`/HTTP against the running app | `supertest` against the app instance | `msw` (outbound calls) | `jest`, `chai-http` |
| React Native / Expo | `jest` via the `jest-expo` preset | `@testing-library/react-native` | `maestro` on a device/simulator | the simulator, watched | `maestro` | `msw` | `detox`, `enzyme` |
| Cloudflare Workers | `vitest` + `@cloudflare/vitest-pool-workers` | — | — (nothing rendered) | the real wire against `wrangler dev` | `@playwright/test` against `wrangler dev` | `msw` | `jest` |
| Rust | `cargo test` (built-in) | — | the app window, driven by the OS layer where one exists | the running binary, watched | — | — | — |

**The two new columns are the inversion made concrete.** *Rendered (7b)* is the unattended real-engine runner — where geometry pins live, and `—` where a stack renders nothing. *Judged drive* is what a person or an agent looks at when a change is finished, and it leads the gate on any stack that renders: Chrome CDP first because it drives a real browser with no script written in advance, Playwright headed where CDP is unavailable. A stack whose judged drive is `—` has no interface to look at, not permission to skip looking.

Test packages install at `latest` — outside the approved-packages pin set. Fake data comes from `@ngneat/falso` — **never** `@faker-js/faker` / `chance`.

## Conventions

- **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses — named `<subject>.test.ts`; shared fixtures, factories, and helpers in `tests/helpers/` (or the suite's existing shared home).
- **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
- **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob. The e2e config **owns its world** — it builds, seeds, and boots the app itself (a `webServer` block or equivalent), so a journey passes from a cold shell like any other test and "the world was not running" is never why one did not run.
- **The real-renderer stratum is a third tree** — `tests/rendered/` under the unit runner's *browser mode*, its own config, kept to geometry regressions that shipped and the composition assertions a component library cannot make alone. It is not a second component suite; anything a simulated DOM can prove stays in `tests/component/`.
- **No test doubles for what you own.** Prefer real instances — in-memory SQLite for drizzle, the actual Express app via `supertest` — over mocks; mock **only** true externals (network via `msw`, time via the runner's fake timers).
- **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone, in any order, **and from a cold shell** — never against a hand-started dev server (Workers: `@cloudflare/vitest-pool-workers`; Express: `supertest` against the app instance).

## Discipline

- **Never weaken, skip, or delete a test to mask an unexplained failure.** When a plan or LLD intentionally changes behavior, updating the affected tests to the new contract is part of that change.
- **A behavior that is compiler-guarded owes no test** — if it cannot ship green-but-broken, a test adds cost and no safety.
- **Throwaway checks are not tests.** Temporary scripts written to drive a change live in scratch space and are deleted — never committed into the test tree.
