Files
rdbms-playground/docs/adr/0008-testing-approach.md
T
claude@clouddev1 88204f25c5 docs(tt4,nfr): record Tier-4 + NFR verification (ADR-0008 A1, ADR-0057)
Document the regression-hardening work from this session:

- ADR-0008 Amendment 1 — Tier 4 realized: the portable-pty + vt100 harness,
  expectrl dropped, serial/fail-fast design, the four flows, sidebar-region
  reads, command pacing (issue #39), CI default-target (advances TT5).
- ADR-0057 (new) — NFR verification strategy: contrast + ΔE2000 gates
  (NFR-5/7), startup/RSS measured (NFR-1/3: release ~29 ms / ~10 MB),
  NFR-2 by architecture, NFR-4/6 reviewer note. Records the two shipped
  light-theme contrast defects this work caught and fixed.
- requirements.md: TT4 [~]->[x], TT5 [/] (only a Windows exec runner left),
  NFR-1/2/3/5/7 ->[x], NFR-4/6 ->[/], each with evidence.
- CHANGELOG.md (new): Keep a Changelog + SemVer, [Unreleased] + [0.2.0].
- README ADR index updated; handoff-76; the working plan.

Docs-only (CI gate skips via paths-ignore).
2026-06-22 13:15:45 +00:00

202 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0008: Testing approach
## Status
Accepted
## Context
The project's working standards (`CLAUDE.md`, plus the user's
global testing rules) require:
- Test coverage established before changes.
- Bugs reproduced with failing tests before fixes.
- Integration tests that exercise the full stack a user touches.
- "All green, zero skips" as the only acceptable end state.
A TUI application needs a credible testing story for those rules
to be enforceable. Naïvely, "TUI testing" sounds like an
afterthought of manual screenshots. In practice, the Rust +
Ratatui ecosystem supports automation across every level that
matters, and we want to commit to that explicitly rather than
discover it ad-hoc.
## Decision
Testing is structured in four tiers. Tiers 13 run on every
commit in CI; tier 4 runs on every commit for a focused subset of
critical flows and on a nightly schedule for broader coverage.
### Tier 1 — Pure-logic unit tests
Standard `cargo test` against modules with no terminal
dependency:
- DSL parser and command dispatcher
- Type mapping (user-facing → SQLite STRICT types)
- Project I/O (`project.yaml` + `data/<table>.csv` round-tripping)
- Database rebuild from authoritative text sources
- Snapshot ring buffer logic (ADR-0006)
- Replay log writer/reader
These are the bulk of the test count. Every behavioural unit has
unit tests; modules with non-trivial logic also have property
tests via `proptest` where the input space justifies it (parser
inputs, type coercion, CSV escaping, etc.).
### Tier 2 — Render assertions via Ratatui `TestBackend`
Ratatui's in-tree `TestBackend` renders into a `Buffer` (a 2D
cell grid). Tests build an app state, render a frame, and assert
on the resulting buffer.
- **Cell-level assertions** for narrow tests
("the status bar shows mode label `Simple` in the expected
style").
- **Snapshot tests** via `insta` for whole-frame coverage of
representative views (default screen, query result table,
schema view, undo confirmation prompt). Snapshots are checked
in and reviewed on diff.
Snapshot discipline:
- Snapshots cover stable, intentional UI surfaces; they are not
added reflexively to every component.
- A snapshot diff is treated as a real review item, not a
rubber-stamp. Reviewers must confirm the change is intended.
### Tier 3 — Synthetic event-loop integration tests
The application's update function consumes
`crossterm::event::Event` values. Tests feed sequences of
synthetic events (`KeyEvent`, `MouseEvent`, resize) to the update
function, then render via `TestBackend` and assert on both
state and buffer.
This tier is the equivalent of `react-testing-library` for our
TUI: it exercises the full input → state → render path without a
real terminal, and is where the most valuable behavioural tests
live. Examples:
- Typing a DSL command in simple mode, submitting with
Ctrl-Enter, asserting the table list updates and the schema
view re-renders.
- Triggering `undo`, asserting the confirmation prompt appears
with the expected timestamp and change summary, confirming,
asserting state restoration.
- Switching modes and verifying the prompt label and border
colour change.
### Tier 4 — PTY-based black-box end-to-end
A small number of critical flows are exercised against the
**actual built binary** in a pseudo-terminal:
- Tooling: `portable-pty` for the PTY, `expectrl` for
expect-style scripting, `vt100` to parse the terminal output
stream into an inspectable cell grid.
- These tests catch issues the lower tiers miss: TTY setup,
signal handling, terminal mode transitions, real I/O timing.
Tier 4 is **reserved for the highest-value flows**, not blanket
coverage. The initial scope is:
- Cold launch → first DDL command → graceful quit.
- Project save → process restart → reopen → identical state.
- Project export → import in a fresh project → rebuilt database
matches the source.
- `undo` immediately after a `DROP TABLE`, including the
confirmation prompt.
Tier 4 tests run in CI on every commit (the focused list above)
and on a nightly schedule for any extended coverage.
## Tooling commitments
- `cargo test` — Tier 1, Tier 2, Tier 3.
- `proptest` — property-based testing for parser and conversion
layers.
- Ratatui's `TestBackend` — frame rendering for tests.
- `insta` — snapshot testing of rendered buffers.
- `portable-pty`, `expectrl`, `vt100` — Tier 4 PTY-based tests.
- CI matrix covers Linux, macOS, and Windows on stable Rust.
## Honest limits
- **No cross-terminal-emulator regression coverage.** Tier 4
exercises a PTY but not real terminal emulators (xterm,
Alacritty, Windows Terminal, etc.). Crossterm abstracts these
well in practice; if a real-emulator regression ever surfaces,
we will revisit.
- **No visual aesthetic checks.** Tests assert cell contents and
styles, not "this layout is pretty". Visual polish is reviewed
manually by humans.
- **Snapshot brittleness** is a known failure mode. We mitigate
by being selective about what gets a snapshot and by treating
snapshot diffs as real review items.
## Consequences
- Test discipline from `CLAUDE.md` is enforceable on every
layer: parser bugs caught at Tier 1, UI flow bugs caught at
Tier 3, real-binary regressions caught at Tier 4.
- Module boundaries are designed for testability from the start
(pure logic modules separate from rendering, an explicit update
function consuming events).
- CI cost is real but bounded: Tiers 13 are fast; Tier 4 is the
only slow tier and is kept narrow.
- Adding a feature implies adding tests at the appropriate tier
(or tiers); coverage is not retrofitted later.
## Amendment 1 — 2026-06-22: Tier 4 realized (TT4)
Tier 4 was a specification only (no PTY deps, no tests) until this
amendment. It is now implemented in `tests/e2e_pty.rs`, and the realized
shape refines the original decision in three ways.
**Tooling — `expectrl` dropped.** The decision named
`portable-pty` + `expectrl` + `vt100`. We kept **`portable-pty`** (0.9, to
spawn the real binary in a PTY at a fixed size) and **`vt100`** (0.16, to
parse the output stream into an inspectable cell grid), both current and
maintained. We **dropped `expectrl`**: it bundles its own PTY abstraction
(which conflicts with `portable-pty`) and matches line-by-line, a poor fit
for a full-screen TUI. A small hand-rolled `wait_for(predicate, timeout)`
that polls the vt100 screen replaces it — fewer dependencies, and assertions
read the actual rendered grid.
**Harness shape.** Each test spawns the binary
(`env!("CARGO_BIN_EXE_rdbms-playground")`) under a fresh PTY at **100×30**
(the wide three-region layout, ADR-0046), with its **own temp `--data-dir`**
so it never touches real projects or resume state, and `--theme dark` for
deterministic styling. Tests run **serially** (one PTY + child at a time)
via a poison-tolerant global lock, so timing stays predictable on the
low-parallelism self-hosted runner. Waits are **tight and fail-fast** (3 s):
a slow wait is a genuine hang, and a timeout panics with a full screen dump
for debuggability.
Two non-obvious robustness rules the harness encodes:
- **Read table presence from the Tables *sidebar* region, not the whole
screen** — the Output panel echoes every command, so a table name typed
in a command would pollute a whole-screen match.
- **Pace each command to completion before the next** (real-user / cast
cadence). A command submitted while the previous one's worker rebuild is
still in flight can be misread against a stale schema cache (issue #39,
discovered here — interactive use is unaffected).
**The four flows** mirror this ADR's Tier-4 scope: (1) cold launch → first
DDL → graceful quit (Ctrl-C); (2) create → quit → reopen via `--resume`,
asserting the *column* (not just the table name) round-trips; (3) export →
import into a fresh project → schema **and** data rebuilt; (4) `undo` after
`DROP TABLE`, through the Y/N confirm modal.
**CI.** The target runs by default in `cargo test`, so the existing Linux
gate exercises it on every push — this **advances TT5** (Tier 4 now runs in
CI on Linux). PTY-in-container was validated (openpty + child spawn work
under the Docker runtime the Gitea runner uses); the first real CI run is
the final confirmation. **Windows execution remains out of scope** (no
Windows runner), and the original Tier-4 note about a *nightly broader
coverage* schedule is **not** implemented — the focused four flows are the
committed TT4 scope; broader coverage is deferred.
**Piggybacked NFR measurement.** The same harness measures NFR-1 (startup
to first frame) and NFR-3 (idle RSS, Linux) — see ADR-0057.