Deterministic Testing & Quality Gates
How quality invariants are mechanically enforced through deterministic gates, anti-weakening tripwires, and testing budgets.
Verification & Testing Strategy
This document defines how the invariants and properties in QUALITY.md
are mechanically proven through automated tests, static analysis, quality
gates — and mandatory runtime validation.
Adapted to the Lichess Mobile foundation (Phase 1+). Test file paths will be
re-established as the new suite is built; the enforcement matrix below is
the durable contract. Legacy tests (legacy/pre-reset) are contract
references — mine them for intended behavior, translate valuable cases,
never copy tests that only encode the old architecture.
1. Quality Invariant Enforcement Matrix
Invariant / Property in QUALITY.md |
Enforcement Mechanism | Test Target |
|---|---|---|
| Layer Independence & Clean Architecture | Static analysis (fvm flutter analyze, zero warnings) + import-boundary gate G03 (.gates/banned-apis.json) + in-suite boundary test |
whole project; the domain layer is lib/src/domain/; boundary test is test/gates/domain_boundary_test.dart |
| Single Chess Representation (dartchess only) | Dependency review + analyzer import linting | pubspec.yaml, domain & application code |
| Legal Move Validation & SAN/UCI | dartchess itself (already upstream-tested); our adapter unit tests | test/import/pgn_importer_test.dart |
| PGN Multi-game & Variation Preservation | Unit tests of import pipeline (dartchess PgnParser → Study/Chapter/tree) |
test/import/pgn_importer_test.dart |
| Study Tree Normalization & Error Reporting | Integration tests incl. malformed-PGN quarantine | test/import/pgn_importer_test.dart, test/import/chapter_orientation_test.dart |
| SRS Interval Growth & Lapse Recovery | Deterministic unit tests with injected Clock + clock rule in G03 (no DateTime.now in scheduling/review paths) |
test/domain/scheduler_test.dart, test/domain/chess_fsrs_scheduler_test.dart, test/gates/domain_boundary_test.dart |
| Repertoire Answer Validation & Persistence | Service tests over repository (in-memory + sqflite) | test/review/review_service_test.dart |
| Auto-Traversal & No Permanent Exclusion | Deterministic engine tests (fixed clock) | test/review/review_engine_test.dart |
| Review Scene Rendering & Interaction | Widget tests using Lichess test_helpers.dart board helpers |
test/view/review/review_screen_test.dart |
| Incremental Persistence | Repository tests asserting write scope | test/persistence/sqlite_study_repository_test.dart |
| Move Validation Latency (< 16 ms, §4.1) | Wall-clock assertion on the median of a graded-move batch, sample list printed on failure | test/review/move_latency_test.dart |
| Spec-to-Test Traceability (SPEC.md) | Every INV-xxx cited by at least one test; tests cite only real IDs (gate G05) |
SPEC.md ↔ test/ suites via SPEC coverage comments |
| No Unreachable Source | Import/export graph walk from lib/main.dart; new unreachable files fail, ratcheted against a baseline |
test/reachability/reachability_test.dart |
| Complete Mandatory Quality Gate | Executable script | verify |
2. Test Suite Organization (target)
test/
├── domain/ # pure Dart: entities, scheduler, review engine (no Flutter)
├── import/ # PGN → study normalization, variation preservation, errors
├── persistence/ # sqflite store (sqflite_common_ffi in CI), durability
├── review/ # review session engine + provider-level tests
└── view/ # review scene widget tests (board helpers, feedback states)
New tests are written with the contract (test-first or test-with) per the
development loop in AGENTS.md.
3. Verification Levels
Level 1: Targeted Brick Check
During development of a specific function or contract, run only the targeted test file:
fvm flutter test test/domain/scheduler_test.dart
Level 2: Full Local Quality Gate (./verify)
Before committing or marking a task complete:
./verify
executing fvm flutter analyze (zero warnings) + fvm flutter test.
Level 3: Runtime Validation (MANDATORY for user-visible work)
Automated green is not evidence of a working app (this project has been burned by exactly that before). For every user-visible milestone:
./verifypasses.fvm flutter run -d linux(or attached device) and manually exercise the affected feature.- Visually inspect the UI (screenshots for UI work).
- Only then record the milestone complete in
IMPLEMENTATION_PLAN.md.
Level 4: Continuous Integration
GitHub Actions runs ./verify on every PR and push to main in a clean
environment.
The Complete Quality Gate Registry
GATES: the registry of everything that can reject a change
Protected path. If a gate is not in this file, it does not exist.
Local entry point (same as CI): ./scripts/gates.sh [t0|t1|t2].
Product suite entry point ./verify (analyze + full flutter test) is owned
by test.yml and stays the authority on whether the suite is green. The gates
below do not duplicate it; they enforce what it cannot: referee integrity,
spec coverage of intent, and check strength.
| ID | Gate | Tier | Runtime | Catches | Known blind spots | Local command | Owner |
|---|---|---|---|---|---|---|---|
| G01 | format + lint + strict types | T0/T1 | min | style drift, unsafe casts | logic errors | owned by test.yml (dart format check + flutter analyze) |
CI |
| G02 | architecture boundary test | T1 | s | domain importing UI/DB/IO, wall-clock reads in scheduling paths | dynamic imports; application-layer network use (import path only, by design) | fvm flutter test test/gates/domain_boundary_test.dart |
gates PR |
| G03 | banned APIs in core | T0 | s | hidden clocks, layer rot | aliased/bracketed call sites; Random() fallback (documented, injected) |
scripts/gates.sh t0 |
gates PR |
| G04 | unit + property + widget tests | T1 | min | regressions, invariant breaks | unspecified behavior; load-sensitive review tests (see Blind spots) | owned by test.yml |
CI |
| G05 | spec traceability | T1 | s | invariants with no test; tests citing unknown IDs | weak tests that cite IDs (needs G06) | python3 scripts/gates/spec_trace_check.py --spec SPEC.md --tests test |
gates PR |
| G06 | hand-picked mutants (local T2) | T2 | min | tests that do not check scheduler boundaries | unlisted lines; equivalent mutants | ./scripts/gates.sh t2 |
contributor |
| G07 | protected paths | T1 | s | referee edits without human approval | uncommitted local edits (CI judges commits) | python3 scripts/gates/protected_paths_check.py --base origin/main |
owner |
| G08 | test-weakening tripwire | T1 | s | removed asserts/tests, new skip:/suppressions in live code |
subtle weakening (renamed expectations, padded tolerances); redteam/ + scripts/gates/ excluded (inert corpus data / self-naming patterns — both G07-guarded) |
python3 scripts/gates/test_weakening_check.py --base origin/main (an Ack-G08: <reason> commit trailer records reviewed intent; findings still print — the trailer is a line of its own starting with Ack-G08:, matched as ^Ack-G08:, so a mention inside another line does not count) |
owner |
| G09 | fail-to-pass (bugfix PRs) | T2 | min | tests that cannot fail | wrong-reason failures (use EXPECT_PATTERN); reviewer-enforced until CI-wired | bash scripts/gates/fail_to_pass_check.sh <base> "<test cmd>" |
reviewer |
| G10 | ratchets (deps, suppressions, skips, tests, domain size) | T1 | s | silent drift | metrics gamed in isolation (kept small on purpose) | ./scripts/gates.sh t1 (collect + compare) |
gates PR |
| G11 | dependency + secret posture | T1 | — | unjustified deps | no scanner wired yet; pubspec.yaml is a protected path so every dep change needs approval | reviewer + G07 | owner |
| G12 | red-team corpus recall | T3/nightly + gate-change PRs | min | weak fast gates | unknown unknowns | python3 scripts/gates/redteam_runner.py --gate-cmd "./scripts/gates.sh t1" |
gates PR |
Class → evidence (enforced by reviewer + PR template; CI-wired where noted)
| Class | Required evidence |
|---|---|
| bugfix | regression test failing on base, passing here (G09 script output pasted; EXPECT_PATTERN for the right reason) |
| perf | before/after numbers on the pinned harness + equivalence test vs old implementation |
| refactor | no test files modified (G08 verifies), behavior-equivalence evidence |
| feature | SPEC.md updated first in a spec-change PR; property tests per new invariant (G05 verifies) |
| test-only | names the mutant, invariant, or ratchet it improves |
| dependency | written reason + pubspec.yaml approval (G07); size/startup note |
| docs | no code changes (fast CI lane stays cheap) |
| gate-change / spec-change | separate PR, no product code, human approval (G07), red-team recall not reduced |
Blind spots (known, accepted)
- Uncommitted local edits bypass G07/G08 locally; CI judges pushed commits.
- Several review controller tests are load-sensitive (
pumpAsyncfixed 80ms waits): they can flake under CPU contention in wide runs and pass in isolation. Re-run the file alone before blaming a change; the real fix is condition-polling helpers (see AGENTS.md lessons 2026-09-28). build_runnerrewriteslib/l10n/*.dartformatting in fresh worktrees; rundart format lib/l10n/after codegen and do not commit the churn.- G05 proves intent coverage, not test quality; G06 covers six scheduler boundaries only. Full mutation and crash-consistency are Phase 2.
Gate health (review quarterly)
| Gate | Rejections | False positives | Flaky reruns | Escapes traced | Action |
|---|---|---|---|---|---|
| (empty — first review 2027-01) |
Escapes log
| Date | What escaped | Root cause | New gate / invariant / red-team case |
|---|---|---|---|
| (none recorded since gates introduced 2026-10-01) | |||
| 2026-10-02 | Every lichess socket handshake was refused with HTTP 400, so cloud evaluation, study sync and the ping indicator were dead while the suite stayed green | The fake channels key on path alone, so no test could see that the sri was missing from the connect URL, and no fast gate reads a wire format | Regression test shipped in #129. SPEC INV-064 and its test citation added in the escape PR (this row's "not added" stood until G-BASEINV was fixed by #135/#137). Red-team case rt-08 guards the trace link; the wire-behavior link is guarded by the required Tests lane (G04) — t1 recall runs no flutter tests, so a reintroduced wire bug escapes t1 by construction |
Adding a row? Follow docs/ESCAPE_TO_GATE.md — the row is step 7 of the
escape loop, after the regression test, SPEC invariant, and red-team case.
Operator checklist (owner actions, not automatable here)
- GitHub branch protection on
main: required status checks (Tests,Gates), required human review, no direct pushes, no force pushes. - Confirm SPEC.md T0/T1 invariants by merging (they transcribe QUALITY.md).
- Quarterly: review this file's health tables, tighten ratchets, delete dead gates.