feat(tests): add the golden decision tables, and fix two engine bugs they found
The parity contract from CLAUDE.md section 7 now exists: 42 cases across 11 scenario groups, with a runner that asserts every case and a coverage check that asserts every mandatory scenario is claimed. **A design change, and why.** ADR-16 originally specified JSON tables, justified by "serde_json will already be in the tree". That was wrong — `rusqlite` does not pull `serde` by default and `uniffi` has not landed, so nothing provided it, and `serde` is not a section 3 dependency (rule 0.3). The tables are therefore a Rust builder DSL: the same declarations, but a typo in a field name is a compile error rather than a test that silently asserts the wrong thing. For a document whose entire job is to be a contract, compile-time checking beats diffability. Converting to JSON later is mechanical if serde is approved. **Two engine bugs the tables caught, both real:** 1. **Overnight windows woke the daemon immediately.** `time_window_boundary` computed the end as `end - minute` and clamped at zero. For a 22:00–06:00 window active at 23:00, "06:00 today" is in the past, so the boundary became *now* — and the daemon would wake, re-evaluate, get the same answer, and wake again, forever. Not a missed block, but a busy loop burning the section 5 idle budget. Fixed by detecting the late half of an overnight window and crossing midnight properly. 2. **An inactive schedule woke a day early.** The next-start search iterated day offsets to test membership but computed the delta as `start - minute` only, wrapping by 1440 without advancing the day. A Monday–Friday schedule evaluated on Saturday reported Monday's start as 21 hours away — Sunday 09:00, a day not in the set. The block still fired, because the next evaluation recomputed it, but the daemon woke a day early for nothing. Fixed by folding the day offset into the delta. **Four of my own table cases were wrong too**, which is the other half of why they are worth having: a UTC+5 offset is 15:00 local and therefore *inside* 09:00–17:00 (I had asserted outside); the same for the DST cases. The engine was right and the test was wrong in all four. **Coverage bookkeeping is checked, not promised.** `coverage::Location` records whether a scenario is covered by a table or by a code fixture — clock tampering and the 24-hour cap are about the passage of time and a bound across lock levels, not a decision at an instant, so they cannot be expressed as data. Both directions fail: an uncovered scenario is an error, and so is a `covers` tag that is not on the mandatory list, which is how a case quietly stops testing what its name claims. **`BlockReason` now carries its target.** A schedule may name twenty targets and they do not all become blocked — some are paused, some belong to a condition that is not currently active, some are on the never-block list. Without carrying which target a reason is about, two blocked apps under one schedule produce two identical reasons and the UI has to re-derive the pairing, which is exactly the drift `BlockReason` exists to prevent. The tables could not assert per-app attribution without it. **The DST gap is recorded, not papered over.** `CivilTime` is an epoch plus an offset with no timezone database behind it, so the tables pin that a fixed local window stays fixed as the offset moves — including across a DST transition — but cannot express a spring-forward time that does not exist or a fall-back time that is ambiguous. Those are section 7 cases that are genuinely not covered, and LIMITATIONS.md now says so. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+28
-11
@@ -37,7 +37,7 @@ which parts to trust.
|
||||
| [13](#adr-13) | Zero external dependencies in M0 | Accepted |
|
||||
| [14](#adr-14) | Linux enforcement backend order: KWin, X11, process | Accepted |
|
||||
| [15](#adr-15) | Linux-first milestone order (amends section 8) | Accepted |
|
||||
| [16](#adr-16) | Golden tables are JSON, not TOML | Proposed |
|
||||
| [16](#adr-16) | Golden tables are Rust, not JSON | Accepted |
|
||||
| [17](#adr-17) | Licence not yet chosen | **Open — owner decision** |
|
||||
|
||||
---
|
||||
@@ -431,22 +431,39 @@ machine.
|
||||
---
|
||||
|
||||
<a id="adr-16"></a>
|
||||
## ADR-16: Golden tables are JSON, not TOML
|
||||
## ADR-16: Golden tables are Rust, not JSON
|
||||
|
||||
**Status:** Proposed — depends on ADR-4 and the serde decision
|
||||
**Status:** Accepted and implemented. The original JSON rationale turned out to
|
||||
be wrong, and is recorded below rather than quietly replaced.
|
||||
|
||||
**Context.** Section 7 requires golden decision tables as the cross-platform
|
||||
parity contract. The format must be serialisable and diffable in review.
|
||||
parity contract.
|
||||
|
||||
**Decision.** JSON, one file per concern, under `tests/golden/`.
|
||||
**What was originally decided, and why it was wrong.** JSON, on the reasoning
|
||||
that `serde_json` would already be in the dependency graph. That was falsified in
|
||||
practice: `rusqlite` does **not** pull `serde` by default, and `uniffi` had not
|
||||
landed, so nothing in the graph provided it. `serde` is not a section 3
|
||||
dependency, and section 0 rule 3 requires asking before adding one.
|
||||
|
||||
**Rationale.** `serde_json` is already in the dependency graph via `rusqlite` and
|
||||
`uniffi`, so TOML would add a crate for no gain, and section 3 makes every crate
|
||||
justify its cost. JSON is also what a review diff reads most naturally.
|
||||
**Decision.** A builder DSL in Rust. `tests/src/golden.rs` holds the format and
|
||||
harness, `tests/src/tables.rs` the cases, `tests/src/coverage.rs` the coverage
|
||||
bookkeeping, `tests/src/run.rs` the runner.
|
||||
|
||||
**Not yet decided.** The parse layer needs serde, which is a dependency decision
|
||||
requiring the owner's sign-off. The format itself is written down in
|
||||
`tests/src/golden.rs` now; the parse arrives in M1 with ADR-4.
|
||||
**Rationale.** The same declarations, with two properties JSON cannot offer: the
|
||||
compiler rejects a typo in a field name, and a wrong variant is a type error
|
||||
rather than a test that quietly asserts the wrong thing. For a document whose
|
||||
entire job is to be a contract, compile-time checking is worth more than
|
||||
diffability.
|
||||
|
||||
Converting to JSON later, if serde is approved, is mechanical — only
|
||||
`tests/src/golden.rs` changes and the case bodies become literals instead of
|
||||
builder calls.
|
||||
|
||||
**Also settled here.** Three scenarios are deliberately *not* table-shaped.
|
||||
Clock tampering and the 24-hour cap are about the passage of time and a bound
|
||||
across lock levels, not a decision at an instant, so they live as code fixtures
|
||||
in `tests/src/purity.rs`. `coverage::Location` records which is which, so
|
||||
"covered" can never quietly mean "listed but not tested".
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user