diff --git a/src/live/venue_probe.py b/src/live/venue_probe.py index 715cd07..38115c1 100644 --- a/src/live/venue_probe.py +++ b/src/live/venue_probe.py @@ -28,19 +28,21 @@ col codice 11051, e' manutenzione; se non risponde affatto, e' il venue ma per u se la sonda stessa non parte, il verdetto e' IGNOTO — **«non vedo» non e' «va tutto bene»** (P5). La sonda parte SOLO quando qualcosa e' gia' fallito: sul percorso sano non costa niente. -⚠️ **LA FINESTRA NON E' UNA PROVA.** `in_release_window()` dice soltanto che l'orologio e' -compatibile con lo slot di release. Non declassa niente da sola: declassa solo cio' che la SONDA -ha gia' riconosciuto come manutenzione, e solo dentro la durata ANNUNCIATA. Una manutenzione che -sfora resta rumorosa (il 18/08 Deribit annuncio' 15-30 minuti e resto' giu' oltre l'ora), e un -guasto vero di martedi' mattina resta rumoroso: altrimenti si sarebbe costruito il silenzio -proprio nell'ora in cui e' piu' probabile che serva. E' la stessa logica di -`venue_watch.MAINT_GRACE_HOURS` — *declassa dentro la tolleranza, RIALZA oltre* — applicata pero' -a una domanda diversa (**questo giro** e' spiegato?) e quindi alla risoluzione del giro orario, -non delle ore consecutive. Sono due politiche vicine e distinte: non accorparle senza misurare. - -REGOLA P9 — *un allarme massimo speso per un evento atteso e' un allarme che non verra' letto il -giorno che e' vero.* E' il motivo per cui `atteso` esiste: declassa la GRAVITA', **non** registra -di meno. L'episodio finisce nel log e nella notifica comunque. +⚠️ **IL RAMO CHE DECLASSAVA E' MORTO, RIMOSSO DA `diagnose()` (#35 B11, 27/09/2026).** +`in_release_window()` diceva se l'orologio cadeva nello slot di release Deribit (martedi' +09:00-09:30 UTC); `diagnose()` la usava per declassare la GRAVITA' di una manutenzione gia' +riconosciuta dalla SONDA (REGOLA P9 — *un allarme massimo speso per un evento atteso e' un +allarme che non verra' letto il giorno che e' vero*). Ma l'UNICO chiamante di `diagnose()` e' +`book_execute.py`, in cron al minuto **:47** di ogni ora (dal 25/08): la finestra 09:00-09:30 non +contiene mai :47, quindi quel ramo non poteva MAI diventare vero — si leggeva come una +declassazione attiva e non lo e' mai stata. Il campo `atteso` RESTA sul verdetto +(`book_execute.py` lo scrive nel dict del report: cambiare il formato per una pulizia e' un costo +che non vale la pena) ma e' hardcoded a False in `diagnose()`. `in_release_window()` e le +costanti `RELEASE_*` restano invece nel modulo: le usa `test_book_cadenza.py`/`test_revisione.py` +per verificare che i cron di book/review evitino lo stesso slot — uno scopo indipendente da +`diagnose()` e ancora valido. Per declassare di nuovo un evento annunciato in `diagnose()` la +strada e' legarlo alla DURATA dell'annuncio (`venue_news`, se e quando esiste), non all'orologio +di un chiamante che non e' mai sincrono con lo slot. """ from __future__ import annotations @@ -53,7 +55,7 @@ import requests from src.live.venue_watch import is_maintenance # P1: la firma 11051 vive LI', non qui # --- verdetti ----------------------------------------------------------------------------- -V_MANUTENZIONE = "VENUE_MANUTENZIONE" # il venue dichiara 11051: atteso se dentro lo slot +V_MANUTENZIONE = "VENUE_MANUTENZIONE" # il venue dichiara 11051 V_VENUE_GIU = "VENUE_GIU" # il venue non risponde, e non dice manutenzione V_GATEWAY = "GATEWAY" # il venue sta bene -> il guasto e' NOSTRO (riparabile) V_IGNOTO = "IGNOTO" # la sonda non e' partita: "non vedo" (P5) @@ -70,8 +72,12 @@ PROBE_TIMEOUT = 8.0 # 30/06/2026 alle 09:00 UTC, "down for around 15-30 minutes"). # ⚠️ M10: *due punti non fanno una tendenza*. Qui i punti sono quattro e c'e' una meccanica # dichiarata dal venue, ma resta una REGOLARITA' OSSERVATA, non un contratto: se Deribit sposta -# lo slot, questa costante mente in silenzio. E' per questo che la finestra non decide da sola — -# decide la sonda, e la finestra al massimo declassa. +# lo slot, questa costante mente in silenzio. +# ⚠️ #35 B11 (27/09/2026): questa finestra NON declassa piu' niente in `diagnose()` (vedi sotto): +# l'unico chiamante gira in cron al minuto :47, che non ci cade mai dentro. La funzione e le +# costanti restano perche' `test_book_cadenza.py`/`test_revisione.py` le usano per un controllo +# diverso e ancora valido: che i cron di `book`/`review` cadano FUORI da questo slot (P1, la +# stessa finestra, non una seconda dichiarata a mano). RELEASE_WEEKDAY = 1 # lunedi'=0 -> 1 = martedi' RELEASE_START_MIN = 9 * 60 # 09:00 UTC RELEASE_LEN_MIN = 30 # durata ANNUNCIATA. Oltre questa la manutenzione torna una notizia. @@ -79,7 +85,8 @@ RELEASE_LEN_MIN = 30 # durata ANNUNCIATA. Oltre questa la manutenzione to @dataclass(frozen=True) class Diagnosi: - """Il verdetto (*cosa*), la spiegazione (*perche'*), e se l'evento era atteso (P9).""" + """Il verdetto (*cosa*) e la spiegazione (*perche'*). `atteso` resta nel formato ma e' sempre + False (#35 B11): l'unico chiamante non cade mai nella finestra che l'avrebbe declassato.""" verdetto: str perche: str atteso: bool @@ -87,7 +94,8 @@ class Diagnosi: @property def gravita(self) -> str: - """Emoji del titolo di notifica. `atteso` declassa, non silenzia (P9).""" + """Emoji del titolo di notifica. `atteso` e' sempre False (#35 B11): ramo morto lasciato + per compatibilita' di formato, non per una declassazione che avviene ancora.""" if self.atteso: return "ℹ️" return {V_GATEWAY: "🛑", V_VENUE_GIU: "🛑", V_MANUTENZIONE: "⚠️"}.get(self.verdetto, "⚠️") @@ -106,8 +114,9 @@ class Diagnosi: def in_release_window(now: datetime | None = None) -> bool: """L'orologio e' dentro lo slot di release Deribit (martedi' 09:00-09:30 UTC)? - PURA e testabile. Non e' una prova di niente da sola: vedi la nota in testa al modulo. - """ + PURA e testabile. Non declassa piu' niente in `diagnose()` (#35 B11): resta per chi verifica + che un cron (book, review) sia schedulato fuori da questo slot (P1: la finestra si deriva da + qui, non si riscrive una seconda volta nel test).""" now = now or datetime.now(timezone.utc) now = now.astimezone(timezone.utc) if now.weekday() != RELEASE_WEEKDAY: @@ -136,7 +145,6 @@ def probe_public(timeout: float = PROBE_TIMEOUT) -> tuple[bool | None, str]: def diagnose(errori_osservati: list[str] | None = None, - now: datetime | None = None, sonda=None) -> Diagnosi: """Classifica un guasto gia' avvenuto. `sonda` e' iniettabile: i test non toccano la rete. @@ -169,7 +177,10 @@ def diagnose(errori_osservati: list[str] | None = None, verdetto = V_VENUE_GIU perche = "l'API pubblica Deribit non serve, e non dichiara manutenzione" - atteso = verdetto == V_MANUTENZIONE and in_release_window(now) + # #35 B11: qui c'era `verdetto == V_MANUTENZIONE and in_release_window(now)`. L'unico + # chiamante (book_execute.py, cron :47) non cade mai nella finestra 09:00-09:30 di martedi': + # il ramo non poteva mai diventare vero. Il campo resta (formato del report), sempre False. + atteso = False prova = "; ".join([*errori[:3], grezzo])[:500] return Diagnosi(verdetto=verdetto, perche=perche, atteso=atteso, prova=prova) diff --git a/tests/test_venue_probe.py b/tests/test_venue_probe.py index eee3d8d..b9758a5 100644 --- a/tests/test_venue_probe.py +++ b/tests/test_venue_probe.py @@ -8,17 +8,17 @@ Cosa difendono, in ordine di gravita': da `place-failed`. Sono due guasti diversi (P5) e solo uno lascia la posizione scoperta. 2. **isolamento per asset**: un guasto su BTC non deve impedire ad ETH di essere protetto. 3. **la diagnosi**: manutenzione, gateway e "non vedo" devono essere distinguibili (P4/P5). - 4. **P9**: `atteso` declassa la gravita' SOLO su evidenza di manutenzione, e SOLO dentro lo slot. + 4. **`atteso` e' sempre False** (#35 B11): il ramo che lo declassava (finestra di release del + martedi') e' stato rimosso perche' l'unico chiamante (cron :47) non ci cadeva mai dentro. """ import sys -from datetime import datetime, timezone from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PROJECT_ROOT)) from src.live.venue_probe import (V_GATEWAY, V_IGNOTO, V_MANUTENZIONE, V_VENUE_GIU, diagnose, - errori_dal_report, in_release_window) + errori_dal_report) MANUTENZIONE_GREZZA = ('HTTP 503 {"jsonrpc":"2.0","error":' '{"message":"system_maintenance","code":11051}}') @@ -94,39 +94,19 @@ def test_la_prova_viaggia_sempre_con_il_verdetto(): # --------------------------------------------------------------------------- -# 2. P9 — `atteso` declassa, ma solo su EVIDENZA e solo DENTRO lo slot. +# 2. `atteso` e' sempre False (#35 B11) — nessun verdetto, manutenzione compresa, si declassa. # --------------------------------------------------------------------------- -def test_finestra_di_release_martedi_nove_utc(): - dentro = datetime(2026, 8, 25, 9, 7, tzinfo=timezone.utc) # martedi', l'episodio vero - assert in_release_window(dentro) is True - assert in_release_window(datetime(2026, 8, 25, 9, 0, tzinfo=timezone.utc)) is True - assert in_release_window(datetime(2026, 8, 25, 9, 29, tzinfo=timezone.utc)) is True - assert in_release_window(datetime(2026, 8, 25, 9, 30, tzinfo=timezone.utc)) is False # sforata - assert in_release_window(datetime(2026, 8, 25, 8, 59, tzinfo=timezone.utc)) is False - assert in_release_window(datetime(2026, 8, 26, 9, 7, tzinfo=timezone.utc)) is False # mercoledi' - - -def test_manutenzione_dentro_lo_slot_e_attesa_e_declassa(): - d = diagnose([], now=datetime(2026, 8, 25, 9, 7, tzinfo=timezone.utc), - sonda=_sonda_manutenzione) - assert d.verdetto == V_MANUTENZIONE and d.atteso is True - assert d.gravita == "ℹ️" # declassato, NON silenziato - assert "ATTESO" in d.riga() - - -def test_manutenzione_che_sfora_lo_slot_torna_rumorosa(): - """Il 18/08 Deribit annuncio' 15-30 minuti e resto' giu' oltre l'ora: alle 10:07 la - manutenzione non era piu' l'evento annunciato, ed e' di nuovo una notizia.""" - d = diagnose([], now=datetime(2026, 8, 18, 10, 7, tzinfo=timezone.utc), - sonda=_sonda_manutenzione) +def test_manutenzione_non_si_declassa_piu_atteso_sempre_falso(): + """Il ramo che declassava (finestra di release del martedi') e' rimosso: una manutenzione + riconosciuta dalla sonda resta comunque `atteso is False` e alla gravita' piena.""" + d = diagnose([], sonda=_sonda_manutenzione) assert d.verdetto == V_MANUTENZIONE and d.atteso is False - assert d.gravita != "ℹ️" + assert d.gravita == "⚠️" + assert "ATTESO" not in d.riga() -def test_un_guasto_vero_di_martedi_mattina_non_viene_declassato(): - """Il rischio della finestra: costruire il silenzio proprio nell'ora in cui serve. Un gateway - rotto dentro lo slot di release deve restare al massimo della gravita'.""" - d = diagnose([], now=datetime(2026, 8, 25, 9, 7, tzinfo=timezone.utc), sonda=_sonda_ok) +def test_un_guasto_vero_non_e_mai_atteso(): + d = diagnose(["BTC: fallback close (HTTPError)"], sonda=_sonda_ok) assert d.verdetto == V_GATEWAY and d.atteso is False and d.gravita == "🛑"