Skip to content

test(gate): assert the checklist gate's unasserted state-machine behaviour - #237

Merged
tupe12334 merged 3 commits into
mainfrom
test/gate-unit-tests
Oct 26, 2026
Merged

tupe12334 merged 3 commits into
mainfrom
test/gate-unit-tests

Conversation

@tupe12334

@tupe12334 tupe12334 commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Gap

core/src/gate.rs holds evaluate_checklist — the per-checklist gate decision, i.e. the sequential checklist state machine. It had no companion <module>_tests.rs.

Correction to this description as originally written: I claimed gate.rs was the only module under core/src/ without one. That was wrong — catalog.rs (20 lines) and error.rs (52 lines) also lack a companion test file. gate.rs was the largest such module at 257 lines, which is why it was picked, but it was not the only one.

Measured correction to an earlier draft of this description: gate.rs line coverage was already 98.01% before this PR. I verified that with cargo llvm-cov --summary-only and again with -- --skip gate::tests (confirming 0 gate::tests ran) — the numbers are identical either way.

So the gap is not unexecuted lines. run_tests.rs and tests/gate_integration.rs already ran these lines; nothing asserted their results. That is precisely the failure mode a coverage percentage hides: a line counts as covered the moment it executes, whether or not any assertion checks what it produced.

Behaviour this PR pins

Behaviour Why it matters
fallback-id scope key when a caller sends no session_id Without it, progress is lost between hook invocations for any caller that omits a session id
next_state auto-advance on a single transition Pins the pre-computed hop that ack.sh relies on
a branching state leaves next_state unset A branch must not auto-advance, or ack.sh silently picks a path for the agent
multi-choice run one of: block message The agent's only way to learn the branch options
silent skip when state.json names a state the flow no longer has Guards against blocking forever after flow.mmd is edited mid-session
step/total counter derived from visited The agent's only progress signal
reset = "always" Must block every time and persist no session state
complete-then-restart cycle Approves one attempt, then re-arms from the top
per-path audit::append arguments See below

The audit contract

audit::append swallows every I/O error by design, and audit_tests.rs pins only the JSONL format — never which arguments a caller passes. The gate is its only caller and passes different arguments per path:

  • reset = "session" block → event = "block", session = <scope key>
  • reset = "always" block → event = "block", session = "always" (a literal, not a session id)
  • complete → event = "complete", state = "[*]"

None of that was asserted anywhere in the crate, and because append never surfaces an error, a wrong argument could not fail loudly in any other test.

Change

  • core/src/gate_tests.rs — new, 7 tests, wired into gate.rs with the same #[path = "<module>_tests.rs"] pattern the rest of the crate uses (per refactor: move unit tests into <module>_tests.rs files #210).
  • core/src/gate.rs — +5 lines, test-module declaration and its clippy allows only.
  • core/Cargo.toml — promotes clippy::verbose_file_reads to deny per the repo's pre-push lint ratchet. Zero violations to fix.

No production logic changed.

Verification

Every runnable repo CI gate, locally:

Gate Result
cargo fmt -- --check clean
cargo clippy --all-targets --locked -- -D warnings clean
cargo test --locked 186 passed (was 178)
cargo +1.88 test --locked (MSRV) 186 passed
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps clean
cargo package --locked ok
cargo llvm-cov gate.rs 98.01% lines / 92.49% regions

cargo-machete and cargo-deny are not installed locally; both pass in CI, and neither can be affected by a diff that adds no dependency.

gate_tests.rs is 238 lines — over linecheck.yml's 200-line warn threshold, well under its 1100-line error threshold.

Decision record

Reviewed through the pr-improvement gate, with each decision put to the local Kev decision model:

Question Outcome
Keep / split / grow keep as one PR, p=0.9154
Seed state.json directly vs. drive through ack.sh seed directly, p=0.8846 — keeps these unit tests independent of the shell script
Assert the gate's audit arguments? First probe near-tied (0.553 / 0.447). Re-asked with verified facts about what audit_tests.rs does and does not pin → add the assertions, p=0.8792. Done in 4f0f0ee.
Merge noul, round 1 0.7792 — below the 0.8 own-PR gate.
Merge noul, round 2 0.6846 — dropped once I disclosed two open concerns: the body overstated the gap, and CI had not re-run on the new head.
Merge noul, round 3 0.8366 — gate cleared. Both concerns were turned into facts rather than questions: this body now states the measured 98.01% coverage, and all 13 checks re-ran green on 6d4d983.

Opened by the moadim routine "Polyhook/steplock production-readiness audit → PR + Slack".

🤖 Generated with Claude Code

Merged at merge noul 0.8366 against the 0.8 own-PR gate, with every check green and no administrator bypass.

tupe12334 and others added 3 commits October 26, 2026 03:18
`gate.rs` holds the per-checklist gate decision — the sequential checklist
state machine — and was the only module under `core/src` with no
`<module>_tests.rs`. `run_tests.rs` exercised it only through the happy
single-step path, so several behaviours could regress undetected:

- the `fallback-id` scope key used when a caller sends no `session_id`
- `next_state` auto-advance vs. a branching state with several transitions
- the multi-choice "run one of" block message
- the silent skip when `state.json` names a state the flow no longer has
- the `step/total` counter derived from `visited`
- `reset = "always"`, which must block without persisting session state
- the complete-then-restart cycle

Add `core/src/gate_tests.rs` with a test per behaviour. No production
code changes.

Opened by the moadim routine "Polyhook/steplock production-readiness
audit → PR + Slack".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`audit::append` swallows every I/O error by design, and `audit_tests.rs`
pins only the JSONL format, never which arguments a caller passes. The
gate is its only caller and passes different arguments per path:

- `reset = "session"` block: `event = "block"`, `session = <scope key>`
- `reset = "always"` block: `event = "block"`, `session = "always"`
- complete:               `event = "complete"`, `state = "[*]"`

None of that was asserted anywhere, so a wrong argument could never fail
loudly. Assert it in the three gate tests that reach those paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pre-push checklist asks every push to promote one more clippy lint.
`verbose_file_reads` fits this crate, which reads config, flow and state
files on the gate's hot path. No violations to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tupe12334 tupe12334 changed the title test(gate): cover the checklist gate's untested state-machine paths test(gate): assert the checklist gate's unasserted state-machine behaviour Oct 26, 2026
@tupe12334
tupe12334 merged commit a7ef43f into main Oct 26, 2026
13 checks passed
@tupe12334
tupe12334 deleted the test/gate-unit-tests branch October 26, 2026 01:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant