analista: il motivo di un guasto si prende da stdout E da stderr, o si perde
Il 2026-08-28 alle 00:37:00Z la CLI `claude` e' uscita 1 scrivendo «Failed to authenticate: OAuth session expired and could not be refreshed» su STDOUT, con stderr VUOTO. `interroga()` componeva il motivo dal solo stderr, quindi nel DB e' finito `analisi_stato='errore'`, `analisi_motivi='uscita 1: '` — e su Telegram e' partito «analisi non disponibile (errore): uscita 1:». La meccanica ha retto (nessuna analisi di ieri spacciata per quella di oggi, esito registrato, notifica partita): a mancare era solo il PERCHE'. P4 — un'allerta risponde a due domande, e con la prima sola la causa si ricostruisce aprendo a mano il transcript della sessione headless. P3 — quando la diagnosi serve, il guasto e' gia' rientrato: il motivo si cattura li' o mai. - `motivo_uscita(returncode, stdout, stderr)`: unisce i due canali (stderr per primo), tronca a 300 caratteri. - Silenzio totale su entrambi i canali -> lo DICE, invece di lasciare i due punti a vuoto: «la CLI non ha detto perche'» e «il perche' l'abbiamo perso noi» sono guasti diversi e finora si scrivevano uguali. - 4 test nuovi, fra cui la regressione col messaggio testuale del 28/08. NON risolve la causa a monte: perche' quella refresh sia fallita non sta in nessun log locale. Il refresh token era valido (rinnovo automatico riuscito alle 08:03:35Z dello stesso giorno, scadenza 2026-09-25) e la stessa chiamata rifatta a mano esce 0 -> guasto transitorio, unico su 24 sessioni locali. Da qui in avanti, se ricapita, il motivo si legge dalla notifica. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -149,6 +149,23 @@ def costruisci_prompt(pagina: str, storico: str) -> str:
|
||||
f"Scrivi ora l'analisi, e nient'altro.")
|
||||
|
||||
|
||||
def motivo_uscita(returncode: int, stdout: str | None, stderr: str | None) -> str:
|
||||
"""Il motivo di un'uscita non-zero della CLI, preso da stderr E da stdout.
|
||||
|
||||
PERCHE' ENTRAMBI. Il 2026-08-28 alle 00:37Z la CLI `claude` e' uscita 1 scrivendo
|
||||
«Failed to authenticate: OAuth session expired and could not be refreshed» su STDOUT,
|
||||
con stderr VUOTO. Leggendo il solo stderr il motivo registrato nel DB e spedito su
|
||||
Telegram fu `uscita 1: `, cioe' niente: l'allerta diceva il COSA e non il PERCHE' (P4),
|
||||
e per risalire alla causa e' servito aprire a mano il transcript della sessione headless.
|
||||
Quando la diagnosi serve il guasto e' gia' rientrato (P3): il motivo si cattura qui o mai.
|
||||
|
||||
E se non c'e' NESSUN messaggio lo si dice, invece di lasciare i due punti a vuoto:
|
||||
"la CLI non ha detto perche'" e "il perche' l'abbiamo perso noi" sono guasti diversi.
|
||||
"""
|
||||
det = " | ".join(x for x in ((stderr or "").strip(), (stdout or "").strip()) if x)
|
||||
return f"uscita {returncode}: {det[:300] if det else 'nessun messaggio su stderr ne stdout'}"
|
||||
|
||||
|
||||
def interroga(prompt: str, modello: str = MODELLO_DEFAULT,
|
||||
timeout: int = TIMEOUT_S) -> tuple[str, str | None]:
|
||||
"""Ritorna (testo, errore). L'errore si RESTITUISCE, non si ingoia: senza, il giorno che
|
||||
@@ -163,7 +180,7 @@ def interroga(prompt: str, modello: str = MODELLO_DEFAULT,
|
||||
except subprocess.TimeoutExpired:
|
||||
return "", f"timeout dopo {timeout}s"
|
||||
if p.returncode != 0:
|
||||
return "", f"uscita {p.returncode}: {(p.stderr or '').strip()[:200]}"
|
||||
return "", motivo_uscita(p.returncode, p.stdout, p.stderr)
|
||||
return pulisci(p.stdout), None
|
||||
|
||||
|
||||
|
||||
+34
-1
@@ -139,7 +139,40 @@ def test_uscita_non_zero_ritorna_errore(monkeypatch):
|
||||
class R:
|
||||
returncode, stdout, stderr = 2, "", "boom"
|
||||
monkeypatch.setattr(subprocess, "run", lambda *a, **k: R())
|
||||
assert "uscita 2" in A.interroga("x")[1]
|
||||
err = A.interroga("x")[1]
|
||||
assert "uscita 2" in err and "boom" in err
|
||||
|
||||
|
||||
# Il guasto vero del 2026-08-28 00:37Z: la CLI e' uscita 1 scrivendo il motivo su STDOUT con
|
||||
# stderr VUOTO. Il motivo registrato fu `uscita 1: ` — l'allerta diceva il COSA e non il PERCHE'
|
||||
# (P4), e la causa e' stata ricostruita solo aprendo a mano il transcript della sessione headless.
|
||||
# Questo test blinda che quel messaggio arrivi fino al DB e a Telegram.
|
||||
def test_motivo_preso_da_stdout_quando_stderr_e_vuoto(monkeypatch):
|
||||
class R:
|
||||
returncode = 1
|
||||
stdout = "Failed to authenticate: OAuth session expired and could not be refreshed"
|
||||
stderr = ""
|
||||
monkeypatch.setattr(subprocess, "run", lambda *a, **k: R())
|
||||
testo, err = A.interroga("x")
|
||||
assert testo == ""
|
||||
assert "OAuth session expired" in err and "uscita 1" in err
|
||||
|
||||
|
||||
def test_motivo_tiene_entrambi_i_canali():
|
||||
err = A.motivo_uscita(1, "detto su stdout", "detto su stderr")
|
||||
assert "detto su stderr" in err and "detto su stdout" in err
|
||||
|
||||
|
||||
def test_silenzio_totale_si_dichiara_invece_di_lasciare_i_due_punti_a_vuoto():
|
||||
"""'la CLI non ha detto perche'' e 'il perche' l'abbiamo perso noi' sono guasti diversi."""
|
||||
err = A.motivo_uscita(1, "", None)
|
||||
assert not err.rstrip().endswith(":")
|
||||
assert "nessun messaggio" in err
|
||||
|
||||
|
||||
def test_motivo_lungo_troncato_ma_non_vuoto():
|
||||
err = A.motivo_uscita(1, "x" * 5000, "")
|
||||
assert len(err) < 400 and "x" in err
|
||||
|
||||
|
||||
# ------------------------------------------------- il DB: cosa scrive e cosa non tocca
|
||||
|
||||
Reference in New Issue
Block a user