fix(run): drop first_accepted gate from catchup synth-arm

Catchup branch gated on first_accepted, but an earlier accepted gray tick
consumes the flag before a dark_* arrives, so the real prime-phase color
falls through to noise classification and no alert fires. Gate on
IDLE + dark_* alone — self-sufficient and correct.

Regression: 2 unit tests for _handle_tick + 1 integration test feeding
run_live a scripted gray→gray→dark_red→dark_red→light_red sequence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-16 18:54:03 +00:00
parent f4b9000100
commit d7305fbbfc
3 changed files with 159 additions and 1 deletions

View File

@@ -218,3 +218,41 @@ def test_handle_tick_first_turquoise_no_catchup_label():
# Decision 3A: cannot distinguish fresh arm from catchup-at-arm-phase
assert "catchup" not in notif.alerts[0].title.lower()
assert "catchup" not in notif.alerts[0].body.lower()
# ---------------------------------------------------------------------------
# Regression: user bug 2026-04-16. Catchup must fire whenever IDLE + dark_* is
# observed, regardless of first_accepted. Prior gray noise tick consumed
# first_accepted and the subsequent dark_red was gated out of catchup.
# ---------------------------------------------------------------------------
def test_handle_tick_catchup_dark_red_when_not_first_accepted():
fsm = StateMachine(lockout_s=60)
notif = FakeNotifier()
audit = FakeAudit()
tr = _handle_tick(fsm, "dark_red", 1.0, notif, audit, first_accepted=False)
assert tr is not None
assert tr.next == State.PRIMED_SELL
assert len(notif.alerts) == 2
assert notif.alerts[0].kind == "arm"
assert notif.alerts[0].direction == "SELL"
assert "catchup" in (notif.alerts[0].title + notif.alerts[0].body).lower()
assert notif.alerts[1].kind == "prime"
assert notif.alerts[1].direction == "SELL"
def test_handle_tick_catchup_dark_green_when_not_first_accepted():
"""Mirror — BUY side."""
fsm = StateMachine(lockout_s=60)
notif = FakeNotifier()
audit = FakeAudit()
tr = _handle_tick(fsm, "dark_green", 1.0, notif, audit, first_accepted=False)
assert tr is not None
assert tr.next == State.PRIMED_BUY
assert len(notif.alerts) == 2
assert notif.alerts[0].direction == "BUY"
assert notif.alerts[1].direction == "BUY"