"""Test della diagnosi di guasto sul percorso Deribit (`src/live/venue_probe.py`) e delle due riparazioni che ne dipendono: la finestra scoperta del disaster-SL e l'isolamento per asset. NESSUN TEST TOCCA LA RETE: la sonda pubblica e' iniettabile: `diagnose(..., sonda=...)`. Cosa difendono, in ordine di gravita': 1. **`naked`**: il disaster-SL cancellato e non ripiazzato deve avere uno stato PROPRIO, diverso 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. """ 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) MANUTENZIONE_GREZZA = ('HTTP 503 {"jsonrpc":"2.0","error":' '{"message":"system_maintenance","code":11051}}') # --- sonde finte ----------------------------------------------------------------------------- def _sonda_ok(): return True, 'HTTP 200 {"jsonrpc":"2.0","result":{"version":"1.2.26"}}' def _sonda_manutenzione(): return False, MANUTENZIONE_GREZZA def _sonda_rifiuta(): return False, "HTTP 500 bad gateway" def _sonda_morta(): return None, "ConnectionError: dns" # --------------------------------------------------------------------------- # 1. La diagnosi distingue i tre guasti che vogliono azioni diverse. # --------------------------------------------------------------------------- def test_venue_sano_significa_che_il_guasto_e_nostro(): """Il caso che vale di piu': 4 dei 5 traceback in 63 giorni erano il NOSTRO gateway. Se l'API pubblica risponde, il venue sta bene e il pezzo rotto e' quello riparabile.""" d = diagnose(["BTC: fallback close (HTTPError)"], sonda=_sonda_ok) assert d.verdetto == V_GATEWAY assert d.riparabile_da_noi is True assert d.atteso is False # un gateway rotto non e' MAI atteso assert d.gravita == "🛑" assert "gateway" in d.perche.lower() def test_manutenzione_riconosciuta_dalla_sonda(): d = diagnose(["BTC: fallback close (HTTPError)"], sonda=_sonda_manutenzione) assert d.verdetto == V_MANUTENZIONE assert d.riparabile_da_noi is False # aspettare, non riparare def test_manutenzione_riconosciuta_dagli_errori_gia_raccolti_senza_sondare(): """Il 18/08 il codice 11051 era GIA' negli errori dello strato book, nello stesso minuto, e non arrivava a chi decideva la gravita'. Ora lo si guarda per primo: la sonda non parte nemmeno (qui lo verifichiamo con una sonda che esploderebbe).""" def _sonda_vietata(): raise AssertionError("la sonda non doveva partire: l'11051 era gia' negli errori") d = diagnose(['OnMaintenance: deribit {"error":{"code":11051}}'], sonda=_sonda_vietata) assert d.verdetto == V_MANUTENZIONE def test_venue_giu_non_e_manutenzione(): d = diagnose([], sonda=_sonda_rifiuta) assert d.verdetto == V_VENUE_GIU assert d.atteso is False # non dichiara manutenzione -> resta rumoroso def test_sonda_morta_e_ignoto_non_va_tutto_bene(): """P5: «non vedo» non e' «va tutto bene». Il verdetto deve restare IGNOTO, mai GATEWAY.""" d = diagnose([], sonda=_sonda_morta) assert d.verdetto == V_IGNOTO assert d.riparabile_da_noi is False assert d.atteso is False def test_la_prova_viaggia_sempre_con_il_verdetto(): """Un verdetto senza prova si legge come opinione. La riga deve portare il perche' (P4).""" d = diagnose(["BTC: fallback close (HTTPError)"], sonda=_sonda_ok) assert d.prova # non vuota assert "—" in d.riga() and d.verdetto in d.riga() # --------------------------------------------------------------------------- # 2. P9 — `atteso` declassa, ma solo su EVIDENZA e solo DENTRO lo slot. # --------------------------------------------------------------------------- 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) assert d.verdetto == V_MANUTENZIONE and d.atteso is False assert d.gravita != "ℹ️" 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) assert d.verdetto == V_GATEWAY and d.atteso is False and d.gravita == "🛑" # --------------------------------------------------------------------------- # 3. Gli errori si DERIVANO dal report, non si reinventano (P1). # --------------------------------------------------------------------------- def test_errori_dal_report_raccoglie_le_misure_gia_fatte(): r = {"assets": [{"asset": "BTC", "mark_src": "fallback close (HTTPError)"}, {"asset": "ETH", "mark_src": "mainnet"}], "skh_feed_errors": {"BTC": "OnMaintenance: deribit 11051"}, "pos_error": "posizione non leggibile: api 500"} errori = errori_dal_report(r) assert any("fallback close" in e for e in errori) assert any("11051" in e for e in errori) assert any("api 500" in e for e in errori) # e la catena completa deve concludere MANUTENZIONE senza nemmeno sondare assert diagnose(errori, sonda=_sonda_ok).verdetto == V_MANUTENZIONE def test_report_vuoto_non_esplode(): assert errori_dal_report({}) == []