"""Unit tests for atm.main._handle_tick. Covers the six cases from the arm+prime notification plan: 1. fresh BUY arm (turquoise) 2. fresh SELL arm (yellow) 3. BUY prime (turquoise → dark_green) 4. silent refresh (turquoise → turquoise) 5. catchup synth-arm from dark_green + prime alert, audit tagged catchup:true 6. late-start guard (light_green from IDLE with first_accepted=True) """ from __future__ import annotations from atm.main import _handle_tick from atm.notifier import Alert from atm.state_machine import State, StateMachine class FakeNotifier: """Captures alerts instead of fanning out. send() is synchronous.""" def __init__(self) -> None: self.alerts: list[Alert] = [] def send(self, alert: Alert) -> None: self.alerts.append(alert) class FakeAudit: """Captures audit events in-memory.""" def __init__(self) -> None: self.events: list[dict] = [] def log(self, event: dict) -> None: self.events.append(event) # --------------------------------------------------------------------------- # 1. fresh BUY arm # --------------------------------------------------------------------------- def test_handle_tick_arm_buy(): fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() tr = _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False) assert tr is not None assert tr.next == State.ARMED_BUY assert tr.reason == "arm" assert len(notif.alerts) == 1 assert notif.alerts[0].kind == "arm" assert notif.alerts[0].direction == "BUY" assert "turquoise" in notif.alerts[0].title # --------------------------------------------------------------------------- # 2. fresh SELL arm # --------------------------------------------------------------------------- def test_handle_tick_arm_sell(): fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() tr = _handle_tick(fsm, "yellow", 1.0, notif, audit, first_accepted=False) assert tr is not None assert tr.next == State.ARMED_SELL assert len(notif.alerts) == 1 assert notif.alerts[0].kind == "arm" assert notif.alerts[0].direction == "SELL" assert "yellow" in notif.alerts[0].title # --------------------------------------------------------------------------- # 3. BUY prime — turquoise then dark_green # --------------------------------------------------------------------------- def test_handle_tick_prime_buy(): fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False) tr2 = _handle_tick(fsm, "dark_green", 2.0, notif, audit, first_accepted=False) assert tr2 is not None assert tr2.next == State.PRIMED_BUY assert len(notif.alerts) == 2 assert notif.alerts[0].kind == "arm" assert notif.alerts[1].kind == "prime" assert notif.alerts[1].direction == "BUY" # Non-catchup prime alert must not mention catchup assert "catchup" not in notif.alerts[1].title.lower() # --------------------------------------------------------------------------- # 4. refresh is silent — second turquoise in ARMED_BUY emits no alert # --------------------------------------------------------------------------- def test_handle_tick_refresh_no_alert(): fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False) tr2 = _handle_tick(fsm, "turquoise", 2.0, notif, audit, first_accepted=False) assert tr2 is not None assert tr2.reason == "refresh" assert len(notif.alerts) == 1 # only the first (real) arm # --------------------------------------------------------------------------- # 5. catchup synth-arm from dark_green with first_accepted=True # --------------------------------------------------------------------------- def test_handle_tick_catchup_dark_green(): fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() tr = _handle_tick(fsm, "dark_green", 1.0, notif, audit, first_accepted=True) # Synth arm + real prime feed → FSM lands in PRIMED_BUY assert tr is not None assert tr.next == State.PRIMED_BUY assert tr.reason == "prime" # Two alerts: synth-arm (catchup-tagged) + prime assert len(notif.alerts) == 2 assert notif.alerts[0].kind == "arm" assert notif.alerts[0].direction == "BUY" assert "catchup" in notif.alerts[0].title.lower() or "catchup" in notif.alerts[0].body.lower() assert notif.alerts[1].kind == "prime" assert notif.alerts[1].direction == "BUY" assert "catchup" in notif.alerts[1].title.lower() or "catchup" in notif.alerts[1].body.lower() # Audit: both tick entries tagged catchup:true catchup_events = [e for e in audit.events if e.get("catchup")] assert len(catchup_events) == 2 synth = next(e for e in catchup_events if e.get("synthesized_from") == "dark_green") assert synth["color"] == "turquoise" assert synth["reason"] == "arm" def test_handle_tick_catchup_dark_red(): """Mirror of dark_green — SELL side.""" fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() tr = _handle_tick(fsm, "dark_red", 1.0, notif, audit, first_accepted=True) assert tr is not None assert tr.next == State.PRIMED_SELL assert len(notif.alerts) == 2 assert notif.alerts[0].direction == "SELL" assert notif.alerts[1].direction == "SELL" # --------------------------------------------------------------------------- # 6. late-start: light_green from IDLE with first_accepted=True # --------------------------------------------------------------------------- def test_handle_tick_late_start(): fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() tr = _handle_tick(fsm, "light_green", 1.0, notif, audit, first_accepted=True) # FSM untouched assert tr is None assert fsm.state == State.IDLE # Exactly one late_start alert assert len(notif.alerts) == 1 assert notif.alerts[0].kind == "late_start" assert notif.alerts[0].direction == "BUY" # Audit has a late_start event assert any(e.get("event") == "late_start" for e in audit.events) def test_handle_tick_late_start_sell(): """Mirror of light_green — light_red triggers SELL late_start.""" fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() tr = _handle_tick(fsm, "light_red", 1.0, notif, audit, first_accepted=True) assert tr is None assert fsm.state == State.IDLE assert len(notif.alerts) == 1 assert notif.alerts[0].kind == "late_start" assert notif.alerts[0].direction == "SELL" # --------------------------------------------------------------------------- # Extra guard: first_accepted=True with turquoise is a normal arm (decision 3A) # --------------------------------------------------------------------------- def test_handle_tick_first_turquoise_no_catchup_label(): fsm = StateMachine(lockout_s=60) notif = FakeNotifier() audit = FakeAudit() tr = _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=True) assert tr is not None assert tr.next == State.ARMED_BUY assert len(notif.alerts) == 1 assert notif.alerts[0].kind == "arm" # 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()