merge lavoro/goldrake-u8 (secondo giro 27/09)
This commit is contained in:
@@ -35,10 +35,16 @@ mkdir -p logs
|
|||||||
# partire anche quando book_execute fallisce per la stessa ragione. Non blocca (exit 2
|
# partire anche quando book_execute fallisce per la stessa ragione. Non blocca (exit 2
|
||||||
# = allarme inviato); l'azione a un allarme e' manuale, vedi src/live/venue_watch.py.
|
# = allarme inviato); l'azione a un allarme e' manuale, vedi src/live/venue_watch.py.
|
||||||
uv run python scripts/live/venue_watch.py --quiet || true
|
uv run python scripts/live/venue_watch.py --quiet || true
|
||||||
uv run python scripts/live/book_execute.py --execute
|
# LOCK SUL CONTO (#36): cuscino_watch (cron_cuscino.sh, :53) puo' mandare ordini spot USDE
|
||||||
|
# sullo stesso conto — separati solo dal tempo, giri fino a 659s misurati. `flock -n` (mai
|
||||||
|
# `-w`, un SIGTERM a meta' ordine perde la riga di log): se cuscino_watch tiene il lock questo
|
||||||
|
# giro si SALTA (rc 75) invece di sovrapporsi. Niente kill/timeout sul comando.
|
||||||
|
flock -n -E 75 logs/.conto.lock uv run python scripts/live/book_execute.py --execute; rc=$?
|
||||||
|
[ $rc -eq 75 ] && echo " book: SALTATO — conto occupato da un altro giro (logs/.conto.lock)"
|
||||||
# Libro di bordo: materializza in data/live/trades.db (che il backup COPRE) l'ora vera dei
|
# Libro di bordo: materializza in data/live/trades.db (che il backup COPRE) l'ora vera dei
|
||||||
# fill, che altrimenti vive solo in questo log — gitignored, fuori dal backup e ruotabile.
|
# fill, che altrimenti vive solo in questo log — gitignored, fuori dal backup e ruotabile.
|
||||||
# Va DOPO book_execute: dentro lo stesso blocco vede le righe appena scritte.
|
# Va DOPO book_execute: dentro lo stesso blocco vede le righe appena scritte. Fuori dal lock:
|
||||||
|
# e' sola lettura, non tocca il conto.
|
||||||
uv run python scripts/live/trades_db.py --sync --quiet || true
|
uv run python scripts/live/trades_db.py --sync --quiet || true
|
||||||
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
} >> logs/cron_book.log 2>&1
|
} >> logs/cron_book.log 2>&1
|
||||||
|
|||||||
@@ -5,11 +5,19 @@
|
|||||||
# PUO' INVIARE ORDINI: sotto zero di slack lancia usde_convert --esegui (vende USDE), con le
|
# PUO' INVIARE ORDINI: sotto zero di slack lancia usde_convert --esegui (vende USDE), con le
|
||||||
# guardie dichiarate nel docstring di scripts/live/cuscino_watch.py; lo interruttore e' lo
|
# guardie dichiarate nel docstring di scripts/live/cuscino_watch.py; lo interruttore e' lo
|
||||||
# stesso del libro (execution_enabled in config/live.json).
|
# stesso del libro (execution_enabled in config/live.json).
|
||||||
|
# LOCK SUL CONTO (#36): cuscino_watch e book_execute (cron_book.sh, :47) sono separati solo dal
|
||||||
|
# tempo e possono mandare ordini sullo stesso conto — giri fino a 659s sono stati misurati.
|
||||||
|
# `flock -n` (mai `-w`: un'attesa farebbe scivolare il giro orario) su logs/.conto.lock: se
|
||||||
|
# book_execute lo tiene, questo giro si SALTA (rc 75) invece di sovrapporsi.
|
||||||
export PATH="/home/adriano/.local/bin:$PATH"
|
export PATH="/home/adriano/.local/bin:$PATH"
|
||||||
cd /opt/docker/PythagorasGoal || exit 1
|
cd /opt/docker/PythagorasGoal || exit 1
|
||||||
mkdir -p logs
|
mkdir -p logs
|
||||||
{
|
{
|
||||||
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_cuscino ====="
|
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_cuscino ====="
|
||||||
uv run python scripts/live/cuscino_watch.py --quiet || true
|
flock -n -E 75 logs/.conto.lock uv run python scripts/live/cuscino_watch.py --quiet; rc=$?
|
||||||
|
[ $rc -eq 75 ] && echo " cuscino: SALTATO — conto occupato (logs/.conto.lock)"
|
||||||
|
# (#30) giro orario dei sorveglianti a max_age<=6h: notifica solo il NUOVO, marcatore in
|
||||||
|
# data/live/monitor_health_detto.json. Fuori dal lock: e' sola lettura, non tocca il conto.
|
||||||
|
uv run python scripts/live/monitor_health.py --quiet --solo-orari || true
|
||||||
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
} >> logs/cron_cuscino.log 2>&1
|
} >> logs/cron_cuscino.log 2>&1
|
||||||
|
|||||||
@@ -10,20 +10,91 @@ monitor e la soglia di copertura 0.80 (la stessa del veto d'integrita' di DVOLSP
|
|||||||
|
|
||||||
uv run python scripts/live/monitor_health.py # report
|
uv run python scripts/live/monitor_health.py # report
|
||||||
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
|
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
|
||||||
|
uv run python scripts/live/monitor_health.py --solo-orari # (#30) solo i sorveglianti a
|
||||||
|
# max_age<=6h (cuscino_watch, venue_watch, collect_chain), notifica solo il NUOVO
|
||||||
|
# rispetto a data/live/monitor_health_detto.json — pensato per girare ogni ora
|
||||||
|
# (cron_cuscino.sh), non per sostituire il giro giornaliero completo.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
from src.live.monitor_health import MIN_COVERAGE, alerts_from, check_all # noqa: E402
|
from src.live.monitor_health import ( # noqa: E402
|
||||||
|
MIN_COVERAGE,
|
||||||
|
alert_nuovi,
|
||||||
|
alerts_from,
|
||||||
|
check_all,
|
||||||
|
marcatore_aggiornato,
|
||||||
|
monitors_orari,
|
||||||
|
)
|
||||||
from src.live.notifier import notify # noqa: E402
|
from src.live.notifier import notify # noqa: E402
|
||||||
|
|
||||||
|
# (#30) marcatore "gia' detto" del giro orario: {"detti": [<chiave alert>], "ts": ...}. Vive fuori
|
||||||
|
# da src/ perche' e' I/O, non parte della misura pura.
|
||||||
|
DETTO = ROOT / "data" / "live" / "monitor_health_detto.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _carica_detti(path: Path = DETTO) -> set[str]:
|
||||||
|
if not path.exists():
|
||||||
|
return set()
|
||||||
|
try:
|
||||||
|
return set(json.loads(path.read_text()).get("detti", []))
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def _salva_detti(detti: set[str], path: Path = DETTO) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(
|
||||||
|
{"detti": sorted(detti), "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")},
|
||||||
|
indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main_orari() -> int:
|
||||||
|
"""(#30) Giro orario: solo i sorveglianti a max_age<=6h, notifica solo il NUOVO rispetto al
|
||||||
|
marcatore. Exit 2 se c'e' un alert (anche gia' detto), 0 altrimenti — il giro giornaliero
|
||||||
|
resta l'unico posto dove "nessun alert nuovo" vuol dire davvero "tutto OK"."""
|
||||||
|
quiet = "--quiet" in sys.argv
|
||||||
|
rows = check_all(ROOT, monitors=monitors_orari())
|
||||||
|
alerts = alerts_from(rows)
|
||||||
|
detti = _carica_detti(DETTO) # DETTO letto qui, non nel default del parametro (monkeypatch)
|
||||||
|
nuovi = alert_nuovi(rows, detti)
|
||||||
|
aggiornato = marcatore_aggiornato(rows, detti)
|
||||||
|
|
||||||
|
if not quiet or alerts:
|
||||||
|
print("=" * 92)
|
||||||
|
print(" MONITOR HEALTH — giro orario (sorveglianti con max_age <= 6h)")
|
||||||
|
print("=" * 92)
|
||||||
|
for r in rows:
|
||||||
|
cov = f"{r['coverage']:.0%}" if r["coverage"] is not None else "-"
|
||||||
|
age = f"{r['age_h']:.0f}h" if r["age_h"] is not None else "-"
|
||||||
|
print(f" {r['name']:<20}{r['status']:>10}{age:>13}{cov:>10}")
|
||||||
|
|
||||||
|
inviato = True
|
||||||
|
if nuovi:
|
||||||
|
msgs = alerts_from(nuovi)
|
||||||
|
inviato = notify("🩺 MONITOR HEALTH — un sorvegliante orario non sta registrando",
|
||||||
|
{f"alert {i+1}": m for i, m in enumerate(msgs)})
|
||||||
|
for m in msgs:
|
||||||
|
print(f" ALERT (nuovo): {m}")
|
||||||
|
if not inviato:
|
||||||
|
print(" ⚠️ invio FALLITO — marcatore NON aggiornato: il prossimo giro ritenta")
|
||||||
|
if not nuovi or inviato:
|
||||||
|
_salva_detti(aggiornato, DETTO)
|
||||||
|
|
||||||
|
return 2 if alerts else 0
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
if "--solo-orari" in sys.argv:
|
||||||
|
return main_orari()
|
||||||
|
|
||||||
rows = check_all(ROOT)
|
rows = check_all(ROOT)
|
||||||
alerts = alerts_from(rows)
|
alerts = alerts_from(rows)
|
||||||
|
|
||||||
@@ -55,15 +126,17 @@ def main() -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
USO = """uso: monitor_health.py [--quiet]
|
USO = """uso: monitor_health.py [--quiet] [--solo-orari]
|
||||||
|
|
||||||
monitor_health.py — i forward-monitor stanno registrando? Riporta e allerta, non tocca nulla.
|
monitor_health.py — i forward-monitor stanno registrando? Riporta e allerta, non tocca nulla.
|
||||||
uv run python scripts/live/monitor_health.py # report
|
uv run python scripts/live/monitor_health.py # report
|
||||||
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
|
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
|
||||||
|
uv run python scripts/live/monitor_health.py --solo-orari # (#30) solo i sorveglianti a
|
||||||
|
max_age<=6h, notifica solo il NUOVO rispetto al marcatore
|
||||||
|
|
||||||
Dettaglio nel docstring in testa al file."""
|
Dettaglio nel docstring in testa al file."""
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from src.live.cli import valida
|
from src.live.cli import valida
|
||||||
valida("monitor_health.py", USO, flag=("--quiet",), con_valore=())
|
valida("monitor_health.py", USO, flag=("--quiet", "--solo-orari"), con_valore=())
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -61,6 +61,18 @@ reazione sbagliata al sintomo giusto: consuma i tentativi senza toccare la causa
|
|||||||
|
|
||||||
uv run python scripts/live/usde_convert.py --quota 0.64 # piano, nessun ordine (0,64 = bersaglio di cuscino_watch)
|
uv run python scripts/live/usde_convert.py --quota 0.64 # piano, nessun ordine (0,64 = bersaglio di cuscino_watch)
|
||||||
uv run python scripts/live/usde_convert.py --quota 0.64 --esegui # manda l'ordine
|
uv run python scripts/live/usde_convert.py --quota 0.64 --esegui # manda l'ordine
|
||||||
|
|
||||||
|
LOCK SUL CONTO (#36). Un lancio a MANO con --esegui condivide il conto con cuscino_watch e
|
||||||
|
book_execute (cron_cuscino.sh/cron_book.sh, entrambi dentro `flock -n logs/.conto.lock`): senza
|
||||||
|
lo stesso lock un ordine a mano nello stesso minuto del cron puo' convertire due volte verso
|
||||||
|
bersagli diversi (limite D5 gia' dichiarato sopra). Si lancia quindi con:
|
||||||
|
|
||||||
|
flock logs/.conto.lock uv run python scripts/live/usde_convert.py --quota 0.64 --esegui
|
||||||
|
|
||||||
|
⚠️ Qui NON senza `-n`: un lancio a mano puo' aspettare il turno invece di saltare. Questo script
|
||||||
|
stesso NON prende il lock (vedi cuscino_watch.py): se lo facesse e girasse come figlio di
|
||||||
|
cuscino_watch (che il lock lo tiene gia'), un secondo `flock -n` lo troverebbe occupato e
|
||||||
|
fallirebbe subito, per costruzione, ogni singola volta.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ import numpy as np
|
|||||||
MIN_COVERAGE = 0.80 # sotto: la serie ha buchi -> non ci si decide sopra un gate
|
MIN_COVERAGE = 0.80 # sotto: la serie ha buchi -> non ci si decide sopra un gate
|
||||||
MIN_BARS_PER_JUDGE = 2 # con una barra sola non esiste ne' coda ne' copertura
|
MIN_BARS_PER_JUDGE = 2 # con una barra sola non esiste ne' coda ne' copertura
|
||||||
GRAZIA_PREMATURO_MIN = 5.0 # tolleranza fra chiusura della barra e mtime del file (clock/IO)
|
GRAZIA_PREMATURO_MIN = 5.0 # tolleranza fra chiusura della barra e mtime del file (clock/IO)
|
||||||
|
SOGLIA_ORARI_H = 6.0 # (#30) max_age_h <= questa soglia = "sorvegliante orario": i tre
|
||||||
|
# con max_age 3h (cuscino_watch, venue_watch, collect_chain) oggi,
|
||||||
|
# derivato dalla tabella e non da una lista di nomi cablata
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -244,3 +247,46 @@ def alerts_from(rows: list[dict]) -> list[str]:
|
|||||||
msg += f" | METTE A RISCHIO il gate {r['gate']}"
|
msg += f" | METTE A RISCHIO il gate {r['gate']}"
|
||||||
out.append(msg)
|
out.append(msg)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# GIRO ORARIO (#30). Il giro giornaliero (cron_daily) resta com'e': report completo, avviso
|
||||||
|
# sempre. I sorveglianti a max_age <= 6h (oggi cuscino_watch, venue_watch, collect_chain) si
|
||||||
|
# scoprono fermi fino a 24h dopo se nessuno li guarda fuori da quel giro: qui si guardano ogni
|
||||||
|
# ora, ma si notifica solo cio' che e' NUOVO rispetto all'ultimo avviso (niente Telegram a
|
||||||
|
# raffica sullo stesso guasto fermo). Tutto PURO: il marcatore lo legge/scrive lo script.
|
||||||
|
# ===========================================================================
|
||||||
|
def monitors_orari(monitors: tuple[MonitorSpec, ...] = MONITORS,
|
||||||
|
soglia_h: float = SOGLIA_ORARI_H) -> tuple[MonitorSpec, ...]:
|
||||||
|
"""I monitor con cadenza di controllo ORARIA: max_age_h <= soglia_h. Derivato dalla tabella
|
||||||
|
`MONITORS`, non da una lista di nomi cablata: aggiungere un monitor orario lo include qui
|
||||||
|
senza toccare questa funzione."""
|
||||||
|
return tuple(m for m in monitors if m.max_age_h <= soglia_h)
|
||||||
|
|
||||||
|
|
||||||
|
def chiave_alert(row: dict) -> str:
|
||||||
|
"""Chiave stabile di un alert per il marcatore 'gia' detto': nome + stato, NON il testo
|
||||||
|
(`why` contiene l'eta' dell'ultima barra, che cambia a ogni giro anche se il guasto e' lo
|
||||||
|
stesso)."""
|
||||||
|
return f"{row['name']}:{row['status']}"
|
||||||
|
|
||||||
|
|
||||||
|
def alert_nuovi(rows: list[dict], detti: set[str]) -> list[dict]:
|
||||||
|
"""Le righe-alert (non OK, non NUOVO — vedi `alerts_from`) la cui chiave NON e' nel
|
||||||
|
marcatore `detti`: quelle davvero nuove, le sole da notificare."""
|
||||||
|
return [r for r in rows if r["status"] not in ("OK", "NUOVO") and chiave_alert(r) not in detti]
|
||||||
|
|
||||||
|
|
||||||
|
def marcatore_aggiornato(rows: list[dict], detti: set[str]) -> set[str]:
|
||||||
|
"""Il marcatore dopo questo giro: le chiavi dei monitor ancora in alert restano/si aggiungono,
|
||||||
|
quelle dei monitor tornati OK si TOLGONO — cosi' un nuovo guasto sullo stesso monitor riparte
|
||||||
|
con un nuovo avviso invece di restare 'gia' detto' per sempre. Non scrive nulla: lo fa lo
|
||||||
|
script, e solo se la notifica e' partita (stessa regola di fee_watch #29)."""
|
||||||
|
out = set(detti)
|
||||||
|
for r in rows:
|
||||||
|
chiavi_monitor = {k for k in out if k.startswith(f"{r['name']}:")}
|
||||||
|
if r["status"] == "OK":
|
||||||
|
out -= chiavi_monitor
|
||||||
|
elif r["status"] != "NUOVO":
|
||||||
|
out.add(chiave_alert(r))
|
||||||
|
return out
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"""Test del giro ORARIO di monitor_health (#30).
|
||||||
|
|
||||||
|
I sorveglianti a max_age<=6h (cuscino_watch, venue_watch, collect_chain) girano alle 00:30 col
|
||||||
|
report giornaliero e basta: un guasto si scopre fino a 24h dopo. Qui si guarda ogni ora, ma si
|
||||||
|
notifica solo cio' che e' NUOVO rispetto al marcatore "gia' detto" — altrimenti un monitor fermo
|
||||||
|
manderebbe lo stesso Telegram ogni ora per giorni.
|
||||||
|
|
||||||
|
Le funzioni PURE (filtro per max_age, chiave dell'alert, chi e' nuovo, il marcatore aggiornato)
|
||||||
|
stanno in src/live/monitor_health.py e si testano su righe finte, senza toccare disco o rete.
|
||||||
|
L'orchestrazione (leggere/scrivere il marcatore, chiamare notify) sta nello script e si testa qui
|
||||||
|
sotto con monkeypatch — NIENTE Telegram vero."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
||||||
|
|
||||||
|
from src.live.monitor_health import ( # noqa: E402
|
||||||
|
MonitorSpec,
|
||||||
|
alert_nuovi,
|
||||||
|
chiave_alert,
|
||||||
|
marcatore_aggiornato,
|
||||||
|
monitors_orari,
|
||||||
|
)
|
||||||
|
|
||||||
|
import monitor_health as MH # noqa: E402 (scripts/live/monitor_health.py)
|
||||||
|
|
||||||
|
# righe finte, nella forma che assess()/check_all() restituiscono davvero
|
||||||
|
ROW_FERMO = dict(name="venue_watch", gate="tripwire di venue orario", n_bars=3, age_h=4.0,
|
||||||
|
coverage=0.9, expected=3, status="FERMO", why="ultima barra 4h fa (limite 3h)")
|
||||||
|
ROW_OK = dict(name="venue_watch", gate="tripwire di venue orario", n_bars=10, age_h=0.2,
|
||||||
|
coverage=1.0, expected=10, status="OK", why="")
|
||||||
|
ROW_NUOVO = dict(name="venue_watch", gate=None, n_bars=1, age_h=0.1, coverage=None,
|
||||||
|
expected=None, status="NUOVO", why="1 barra: non c'e' abbastanza serie")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# funzioni PURE — filtro per max_age (src/live/monitor_health.py)
|
||||||
|
# ===========================================================================
|
||||||
|
def test_il_filtro_deriva_dalla_tabella_non_da_una_lista_di_nomi():
|
||||||
|
specs = (
|
||||||
|
MonitorSpec("a", "a", "runs.jsonl", 1.0, max_age_h=3.0),
|
||||||
|
MonitorSpec("b", "b", "runs.jsonl", 1.0, max_age_h=6.0), # al bordo: dentro
|
||||||
|
MonitorSpec("c", "c", "runs.jsonl", 24.0, max_age_h=30.0),
|
||||||
|
)
|
||||||
|
assert {m.name for m in monitors_orari(specs)} == {"a", "b"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_tre_orari_di_oggi_sono_quelli_noti():
|
||||||
|
"""Se la tabella cambia questo test lo dice: e' l'ancora fra il registro e il flag."""
|
||||||
|
assert {m.name for m in monitors_orari()} == {"collect_chain", "cuscino_watch", "venue_watch"}
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# funzioni PURE — chiave, nuovi, marcatore
|
||||||
|
# ===========================================================================
|
||||||
|
def test_chiave_e_nome_piu_stato_non_il_testo():
|
||||||
|
"""Il testo (`why`) contiene l'eta', che cambia ogni ora anche se il guasto e' lo stesso."""
|
||||||
|
assert chiave_alert(ROW_FERMO) == "venue_watch:FERMO"
|
||||||
|
piu_vecchio = {**ROW_FERMO, "age_h": 9.0, "why": "ultima barra 9h fa (limite 3h)"}
|
||||||
|
assert chiave_alert(piu_vecchio) == chiave_alert(ROW_FERMO)
|
||||||
|
|
||||||
|
|
||||||
|
def test_alert_nuovo_se_la_chiave_non_e_nel_marcatore():
|
||||||
|
assert alert_nuovi([ROW_FERMO], set()) == [ROW_FERMO]
|
||||||
|
|
||||||
|
|
||||||
|
def test_lo_stesso_alert_al_secondo_giro_non_e_piu_nuovo():
|
||||||
|
assert alert_nuovi([ROW_FERMO], {"venue_watch:FERMO"}) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_nuovo_non_e_un_alert_da_notificare():
|
||||||
|
assert alert_nuovi([ROW_NUOVO], set()) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_marcatore_aggiunge_la_chiave_di_un_alert():
|
||||||
|
assert marcatore_aggiornato([ROW_FERMO], set()) == {"venue_watch:FERMO"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_ritorno_a_ok_toglie_la_chiave_dal_marcatore():
|
||||||
|
"""Cosi' un nuovo guasto sullo stesso monitor riparte con un nuovo avviso."""
|
||||||
|
assert marcatore_aggiornato([ROW_OK], {"venue_watch:FERMO"}) == set()
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# orchestrazione — scripts/live/monitor_health.py main_orari(), NIENTE Telegram
|
||||||
|
# ===========================================================================
|
||||||
|
def test_notify_fallito_non_scrive_il_marcatore(tmp_path, monkeypatch):
|
||||||
|
"""Stessa regola di fee_watch #29: niente 'gia' detto' su un invio fallito, altrimenti il
|
||||||
|
prossimo giro non ritenterebbe."""
|
||||||
|
detto = tmp_path / "detto.json"
|
||||||
|
monkeypatch.setattr(MH, "DETTO", detto)
|
||||||
|
monkeypatch.setattr(MH, "check_all", lambda root, monitors=None: [ROW_FERMO])
|
||||||
|
chiamate = []
|
||||||
|
monkeypatch.setattr(MH, "notify", lambda *a, **k: (chiamate.append((a, k)), False)[1])
|
||||||
|
monkeypatch.setattr(sys, "argv", ["monitor_health.py", "--quiet", "--solo-orari"])
|
||||||
|
|
||||||
|
rc = MH.main_orari()
|
||||||
|
|
||||||
|
assert rc == 2 # c'e' un alert
|
||||||
|
assert len(chiamate) == 1 # e si e' provato a notificarlo
|
||||||
|
assert not detto.exists() # ma l'invio e' fallito: nessun marcatore
|
||||||
|
|
||||||
|
|
||||||
|
def test_notify_riuscito_scrive_il_marcatore_e_non_rinotifica(tmp_path, monkeypatch):
|
||||||
|
detto = tmp_path / "detto.json"
|
||||||
|
monkeypatch.setattr(MH, "DETTO", detto)
|
||||||
|
monkeypatch.setattr(MH, "check_all", lambda root, monitors=None: [ROW_FERMO])
|
||||||
|
monkeypatch.setattr(sys, "argv", ["monitor_health.py", "--quiet", "--solo-orari"])
|
||||||
|
|
||||||
|
monkeypatch.setattr(MH, "notify", lambda *a, **k: True)
|
||||||
|
rc1 = MH.main_orari()
|
||||||
|
assert rc1 == 2
|
||||||
|
assert json.loads(detto.read_text())["detti"] == ["venue_watch:FERMO"]
|
||||||
|
|
||||||
|
chiamate = []
|
||||||
|
monkeypatch.setattr(MH, "notify", lambda *a, **k: (chiamate.append(1), True)[1])
|
||||||
|
rc2 = MH.main_orari()
|
||||||
|
assert rc2 == 2 # l'alert c'e' ancora (anche se gia' detto: exit 2 comunque)
|
||||||
|
assert chiamate == [] # ma non si rimanda lo stesso Telegram
|
||||||
|
|
||||||
|
|
||||||
|
def test_tornato_ok_il_giro_dopo_pulisce_il_marcatore(tmp_path, monkeypatch):
|
||||||
|
detto = tmp_path / "detto.json"
|
||||||
|
detto.write_text(json.dumps({"detti": ["venue_watch:FERMO"], "ts": "2026-09-27T00:00:00Z"}))
|
||||||
|
monkeypatch.setattr(MH, "DETTO", detto)
|
||||||
|
monkeypatch.setattr(MH, "check_all", lambda root, monitors=None: [ROW_OK])
|
||||||
|
monkeypatch.setattr(sys, "argv", ["monitor_health.py", "--quiet", "--solo-orari"])
|
||||||
|
chiamate = []
|
||||||
|
monkeypatch.setattr(MH, "notify", lambda *a, **k: (chiamate.append(1), True)[1])
|
||||||
|
|
||||||
|
rc = MH.main_orari()
|
||||||
|
|
||||||
|
assert rc == 0
|
||||||
|
assert chiamate == [] # OK non notifica mai
|
||||||
|
assert json.loads(detto.read_text())["detti"] == []
|
||||||
Reference in New Issue
Block a user