M1: golden decision tables (42 cases), plus two engine fixes #3
Reference in New Issue
Block a user
Delete Branch "m1/golden-tables"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What changed
The parity contract from CLAUDE.md section 7 now exists: 42 golden cases
across 11 scenario groups, a runner that asserts every case, and a coverage check
that asserts every mandatory scenario is claimed.
Stacked. This branch is
m1/golden-tablesbased onm1/storage. Merge #2(#1 first).
Which requirement this covers
purity.rspurity.rsTest evidence
50 tests total, up from 44.
Two engine bugs the tables found
Both real, and both invisible to the existing unit tests.
1. Overnight windows woke the daemon immediately, forever.
time_window_boundarycomputed the end asend - minuteand clamped at zero. Fora 22:00-06:00 window active at 23:00, "06:00 today" is already in the past, so
the boundary became now. The daemon would wake, re-evaluate, get the same
answer, and wake again — a busy loop spending the section 5 idle budget forever.
Not a missed block, which is why the existing tests missed it: they checked
whether the app is blocked and never checked when the engine wanted to wake.
2. An inactive schedule woke a day early.
The next-start search iterated day offsets to test membership but computed the
delta as
start - minutealone, wrapping by 1440 without advancing the day. AMonday-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 correctly, but the daemon woke a day early for
nothing.
Both are now pinned by cases that fail on the original code.
Four of my own cases were wrong too
The other half of why these are worth having. In the timezone group I asserted
that 10:00 UTC at UTC+5 falls outside a 09:00-17:00 window. It is 15:00 local,
which is inside. Same for both DST cases. The engine was right in all four; the
test was wrong.
One design change, flagged
ADR-16 originally specified JSON tables, justified by "serde_json will already
be in the tree". That was falsified in practice:
rusqlitedoes not pullserdeby default, and
uniffihas not landed.serdeis not a section 3 dependency andrule 0.3 requires asking before adding one.
So the tables are a Rust builder DSL. 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 is
worth more than diffability. Converting to JSON later, if serde is approved, is
mechanical — only
tests/src/golden.rschanges.ADR-16 records the original reasoning and why it failed, rather than being quietly
rewritten.
One API change
BlockReasonnow carries itstarget. A schedule may name twenty targets andthey do not all become blocked — some are paused, some belong to an inactive
condition, 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. That is exactly the drift
BlockReasonexists to prevent, and the tables could not assert per-appattribution without it.
Notes for the reviewer
Coverage is checked, not promised.
coverage::Locationrecords whether ascenario is covered by a table or a fixture, and both directions fail: an
uncovered scenario is an error, and so is a
coverstag not on the mandatorylist — which is how a case quietly stops testing what its name claims.
Clock tampering and the 24-hour cap are deliberately not table-shaped. They
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
purity.rs.The DST gap is real and recorded.
CivilTimeis an epoch plus an offset withno timezone database behind it. The tables pin that a fixed local window stays
fixed as the offset moves, including across a transition — but they 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 uncovered.
LIMITATIONS.mdnow says so, and it is the strongest argument for atimezone-aware time type.
Wakeup::Anyis the default. A case about blocking behaviour should not failbecause a window end moved. The three cases that carry the idle budget declare
their wakeup explicitly, and
idle_budget_cases_declare_their_wakeupfails if oneof them stops doing so.
Checklist:
scripts/ci.shpasses, skips statedDo not merge this yourself
Squash or rebase. Merge #2 first — this branch is based on it.
Review approval: checked against test suite and approved per user instruction.