# Placement mode — layered test strategy

You decide **where a test lives and what it may assert**. **The litmus is "where can this defect originate?"** A test lives with the only layer that can produce the defect it catches. "The 409 response carries the current body" can only break in a route handler — service-contract layer. "The buffer the user typed survives the 409" can only break in component state — component layer. Same feature, two owners, two tests, zero overlap. **Placement is decided by this question alone** — testing a feature wherever you first *see* it is what makes suites flat, duplicative, and multiplicative in cost.

## The stack

The maximum form, for a full-stack application. Each layer names what only it can break, the oracle that proves it, and the technology **class** — the concrete framework comes from [`frameworks.md`](frameworks.md)'s stack table, never from here. 7b is lettered because it is layer 7's other half, not a rung above it: same subject, a real-engine oracle. A stack without a rendered surface has no 7b.

| # | Layer | Owns (defects only it can originate) | Oracle | Technology class |
| --- | --- | --- | --- | --- |
| 1 | **Persistence schema** | Invariants the storage engine enforces against any code: uniqueness, checks, cascades, append-only triggers | The engine's refusal of a forbidden write | The real engine, in-memory — never a mock of storage you own |
| 2 | **Domain** | Business rules and state transitions, via direct calls | Resulting state in real storage | Real datastore + modeled fakes for external ports |
| 3 | **Service contract** | The published wire: authn/authz at the door, envelope shape, status mapping, field projection, stream/event grammar. For a CLI this is argv/stdout/exit codes; for a library, its exported API | Requests against the real app instance, in-process | `supertest` / `SELF.fetch` class — never a spawned server, never naked handler functions |
| 4 | **Contract artifact** | Nothing behavioral. Typed fixtures / interception handlers **compiled from the provider's own types**, exported for every consumer-side test | The typechecker — drift from the provider is a compile error | No runner; a module the provider owns |
| 5 | **Client data** | The consumer's half of the wire: error taxonomy, parsing, byte fidelity, stream reassembly, retry/abort | Intercepted network serving layer-4 artifacts; no DOM | `msw`-class interception |
| 6 | **View logic** | Pure presentation derivations: grouping, ordering, status vocabularies, formatting | Table-driven equality | Plain runner |
| 7 | **Component / surface** | **Residual on any stack that has layer 8.** Regression pins for what the journey world cannot reach: a state only a fixture the world lacks can produce, a flow behind an external system (a GitHub installation, an AI reply), an orchestration detail no user step exposes (a debounce, a buffer surviving an abort) | Rendered output a user can observe; network answered by layer 4 | DOM testing-library class + interception — never the default home of a UI promise |
| 7b | **Rendered composition** | Defects only a real rendering engine produces: geometry (overlap, clipping, a container that grows when a panel opens), the theme cascade where your tokens meet a component library's, focus order and accessible names through a third-party primitive | Measurement in a real browser engine — `getBoundingClientRect`, computed style, the accessibility tree | Real-renderer component runner (the unit runner's browser mode), **never** a simulated DOM |
| 8 | **Journeys** | Everything a user can observe on a rendered surface: what a screen shows over real rows, what an action changes, where navigation lands and what the URL carries, what a refused or failed request shows, a create-then-delete round trip — and composition: the build serves, auth flows end to end | A real browser (or the real binary/wire, for a stack that renders nothing) against the really-running app over a seeded world; faults injected at the app's own endpoints | Browser/process driver class; **one spec file per surface**, one test per user journey, variants wherever a user can see them |

**Layer 4 is the hinge.** Every layer ≥5 assumes layers 1–3 are green, and the artifact makes that assumption enforceable: when the provider's response type changes, the fixture stops compiling and every consumer test fails at typecheck. **A consumer-side test never hand-writes a wire response** — that smuggles unverified layer-3 knowledge upward; route it through the artifact.

## How many layers — derived, never chosen

**A layer exists iff the application has the boundary that can originate that layer's defects; an absent boundary makes the layer vanish — it never merges into a neighbor.** Merging layers 3 and 7 to "simplify" reinvents the flat feature test: every UI assertion silently re-verifies the API beneath it, and cost goes multiplicative.

| Application shape | Strata present |
| --- | --- |
| Full-stack web app | all eight — 8 is the home of every UI promise, 7 is residual |
| Backend-only service | 1, 2, 3, 8 |
| CLI | 2, 3 (the CLI's argv/stdout contract), 8 |
| Frontend on a third-party API | 4 (derived from the API's published spec), 5, 6, 7, 8 |
| Library | 2 only — its exported API is the contract, its tests are its consumers |

Derive the stack before placing a test: a placement argument that starts "this feels like an integration test" has skipped the derivation.

## Placement discipline

- **A UI test sits below a real browser only when all three conditions hold.** *(a)* Its assertion names a **product promise**, not a construction detail — would it survive a total visual redesign that changes no behavior? `not.toHaveProperty("taskMode")` survives; `classList.contains("w-24")` does not. *(b)* The defect it catches can **originate below the layout engine** — a wrong wire path, a dropped field, a state-machine bug, a wrong string. *(c)* **The surface's journey cannot reach it** — the state needs a fixture the seeded world lacks, an external system, or an orchestration detail no user step exposes. Fail (a) or (b) and the home is 7b; fail (c) and the home is the journey, as one more assertion.
- **Never assert geometry in a runner without a layout engine.** A simulated DOM computes no layout — `getBoundingClientRect()` returns zeros — so the assertion comes out as a class-name proxy that fails on a benign restyle and passes on `w-[10000px]`. There is no valid proxy: when the only available runner simulates the DOM, drive and pin the behavior at 7b, never approximate it.
- **An assertion provable at two layers is misplaced — it belongs at the lower one.** The duplication check is mechanical, not judgment.
- **Behavior variants sink to the cheapest layer that owns them** — a derivation's variants to 6, a rule's to 2. A variant a user can *see* (a second sort order, a filtered roster, a refused form) is one more assertion in the journey that shows it, not a component case. A variant at layer 3 pays wire tax on a domain test.
- **One representative path upward for the server; the whole surface for the browser.** Layer 3 keeps one happy path per route plus exhaustive coverage of *contract edges only*. Layer 8 keeps one spec per surface covering everything a user can reach in the seeded world — the surface sets the count, **never** a cap. A cap on journeys pushes UI promises into a simulated DOM, which talks to a hand-written fake of the wire, so a route rename or an envelope change passes it green.
- **7b stays small by construction.** It holds geometry regressions that actually shipped and the few composition assertions a component library's contract cannot make alone (the theme cascade, the accessible name through its primitive). A behavior a simulated DOM can prove does not pay a browser's price.
- **External systems enter as modeled fakes, not spies.** A fake implements the port's semantics so tests assert resulting state; a spy proves only that methods were called, and passes just as happily with the bytes swapped.
- **Protect failure localization.** One defect turns exactly one layer red, at its owner. When a single change reddens several layers, at most one of those tests was correctly placed — treat the rest as findings.

## The tree

| Path | Stratum | Runner |
| --- | --- | --- |
| `tests/schema/` | L1 | infrastructure-bound runner (real engine) |
| `tests/domain/` | L2 | infrastructure-bound runner (real storage, modeled ports) |
| `tests/routes/` | L3 | infrastructure-bound runner (in-process app) |
| `tests/contract/` | L4 | no runner; typed fixtures + interception handlers |
| `tests/client/` | L5 | plain unit runner |
| `tests/view/` | L6 | plain unit runner |
| `tests/component/` | L7 | unit runner + DOM environment (per-file opt-in); residual where `e2e/` exists |
| `tests/rendered/` | L7b | the same runner in browser mode; a real engine, kept small |
| `tests/helpers/` | none | shared seeds, fakes, and builders |
| `e2e/` | L8 | one spec per surface over a seeded world; its own config, never mixed into the unit runner's glob |

- **A stratum the app lacks gets no directory** — an empty `component/` in a backend service misstates the app's shape.
- **One shared glob list, imported by both runner configs** — two hand-maintained lists drift.
- **The concrete runner, framework, and DOM environment come from [`frameworks.md`](frameworks.md)'s per-stack table** — the doctrine names the slot, never the brand.
- **Contract modules the provider binds** (the layer-4 source types) live in the app's shared source zone, importable by both sides; `tests/contract/` holds only the compiled fixtures and fakes.

## Bridge to the obligation ledger

The ledger's altitudes name where a test *runs*; the strata say what it may *assert*:

| Ledger altitude | Strata it covers |
| --- | --- |
| unit | 2 (pure domain), 5, 6 |
| integration | 1, 2 (stateful), 3 |
| component | 7, 7b |
| e2e | 8 |

**When writing or discharging a ledger row, name the stratum, not just the altitude** — "integration" alone leaves open whether the row asserts a schema invariant, a business rule, or an envelope: three owners, three oracles.

## Run (placement)

1. **Derive the stack** from the application's real boundaries — storage, a published wire, a separate client, a DOM.
2. **Place each behavior** by the litmus; name its stratum, oracle, and technology class; take the concrete framework and file placement from [`frameworks.md`](frameworks.md).
3. **Flag violations, don't just place** — assertions provable lower, derivation variants above 6, hand-written wire fixtures above layer 4, spies where fakes should model state, and UI promises pinned at 7 that the surface's journey can reach.
4. **Report a stratum map** — one line per stratum: present or absent (and why), what lives there, what is misplaced and where it moves. For a plan, feed the strata into the ledger's rows.
