# Plan — TT4 (Tier-4 PTY tests) + NFR verification + CHANGELOG Status: **approved 2026-06-22**, implementation pending. Supersedes the transient harness plan-mode copy. This is the persistent, version-controlled plan per project convention (`docs/plans/`). ## Context RDBMS Playground now ships public binaries (v0.2.0 on crates.io, plus Scoop/Homebrew/winget). With real users pulling updates, the priority is **regression protection before the next release**. Three tracked gaps are taken up together: - **TT4** (`requirements.md`, ADR-0008 Tier 4): the four critical end-to-end flows are specified but **no PTY harness exists** — no deps, no tests. Tiers 1–3 never exercise the *real built binary* through a terminal. - **NFR-1..7**: quality bars (startup, latency, memory, contrast, distinctive design, parity) that were **never formally verified** despite shipping. - **CHANGELOG**: none exists; useful now that releases are public (and lets the CI `winget` job pass `--release-notes-url` later). Baseline (Phase 1): `cargo test` = **2509 passed / 0 failed / 1 ignored**; clippy + fmt clean. This is the number Phase 5 verifies against (new tests add to it; nothing may regress). User decisions (Phase 3): - **TT4 in CI:** own `e2e_pty` test target, runs by default in `cargo test` (so the Linux gate exercises it every push). **Tight, fail-fast timeouts** — the runner is self-hosted, low-parallelism, low expected flakiness (so a failure surfaces fast rather than hanging on a generous timeout). - **NFR:** WCAG contrast = hard gated test; startup + idle-memory = measured via the PTY harness against *generous* bounds + numbers recorded; NFR-2/4/6 = verify-by-argument + reviewer note. Each requirement → `[x]`/`[/]` with evidence. - **CHANGELOG:** Keep a Changelog + SemVer; `[Unreleased]` + a single consolidated `[0.2.0]` only (0.1.0 treated as pre-history). --- ## Workstream 1 — TT4: Tier-4 PTY harness + 4 flows ### Tooling (refines ADR-0008's stated `portable-pty`+`expectrl`+`vt100`) - **`portable-pty`** (wezterm, actively maintained) — spawn the real binary in a PTY with a fixed window size. - **`vt100`** — parse the PTY output stream into an inspectable cell grid. - **Drop `expectrl`** — it bundles its *own* PTY abstraction (conflicts with portable-pty) and is line-oriented, a poor fit for a full-screen TUI. Replace it with a small hand-rolled `wait_for(predicate, timeout)` polling the vt100 screen. This deviation is recorded in the ADR-0008 amendment. - **Checkpoint at implementation:** verify `vt100` is still maintained/current (global currency rule). If stale, evaluate `avt` as the screen parser before committing the dep. `cargo add` picks latest; pin sensibly. ### Harness (`tests/e2e_pty.rs` — new `[[test]]` target) A `support` module providing: - `struct PtyApp` — opens a PTY (e.g. **100×30**, the wide three-region layout), spawns `env!("CARGO_BIN_EXE_rdbms-playground")` with a **per-test temp `--data-dir`** (`tempfile::TempDir`), `--theme dark`, `TERM=xterm-256color`; a reader thread feeds bytes into a `vt100::Parser`. - `send(bytes)` (raw; `\r` = Enter, `\x03` = Ctrl-C), `send_line(s)` = `s` + `\r`. - `wait_for(&str, Duration)` — poll the vt100 screen text until the substring appears or the (tight, ~**3 s**) timeout fires; on timeout **panic with a full screen dump** (fail-fast + debuggable). A `screen_text()` helper for assertions. - `quit()` — `send("\x03")`, wait for child exit, assert success. - Run **serially** (`serial_test` dep, or a shared `Mutex`/single-threaded target) — PTYs + temp dirs shouldn't race; also keeps timing stable. - **Instrument** with `eprintln!`/screen dumps on every wait so a CI failure is diagnosable from logs (per the project's logging discipline). ### The four flows (one `#[test]` each, mirroring ADR-0008 §Tier-4) 1. **Cold launch → DDL → quit.** Launch fresh; `wait_for("SIMPLE")` + `wait_for("(none yet)")`; `send_line("create table Customers with pk id(serial)")`; `wait_for("Customers")` in the Tables panel; `quit()`. 2. **Save → restart → reopen.** Launch project path P under the temp data-dir; create a table; `quit()` (autosave already persisted per-command); relaunch **same P**; `wait_for("Customers")` — schema restored from text/rebuild. 3. **Export → import → rebuild.** Project A: create table + insert a row; `send_line("export A.zip")` (explicit path under the temp dir); `wait_for` export-success note; `quit()`. Fresh project B: `send_line("import /A.zip")`; `wait_for` import note (rebuild auto-runs on missing `.db`); assert the table + row are present (`show data` / Tables panel). 4. **Undo after DROP.** Create table; `send_line("drop table Customers")`; confirm gone; `send_line("undo")`; **`wait_for("Restore that earlier state?")`** (the real modal); `send("y")`; `wait_for("Customers")` restored. ### CI No workflow edit needed for execution: `e2e_pty` is a default target, so the existing `gate` job's `cargo test --no-fail-fast` runs it on the Linux container. **Validate locally first** that a PTY opens inside the `rdbms-playground-ci` image (container `/dev/ptmx`); if not, add `--init`/pts mount notes to the ADR. Advances **TT5** (Tier-4-in-CI on Linux); Windows execution runner stays out of scope (no runner). --- ## Workstream 2 — NFR verification ### Gated: WCAG contrast (NFR-5, NFR-7) — `src/theme.rs` `#[cfg(test)]` - Add a `relative_luminance(Color) -> f64` + `contrast_ratio(a,b) -> f64` helper (WCAG 2.x sRGB formula) in the test module; a `match Color::Rgb` channel extractor (panics on non-RGB — also a guard against a future named-colour regression). - For **both** `Theme::light()` and `Theme::dark()`, assert **≥ 4.5:1** for normal-text foregrounds on `bg`: `fg`, `error`, `warning`, `system`, `plan_efficient`, `mode_simple`, `mode_advanced`, and the info-carrying syntax tokens (`tok_keyword/identifier/type/number/string/flag/function/error`). - **`muted`/`tok_punct`** (deliberately dim secondary text) and **borders** (`border`/`border_advanced`, non-text UI): assert the WCAG **non-text 3:1** threshold, with a comment citing the WCAG large-text/non-text allowance. If any *fails even 3:1*, that's a real finding → surface to the user, don't silently relax. (Supersedes the existing inequality-only theme tests.) - This runs in the existing gate — permanent regression protection on the palette. ### Measured + documented: startup (NFR-1) + idle memory (NFR-3) Two measurement tests in `e2e_pty.rs`: - **Startup:** time from spawn to `wait_for("SIMPLE")` (first rendered frame); assert a **generous** bound (e.g. < 1500 ms locally) and `eprintln!` the actual. - **Idle memory:** after the app is idle, read the child's `/proc//status` `VmRSS` (Linux); assert a generous bound and print actual. (Linux-gated via `#[cfg(target_os = "linux")]`.) The point is gross-regression detection, not a tight SLA gate (user decision). ### Verify-by-argument + reviewer note (NFR-2, NFR-4, NFR-6) - **NFR-2** (input off-thread): already architecturally true — DB runs on the worker thread (ADR-0010) and input on a separate Tokio task (confirmed in `runtime.rs`). Record the evidence; optionally a test that a query doesn't block a subsequent keystroke render. - **NFR-4** (distinctive design) / **NFR-6** (cross-platform parity): qualitative / partly-CI. Written reviewer verification (DA-hat) in the NFR verification doc; parity partly evidenced by the CI matrix (Linux+macOS execute, Windows builds). ### Requirements bookkeeping Move NFR-5/7 → `[x]` (gated test), NFR-1/3 → `[x]` (measured + bounded test + recorded numbers), NFR-2 → `[x]` (architectural evidence), NFR-4/6 → `[/]` with the reviewer note (honest: not fully automatable). Each with a commit/test ref. --- ## Workstream 3 — CHANGELOG - New **`CHANGELOG.md`** at repo root, **Keep a Changelog** + SemVer. - `[Unreleased]` (empty/seeded) + one consolidated **`[0.2.0] - `** with Added/Changed/Fixed of the notable user-facing changes (install methods, packaging, `--version`/`version`, seed improvements, hint feature, readline keys, cross-mode history) mined from ADRs 0053–0056 + handoffs 73–75. 0.1.0 = pre-history (a one-line note). - Optional follow-up (flag, don't auto-do): wire `--release-notes-url` into the CI `winget`/release path — out of scope unless you want it now. --- ## Docs / ADR updates (project discipline) - **Amend ADR-0008** (Tier-4 section): record the realized tooling (`portable-pty` + `vt100` + hand-rolled `wait_for`, `expectrl` dropped), serial execution, tight-timeout/fail-fast stance, the default-target CI wiring, and the four implemented flows. Update `docs/adr/README.md` in the same edit. - **New ADR-0057 — NFR verification strategy** (next free number; assigned at merge per ADR-0000): the contrast-gate + thresholds (incl. the 3:1 muted/border allowance), the measured-not-tightly-gated stance for startup/memory, and the verify-by-argument treatment of NFR-2/4/6. Add a short **NFR verification record** (the measured numbers + reviewer notes) — either in the ADR or a `docs/` companion. Update the README index. - Update `docs/requirements.md`: TT4 `[~]`→`[x]`, TT5 `[/]` note (Tier-4 now in Linux CI; Windows-exec still pending), NFR rows as above. - A handoff note (next sequence number) per project convention. --- ## Verification (Phase 5) 1. `cargo test` — full suite green; new e2e_pty + contrast tests pass; **2509 + new, 0 failed, ≤1 ignored**; no regressions vs baseline. 2. `cargo clippy --all-targets -- -D warnings` + `cargo fmt --check` clean. 3. Run `e2e_pty` repeatedly (e.g. 5–10×) locally to confirm non-flaky under the tight timeouts before relying on the gate. 4. Confirm a PTY opens inside the CI image (or document the fix). 5. DA-hat pass over each workstream + the NFR reviewer judgements, written down. 6. `cargo sweep` at the milestone (CLAUDE.md build hygiene). ## Risks / checkpoints - **vt100 currency** — verify before adding; fall back to `avt` if stale. - **PTY-in-container** — validate early; the whole TT4 CI value depends on it. - **muted/border contrast** — may fail even 3:1; if so it's a finding to escalate, not silently relax (could mean a small palette tweak — touches a decided area, so escalate). - **Commits** — confirm each message; no AI attribution; append-only; never push.