diff --git a/api/tests/test_invariants.py b/api/tests/test_invariants.py index 2328f49..734cdfc 100644 --- a/api/tests/test_invariants.py +++ b/api/tests/test_invariants.py @@ -537,3 +537,13 @@ def test_g76_canary_has_two_independent_signals(): had one signal and nothing watched the watcher.""" src = (ROOT / "scripts" / "canary.py").read_text() assert "return 1 if any" in src, "canary must exit non-zero so CI goes red" + + +def test_g76_alert_path_sets_a_user_agent(): + """Without an explicit User-Agent, urllib sends 'Python-urllib/3.x' and + Resend rejects it 403 while the identical curl succeeds. Caught by testing + the alert path: the canary would have detected every outage correctly and + told nobody.""" + src = (ROOT / "scripts" / "canary.py").read_text() + send = src[src.index("def send_alert") : src.index("def main(")] + assert "User-Agent" in send diff --git a/scripts/canary.py b/scripts/canary.py index 47016f7..823c022 100755 --- a/scripts/canary.py +++ b/scripts/canary.py @@ -177,12 +177,24 @@ def send_alert(subject: str, lines: list[str]) -> bool: headers={ "Authorization": f"Bearer {RESEND_KEY}", "Content-Type": "application/json", + # ⚠️ REQUIRED. Without an explicit User-Agent, urllib sends + # "Python-urllib/3.x" and the request is rejected 403 by bot + # filtering — while the identical request via curl succeeds. This + # exact failure was caught by testing the alert path rather than + # assuming it: the canary would have detected every outage + # correctly and told nobody. + "User-Agent": "windy-git-canary/1.0", }, ) try: with urllib.request.urlopen(req, timeout=30) as r: print(f" alert sent ({r.status})") return True + except urllib.error.HTTPError as e: + # Print the body. "403 Forbidden" alone sends you hunting for a bad key; + # the body usually names the real cause. + print(f"!! alert FAILED: HTTP {e.code}: {e.read().decode()[:200]}") + return False except Exception as e: # noqa: BLE001 print(f"!! alert FAILED: {e}") return False