From ef3450d87b21b0e3674f59718fbc9a86bd049195 Mon Sep 17 00:00:00 2001 From: Grant Whitmer Date: Wed, 12 Aug 2026 15:42:58 -0400 Subject: [PATCH] G7.6: never let bookkeeping kill the monitor An unwritable state path raised and took the whole canary down. That is the worst possible trade for a monitoring tool: it reports nothing at all, and reports it silently. State is an optimisation for transition detection; the probing is the point. Found by fat-fingering an env var, which is exactly how it would happen in production. Co-Authored-By: Claude Opus 5 --- api/tests/test_invariants.py | 8 ++++++++ scripts/canary.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/api/tests/test_invariants.py b/api/tests/test_invariants.py index 765c36c..370b251 100644 --- a/api/tests/test_invariants.py +++ b/api/tests/test_invariants.py @@ -601,3 +601,11 @@ def test_g35_did_not_disable_validation_to_register(): one-time ordering problem.""" for f in (ROOT / "scripts").glob("*.py"): assert "skip_validation" not in f.read_text() + + +def test_g76_canary_survives_an_unwritable_state_path(): + """A monitoring tool that dies of a config problem reports nothing at all, + and reports it silently. State is an optimisation; probing is the point.""" + src = (ROOT / "scripts" / "canary.py").read_text() + save = src[src.index("def save_state") : src.index("def send_alert")] + assert "except OSError" in save diff --git a/scripts/canary.py b/scripts/canary.py index 823c022..aa84c53 100755 --- a/scripts/canary.py +++ b/scripts/canary.py @@ -156,8 +156,20 @@ def load_state() -> dict: def save_state(results: list[Result]) -> None: - with open(STATE_PATH, "w") as f: - json.dump({r.name: r.status for r in results}, f, indent=2) + """Never let bookkeeping kill the monitor. + + State is an optimisation — it lets the next run tell "still broken" from + "just broke". The probing is the valuable part. An unwritable path used to + raise here and take the whole canary down, which is the worst possible + trade: a monitoring tool that dies of a config problem reports nothing at + all, and reports it silently. + """ + try: + with open(STATE_PATH, "w") as f: + json.dump({r.name: r.status for r in results}, f, indent=2) + except OSError as exc: + print(f"!! could not save state to {STATE_PATH}: {exc}") + print(" (probes still ran; transition detection is degraded this run)") def send_alert(subject: str, lines: list[str]) -> bool: