test(autonudge): pin the gate premise the paused-loop tests rely on - #8321
Conversation
69e4513 to
94a2504
Compare
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS A real diagnosed failure mode, closed with one proportionate assertion against the production predicate — mutation-verified, test-only, no new surface. [DESIGN-REVIEWED] 60ad3b1 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
|
CI note — the one red check is a repo-wide npm-registry timeout, not this diff. It needs a maintainer decision, not a change here.
(five attempts named It is failing across the repository, not on this PR. A sample of open PRs between 01:45 and 01:51 UTC shows the same job Why it cannot be this diff, independently of that. The gate fails closed because What I did and deliberately did not do. I re-ran the job five times rather than work around it. I am not adding a The question for a maintainer: Everything else on |
The five paused-loop tests for monitor_update depend on `gate=True` putting their `_FakeLoop` on the LEGACY side of `is_structured_monitor_loop`, which reads `monitor is not None and not gate`. The class docstring states that dependency; nothing executes it. So an edit to the predicate reports itself only as five assertion failures against "cannot apply legacy fields to a structured monitor" -- a refusal string that names neither the flag nor the routing, and reads as a bug in the wording under test rather than in the classification upstream of it. That is how the same five went red on main for several hours after two PRs that were each correct alone landed three hours apart. The added test asserts the three classifications the doubles rely on against the production predicate: a gated prompt loop carrying probe state is legacy, a controller record is structured, a plain prompt loop is legacy. Dropping the `gate` term from the predicate now fails six tests instead of five, and the sixth names the predicate, so the batch carries its own cause. Test-only; no production behaviour changes. Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
94a2504 to
60ad3b1
Compare
|
Resolved without a maintainer decision — main answered the question, and a rebase picks it up. My earlier note asked whether the
That is why a re-run could never have cleared this PR: a re-run replays the original merge-ref snapshot, so it re-tested a base that predated the fix. The remedy was a rebase, not another attempt. What I did. Rebased onto Local verification on the new base. Targeted tests The three review lanes stamped at [operator: bolichen97] |
Problem / Motivation
main's fivemonitor_updatepaused-loop tests intest/test_autonudge_stop_auth.pywent red for several hours today, and thefailure told nobody why. All five reported the same thing:
That string is a production refusal, so the batch reads as a bug in the
wording under test. The actual cause was one level up:
monitor_updatesplitson record kind before it reads a single bound, and
is_structured_monitor_loopdecides that kind withmonitor is not None and not gate. The tests'_FakeLooppredatedgate, sothe moment they gave it the
monitorholding the terminal state they are about,it classified as a controller record and the legacy branch under test became
unreachable. #8317 has since fixed the doubles.
The premise those five now rest on —
gate=Trueputs a probe-state double onthe legacy side — is stated in
_FakeLoop's docstring and executed nowhere.Why it matters
The premise is load-bearing for five tests and invisible in all five. Nothing
observes it, so the next edit to
is_structured_monitor_loopreproduces exactlythe diagnosis cost that just happened: five assertion failures against a refusal
string that names neither the flag nor the routing, in a file whose diff is
clean. It cost this repo a red shard inherited by every PR on that tip, plus two
independent agents reaching the same diagnosis from scratch.
It is also a live risk rather than a hypothetical one, because the classification
straddles two subsystems that ship separately: the discriminator lives in
autonudge.py, the split that reads it indashboard/session_directive_apply.py,and the tests that depend on it name neither in their own diff. That is precisely
the pairing no path-based or file-overlap check can make — it is only visible
through the predicate.
What changed (motivation → approach → change)
Goal: make the premise fail as itself, so the batch carries its own cause.
Approach considered and rejected: restating the invariant in more prose next
to each of the five. Prose is what is already there, and it is what did not fire.
The change: one test,
test_a_probe_state_double_is_a_legacy_loop_only_while_it_carries_the_gate,asserting the three classifications the doubles rely on against the production
predicate itself:
is_structured_monitor_loopmonitor=_FakeMonitor(), gate=True— gated prompt loopFalse(legacy)monitor=_FakeMonitor()— controller recordTrue(structured)False(legacy)Those three are the invariant production already enforces in both directions: the
prompt-loop arming path (
AutoNudgeService.add→_add_unserialized) attaches amonitor only
if gate;add_monitorbuilds its controller record on thedataclass default (
gate=False); and_monitor_tick_is_quiet— the onlywriter of
monitor.terminal_pendingand of theoutcome/MONITOR_TERMINAL_REASONsettlement these tests assert on — returns early onif not loop.gate. It is also what the spec says, indocs/system-specs/modules/learn-cron-dashboard.md: "A prompt-driven zero-tokengate may also carry probe state, but it remains a legacy loop (
gate=true) onthe AutoNudge control surfaces; only controller records (
gate=false) use thestructured monitor APIs". Nothing documented changes, so no spec edit is owed.
Test-only. No production line moves.
Tests
test/test_autonudge_stop_auth.py:51 passed.gateterm fromis_structured_monitor_loop(return getattr(loop, "monitor", None) is not None) takes the module from5 failed, 45 passedto6 failed, 45 passed—the sixth being this test, which names the predicate and the flag rather than a
refusal string. Reverted after measuring; the production file is untouched in
this diff.
Manual verification
N/A — the change is a test, and the suite is the observation. The mutation check
above is the substantive verification.
Related Issues
no linked issue: this is the harvest of a main regression already fixed by #8317,
not a tracked defect of its own.
Pattern harvest
Rule candidate: review-prompt
Pattern: a hand-rolled test double whose attribute set predates a production
discriminator silently answers that discriminator's default. The test then
routes to the wrong branch and asserts against the other branch's error
string — so the failure names a real production refusal and reads as a bug in the
code under test, while the double is the last place anyone looks.
A checkable form, for a reviewer or a review prompt: when a diff adds a field
that a
getattr(x, "field", default)predicate splits on, the review has to lookfor doubles of
xin the test tree, not only for callers insrc/.And the reason this PR exists rather than a comment: an invariant a test
depends on but does not assert is not covered. When N tests share an unasserted
premise, a break in it costs N confusing failures and zero informative ones, so
the premise earns its own assertion against the production symbol.
Not a lint rule: the trigger is "this double stands in for that dataclass", which
nothing in the source states.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)gatediscriminator this pins; no documented behaviour changes.