feat(run): screenshot attach, Telegram ok:false fix, post-FIRE catchup guard
Three bundled fixes on the dispatch + FSM + notifier triangle:
1. Telegram silent-success bug: parse JSON body after 200 OK, raise on
ok:false so FanoutNotifier retries + DLQs + stats surface the failure.
Previously Discord succeeded while Telegram silently dropped.
2. Per-kind screenshot attach: new AlertsCfg dataclass with per-kind toggle
(late_start, catchup, arm, prime, trigger). _save_annotated_frame helper
extracted from inline FIRE block, threaded via Snapshot closure into
_handle_tick. Failures audit-logged, never silent.
3. Post-FIRE catchup regression (d7305fb): residual dark_green/dark_red dots
after a FIRE cycle look like startup-catchup from (color, state) alone.
New fsm.fired_in_session(direction) gate suppresses synth-arm after a
cycle already fired in that direction. Opposite direction unaffected.
Also: queue-overflow on_drop audit callback, periodic + shutdown heartbeat
stats per-backend, config back-compat (bool or dict for attach_screenshots).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,58 @@ def test_refresh_arm_ts() -> None:
|
||||
assert t2.arm_ts == 9.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 11. fired_in_session — public API for catchup suppression after fire
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_fired_in_session_fresh_fsm() -> None:
|
||||
"""Fresh FSM — neither direction has fired."""
|
||||
sm = StateMachine()
|
||||
assert sm.fired_in_session("BUY") is False
|
||||
assert sm.fired_in_session("SELL") is False
|
||||
|
||||
|
||||
def test_fired_in_session_after_buy_fire() -> None:
|
||||
"""After a BUY fire: BUY=True, SELL=False (direction-scoped)."""
|
||||
sm = StateMachine(lockout_s=240)
|
||||
sm.feed("turquoise", 1.0)
|
||||
sm.feed("dark_green", 2.0)
|
||||
t = sm.feed("light_green", 3.0)
|
||||
assert t.reason == "fire"
|
||||
|
||||
assert sm.fired_in_session("BUY") is True
|
||||
assert sm.fired_in_session("SELL") is False
|
||||
|
||||
|
||||
def test_fired_in_session_after_sell_fire() -> None:
|
||||
"""Mirror — after SELL fire: SELL=True, BUY=False."""
|
||||
sm = StateMachine(lockout_s=240)
|
||||
sm.feed("yellow", 1.0)
|
||||
sm.feed("dark_red", 2.0)
|
||||
t = sm.feed("light_red", 3.0)
|
||||
assert t.reason == "fire"
|
||||
|
||||
assert sm.fired_in_session("SELL") is True
|
||||
assert sm.fired_in_session("BUY") is False
|
||||
|
||||
|
||||
def test_fired_in_session_both_directions() -> None:
|
||||
"""Fire both directions — both True."""
|
||||
sm = StateMachine(lockout_s=240)
|
||||
# BUY cycle
|
||||
sm.feed("turquoise", 1.0)
|
||||
sm.feed("dark_green", 2.0)
|
||||
sm.feed("light_green", 3.0)
|
||||
# SELL cycle
|
||||
sm.feed("yellow", 100.0)
|
||||
sm.feed("dark_red", 101.0)
|
||||
sm.feed("light_red", 102.0)
|
||||
|
||||
assert sm.fired_in_session("BUY") is True
|
||||
assert sm.fired_in_session("SELL") is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 11. exhaustive — parameterize over every (state, color) pair
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user