allarmi: il marcatore "gia' detto" si scrive DOPO l'invio, non prima
Debito §5.2 chiuso su decisione dell'operatore. `run_once` salvava lo stato coi marcatori `alerted` gia' a True e l'invio lo faceva il chiamante DOPO: col 6,9% di invii falliti misurato (2 su 29), un 🚨 perso restava perso per l'EPISODIO INTERO — l'ora dopo lo stato diceva "gia' detto" e usciva WATCH/MUTO. Gli episodi storici durano 200-2.324 ore, quindi il buco non era teorico. - `run_once(state_path, sender=None)`: il sender e' INIETTATO, non importato — e' cio' che tiene la funzione testabile senza rete d'uscita, che era la ragione del disegno precedente. Senza sender il comportamento resta quello di prima e il report lo DICE (`invio`), invece di lasciar credere che qualcosa sia partito. - Su invio fallito si disfano SOLO i marcatori "gia' detto", non le misure: · asset in ALERT -> alerted=False, l'ora dopo ri-allerta; · lock MAINT/ALERT -> alerted_soft/hard=False ma le ORE restano a correre, cosi' una manutenzione che sfora la grazia sale ad ALERT anche col trasporto giu' (disfare anche le ore congelerebbe l'escalation proprio mentre non si riesce a parlare); · lock RIENTRATO -> si ripristina l'intero LockState, perche' il rientro si annuncia una volta sola e senza le ore non ci sarebbe piu' niente da dire. - `notify(..., tentativi=)`: il retry esisteva in `send` e non arrivava qui. venue_watch ora manda con 3 tentativi. - L'esito dell'invio finisce nel log del cron invece di sparire. 5 test nuovi. Il primo e' quello che conta — dopo un invio fallito, l'ora dopo ri-allerta — col suo controllo positivo (un invio riuscito consuma l'allarme UNA volta sola), senza il quale "ri-allerta sempre" passerebbe. Suite: 782 passati, 2 falliti (i due del gate GTAA, non toccati qui). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
from src.live.venue_watch import (BLIND_ALERT_HOURS, PERSIST_HOURS, # noqa: E402
|
||||
THRESHOLD_BPS, AssetState, WatchState,
|
||||
dislocation_bps, load_state, save_state, step)
|
||||
import src.live.venue_watch as VW # noqa: E402 — i test sul trasporto patchano `observe`
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
@@ -416,3 +417,85 @@ def test_una_sola_ora_BLIND_spezza_lo_streak_e_impedisce_l_allarme():
|
||||
st2, lvl2 = step(st2, bps)
|
||||
assert lvl2 != "ALERT"
|
||||
assert st2.streak_hours < PERSIST_HOURS, "l'ora BLIND deve azzerare lo streak"
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# IL TRASPORTO — un 🚨 perso non deve costare l'EPISODIO INTERO
|
||||
# =============================================================================================
|
||||
# Fino al 2026-08-28 `run_once` salvava lo stato coi marcatori `alerted` gia' a True e l'invio
|
||||
# lo faceva il chiamante DOPO. Col 6,9% di invii falliti misurato (2 su 29), un allarme perso
|
||||
# restava perso finche' durava l'episodio — e gli episodi storici durano 200-2.324 ore.
|
||||
# Questi test guardano l'unica cosa che conta: dopo un invio FALLITO, l'ora dopo ri-allerta?
|
||||
|
||||
def _obs_finto(monkeypatch, bps, locked=False, maint=False):
|
||||
def fake():
|
||||
return {"ts": 1, "platform_locked": locked, "locked_raw": str(locked).lower(),
|
||||
"maintenance": maint,
|
||||
"assets": {a: dict(deribit=1.0, n_refs=3, ref_spread_bps=1.0, bps=bps)
|
||||
for a in VW.ASSETS},
|
||||
"deribit_errors": []}
|
||||
monkeypatch.setattr(VW, "observe", fake)
|
||||
|
||||
|
||||
def _porta_ad_alert(monkeypatch, tmp_path, sender):
|
||||
"""Fa salire lo streak fino all'ALERT e ritorna il report del giro che allerta."""
|
||||
sp = tmp_path / "vw.json"
|
||||
_obs_finto(monkeypatch, bps=VW.THRESHOLD_BPS * 3)
|
||||
rep = None
|
||||
for _ in range(VW.PERSIST_HOURS):
|
||||
rep = VW.run_once(state_path=sp, sender=sender)
|
||||
return sp, rep
|
||||
|
||||
|
||||
def test_invio_fallito_NON_consuma_l_allarme(monkeypatch, tmp_path):
|
||||
ko = lambda rep: (False, "ConnectionError: rete giu'")
|
||||
sp, rep = _porta_ad_alert(monkeypatch, tmp_path, ko)
|
||||
assert any(l == "ALERT" for l in rep["levels"].values())
|
||||
assert rep["invio"].startswith("FALLITO")
|
||||
# il marcatore 'gia' detto' NON deve essere sul disco
|
||||
st = VW.load_state(sp)
|
||||
assert all(not s.alerted for s in st.assets.values()), "un invio fallito ha consumato l'allarme"
|
||||
# l'ora dopo, stessa dislocazione: deve RI-allertare
|
||||
rep2 = VW.run_once(state_path=sp, sender=ko)
|
||||
assert any(l == "ALERT" for l in rep2["levels"].values()), "l'allarme non e' stato ritentato"
|
||||
|
||||
|
||||
def test_invio_riuscito_consuma_l_allarme_una_volta_sola(monkeypatch, tmp_path):
|
||||
"""Il controllo positivo: senza questo, 'ri-allerta sempre' passerebbe il test di sopra."""
|
||||
ok = lambda rep: (True, "ok")
|
||||
sp, rep = _porta_ad_alert(monkeypatch, tmp_path, ok)
|
||||
assert any(l == "ALERT" for l in rep["levels"].values())
|
||||
assert rep["invio"] == "inviato"
|
||||
rep2 = VW.run_once(state_path=sp, sender=ok)
|
||||
assert not any(l == "ALERT" for l in rep2["levels"].values()), "allarme ripetuto ogni ora"
|
||||
assert all(l == "WATCH" for l in rep2["levels"].values())
|
||||
|
||||
|
||||
def test_lock_maint_fallito_ritenta_ma_le_ORE_continuano_a_correre(monkeypatch, tmp_path):
|
||||
"""Disfare anche le ore congelerebbe l'escalation proprio mentre non si riesce a parlare:
|
||||
una manutenzione che sfora la grazia deve salire ad ALERT anche col trasporto giu'."""
|
||||
sp = tmp_path / "vw.json"
|
||||
ko = lambda rep: (False, "rete giu'")
|
||||
_obs_finto(monkeypatch, bps=0.0, locked=True, maint=True)
|
||||
livelli = [VW.run_once(state_path=sp, sender=ko)["lock_level"]
|
||||
for _ in range(VW.MAINT_GRACE_HOURS + 1)]
|
||||
assert livelli[0] == "MAINT"
|
||||
assert livelli[1] == "MAINT", "il MAINT perso non e' stato ritentato"
|
||||
assert livelli[-1] == "ALERT", "le ore non sono avanzate: l'escalation si e' congelata"
|
||||
|
||||
|
||||
def test_rientro_perso_si_riannuncia(monkeypatch, tmp_path):
|
||||
"""Il rientro si dice UNA volta e azzera le ore: senza ripristinare lo stato precedente
|
||||
non ci sarebbe piu' un rientro da annunciare."""
|
||||
sp = tmp_path / "vw.json"
|
||||
_obs_finto(monkeypatch, bps=0.0, locked=True, maint=False)
|
||||
VW.run_once(state_path=sp, sender=lambda r: (True, "ok"))
|
||||
_obs_finto(monkeypatch, bps=0.0, locked=False)
|
||||
assert VW.run_once(state_path=sp, sender=lambda r: (False, "giu'"))["lock_level"] == "RIENTRATO"
|
||||
assert VW.run_once(state_path=sp, sender=lambda r: (True, "ok"))["lock_level"] == "RIENTRATO"
|
||||
|
||||
|
||||
def test_senza_sender_il_report_lo_DICE(monkeypatch, tmp_path):
|
||||
"""'non tentato' non e' 'inviato': il silenzio non deve leggersi come un invio riuscito."""
|
||||
sp, rep = _porta_ad_alert(monkeypatch, tmp_path, None)
|
||||
assert "non tentato" in rep["invio"]
|
||||
|
||||
Reference in New Issue
Block a user