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).
202 lines
8.5 KiB
Markdown
202 lines
8.5 KiB
Markdown
# 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 1–3 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 1–3 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. |