Files
PythagorasGoal/tests/test_venue_probe.py
T

131 lines
5.9 KiB
Python

"""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. **`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 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)
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 <html>bad gateway</html>"
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. `atteso` e' sempre False (#35 B11) — nessun verdetto, manutenzione compresa, si declassa.
# ---------------------------------------------------------------------------
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 "ATTESO" not in d.riga()
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 == "🛑"
# ---------------------------------------------------------------------------
# 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({}) == []