test(autonudge): gate the prompt-loop double that carries probe state - #8317
Conversation
The five monitor_update paused-loop tests build a prompt loop whose monitor holds a terminal_pending / outcome, but the double never sets gate. is_structured_monitor_loop reads "monitor is not None and not gate", so an ungated loop with probe state is a controller-owned structured monitor and the applier refuses message/max_cycles/active as legacy fields -- every assertion then fails against that refusal. A real prompt loop only ever carries probe state when it is gated (the service infers a monitor only when loop.gate is true), so the double now takes gate and the five tests set it, matching the invariant the production discriminator relies on. No production code changes.
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: |
Design Review (Fable 5) — ✅ PASSDesign-level review of Test-only change verified against production code; the double now mirrors the Design-Verdict: PASS Test-only alignment of a hand-rolled double with the real Suggestions
[DESIGN-REVIEWED] a326fc5 |
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: |
Problem / Motivation
mainis red:Backend Tests (3.12, 1)andBackend Tests (Windows) (1)fail 5 tests intest/test_autonudge_stop_auth.py(test_applier_owed_terminal_turn_is_not_reported_as_a_spent_cap,test_applier_owed_blocked_turn_is_not_reported_as_a_merge,test_applier_settled_terminal_loop_is_not_reported_as_a_manual_pause,test_applier_a_settled_outcome_outranks_a_stale_owed_turn,test_applier_a_spent_cap_with_no_terminal_news_still_revives) withReproduces on a pure
origin/maincheckout.Why it matters
Every open PR inherits the two red backend shards, so
PR Readinesscannot pass for anyone until this is fixed.What changed (motivation → approach → change)
The five tests (from #8201) build a prompt loop whose
monitorcarriesterminal_pending/outcome, but the_FakeLoopdouble never setsgate.is_structured_monitor_loop(from #5184) readsmonitor is not None and not gate, so an ungated loop with probe state is classified as a controller-owned structured monitor, andapply_session_directive("monitor_update", ...)routes to_structured_monitor_update, which refusesmessage/max_cycles/activeas legacy fields. The assertions then run against that refusal string.A real prompt loop only ever carries probe state when it is gated — the service infers a
MonitorStateonly whenloop.gateis true, and theNudgeLoop.monitorfield documents thatgate=Truerecords belong to the prompt path. The double was violating the invariant the discriminator relies on. Fix:_FakeLoopgains agateparameter (defaultFalse, matchingNudgeLoop) and the five tests passgate=True. No production code changes.Tests
test/test_autonudge_stop_auth.py: 50 passed locally (was 45 passed / 5 failed onorigin/main). black / isort / flake8 clean on the file.Manual verification
N/A — unit coverage sufficient; the change is confined to a test double.
Related Issues
no linked issue: main-red introduced by the interaction of #8201 and #5184, caught by CI on open PRs.
Pattern harvest
Rule candidate: review-prompt
Pattern: a hand-rolled test double omits a field a production discriminator reads via
getattr(obj, name, default), so the default silently selects the wrong code path.Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)