ADR-095: Three-tier viewer test pyramid + production-shaped fixtures¶
- Status: Accepted
- Date: 2026-05-16
- Authors: Marko Dragoljevic
- Related RFCs: RFC-086 (the origin of this decision)
- Related ADRs:
- ADR-094 (graph handoff orchestrator FSM — what the matrix tests)
- ADR-066 (Playwright for UI E2E)
- Related Documents:
- VIEWER_GRAPH_SPEC.md §"Matrix assertion layers"
web/gi-kg-viewer/e2e/HANDOFF_MATRIX.md(Tier 1 coverage contract)
Context & Problem Statement¶
The graph handoff matrix (41 rows, 52 tests) catches every contract regression in ~30 seconds. But real-corpus validation against a 333-node graph surfaced three production bugs that the matrix mocks cannot reach:
- V5: hot-state Library→Library handoff hits the 15s stuck-timeout in production. Passes in mocks because the 1-episode fixture's layout finishes in <100ms before the second handoff fires.
- V2: Digest topic pill sets
subject.kind=topicbut no cy node auto-selects in a 333-node graph (the resolver doesn't find a matching prefix variant); rail looks right, graph doesn't. - V3: Search "Show on graph" can't be exercised without a real vector
index — the mock's contrived
doc_typedoesn't match the production filter set.
The shared root cause: the mock fixture collapses scale and timing to zero. A 1-episode, 1-cluster fixture with sub-100ms layout cannot exercise the supersession path that breaks at 200+ nodes with 1–3s layout time. The matrix is necessary but not sufficient.
Decision¶
Adopt a three-tier test pyramid for the viewer:
| Tier | Fixture | Speed | When it runs | What it catches |
|---|---|---|---|---|
| 1 — Fast matrix | Mocked (1 ep, 1 cluster) | ~30 s | Every PR | Contract regressions, FSM transitions, envelope shape |
| 2 — Production-shaped | Checked-in fixture (25 eps, 5 feeds, 5 clusters, GI+KG, 200–400 cy nodes) | ~3–5 min | make ci-ui-full |
Scale + timing-sensitive bugs, KG second-wave, compound mount ordering |
| 3 — Real-backend validation | Operator-supplied corpus + make serve |
~5–10 min | make ci-ui-validation + scheduled cron |
Drift between Tier-2 fixture and reality |
All three tiers share the same assertHandoffApplied helper and the
same 6-point user-visible contract (selection + camera zoom + camera
pan-center + subject store + invariant + no console errors). Tier 2
mirrors Tier 1's 41 rows row-for-row.
Institutional rule (locked): every bug surfaced by Tier 3 (real-corpus validation) lands a Tier 2 matrix row that reproduces it before the fix PR merges. Bugs become structurally non-regressable.
Rationale¶
- Tier separation preserves the fast loop. Bumping the Tier 1 fixture to 25 episodes would slow PR validation from 30 s to ~3 min. The fast tier MUST stay fast; the slow tier handles scale.
- Shared assertion helper means shared contract evolution. When the 6-point contract grows (e.g. adding L7 selection-dim), it lands once and all three tiers benefit. No drift between layers.
- Production-shaped fixture is reproducible. A deterministic
scripts/build_production_shaped_fixture.pyregenerates the fixture from a real corpus snapshot. PR diffs stay small when behaviour evolves. - Tier 3 catches what Tier 2 can't. A checked-in fixture is a point-in-time snapshot; real corpus behaviour drifts as the pipeline evolves (new topic-cluster shapes, new vector index, new metadata fields). Tier 3 keeps Tier 2's fixture honest.
- The institutional rule makes regression impossible. Without it, Tier 3 bugs would surface, get fixed, then re-surface 3 months later because no one wrote the test. With it, every bug is encoded as a permanent regression barrier.
Alternatives Considered¶
-
Bump Tier 1 fixture size to 25 episodes. Rejected — slows PR validation by 6×; existing spec files would all need to update for new cy ids; the fast loop is the wrong place to spend that latency.
-
Snapshot-test the matrix against
make serve. Rejected — ties CI to backend bootstrap (~10s warmup) and stateful corpus loading. The fast tier MUST run without out-of-process dependency. -
Property-based testing of FSM transitions. Rejected for now — 75 unit tests already cover the transition table at the right level; what we're missing is scale and timing, not transition coverage.
-
Visual regression with screenshot diffs. Rejected for now — brittle to font / theme / animation changes. The camera-pan center assertion (L3) already catches "user-visible wrong-place" without the maintenance burden.
-
Stop at Tier 1 + manual checklist for Tier 3. Rejected — the manual checklist is what we have today; it didn't catch V5 until accidentally running validation. The institutional rule is only enforceable when Tier 2 exists.
Consequences¶
- Positive:
- V5-class scale-sensitive bugs surface in CI, not in production.
- The fast loop stays fast (~30 s); deeper coverage opt-in via
ci-ui-full. - Every Tier-3 bug becomes a permanent Tier-2 regression barrier.
- Future entry surfaces inherit Tier 1 + Tier 2 coverage by recipe; the migration recipe in VIEWER_GRAPH_SPEC.md grows a Tier-2 step.
-
Real-backend validation becomes a documented release gate.
-
Negative:
- +5 days initial investment to author the fixture, mirror the matrix, fix the bugs Tier 2 will surface.
- Fixture maintenance: corpus schema changes require regeneration via the build script. Mitigation: deterministic + idempotent script.
-
Three places assert the same contract. Mitigation: shared
assertHandoffAppliedhelper across all tiers; no duplication of assertion logic. -
Neutral:
make ci-ui-fullgets ~3 minutes slower. Matches operator expectations for substantial-refactor gates.- PR template grows three checkboxes.
Implementation Notes¶
- Fixture:
web/gi-kg-viewer/e2e/fixtures/production-shaped/(checked in, <5 MB total). - Build script:
scripts/build_production_shaped_fixture.py(deterministic; regenerable from a real corpus snapshot). - Tier 2 specs:
web/gi-kg-viewer/e2e/handoff-production/*.spec.tsmirror Tier 1's 8 spec files row-for-row. - Tier 2 mock helper:
setupProductionShapedMocks(page)inweb/gi-kg-viewer/e2e/handoff-production/_helpers.ts. - Tier 3 spec:
web/gi-kg-viewer/e2e/validation/real-corpus.spec.ts(already exists; promoted from session artefact to permanent gate). - Tier 3 config:
web/gi-kg-viewer/playwright.validation.config.ts(already exists). - Make targets:
make ci-ui-fast— unchanged (Tier 1 only)make ci-ui-full— adds Tier 2make ci-ui-validation CORPUS=/path/to/corpus— Tier 3- PR template: three checkboxes for the three tiers.
- Pattern: Test pyramid (Mike Cohn) adapted for UI-heavy applications, with scale + timing as the discriminating dimension between Tier 1 and Tier 2.
References¶
- RFC-086: Three-tier viewer test pyramid + production-shaped fixtures
- ADR-094: Graph handoff orchestrator FSM
- ADR-066: Playwright for UI E2E testing
web/gi-kg-viewer/e2e/HANDOFF_MATRIX.md— Tier 1 contract- VIEWER_GRAPH_SPEC.md — operational reference