feat(multi-chart): refactor _run_multi_tick + fix alert spam pe oscilație strip

Bug critic: _strips_match(tol=10) trip pe pulsații naturale de lățime ~18px între
ticks (ex. 792↔810px). Fiecare trip → _commit_layout_change → reset FSM + alert
Telegram + scheduler stop. Logul 2026-05-04.jsonl arăta 576 evenimente
layout_change/zi, plus prime alerts repetate la dark_red/dark_green (FSM resetat
înghite lockout-ul) și sincronizare cross-chart pe ambele FSM-uri simultan.

Fix:
- main.py:1511 — gate doar pe count change (len(new) != len(current));
  count stabil → silent update sub_roi indiferent de jitter
- main.py:1438 — silent=True pe alert layout_change (Telegram fără sunet)
- 2 teste regresie noi: width oscillation 792↔810 + silent assertion
- 2 teste async reparate: bootstrap _detect_strips_for_ctx pentru ScriptedDetector
  (regresie după ce _run_multi_tick a devenit unica cale de detecție)

Plus refactor multi-chart pre-existent: layout.py modul nou, _detect_strips_for_ctx,
ChartState per-chart FSM/Detector, ROI per-strip pe screenshots, scripts/diag_*.

Verificat: 292 passed, 2 skipped în 10s.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 17:59:18 +03:00
parent 8a1be979fe
commit c950a5a699
14 changed files with 1952 additions and 255 deletions

View File

@@ -7,7 +7,7 @@ from pathlib import Path
import pytest
from atm.notifier import Alert
from atm.notifier import Alert, _alert_prefix
from atm.notifier.fanout import FanoutNotifier
@@ -358,3 +358,32 @@ def test_fanout_on_drop_exception_swallowed(tmp_path: Path) -> None:
s = fan.stats()
# Some alerts still went through
assert s["slow"]["sent"] > 0 or s["slow"]["dropped"] > 0
# ---------------------------------------------------------------------------
# Alert.chart_id + _alert_prefix
# ---------------------------------------------------------------------------
def test_alert_chart_id_default() -> None:
assert Alert(kind="arm", title="t", body="b").chart_id == ""
def test_alert_chart_id_set() -> None:
assert Alert(kind="arm", title="t", body="b", chart_id="left").chart_id == "left"
def test_alert_prefix_empty() -> None:
assert _alert_prefix("") == ""
def test_alert_prefix_left() -> None:
assert _alert_prefix("left") == "[stânga] "
def test_alert_prefix_right() -> None:
assert _alert_prefix("right") == "[dreapta] "
def test_alert_prefix_chart_n() -> None:
assert _alert_prefix("chart_0") == "[chart 1] "
assert _alert_prefix("chart_1") == "[chart 2] "