From 29a67ec775f6c9250edb46dac9d8f337b59f7e50 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 4 Jun 2026 20:31:43 +0800 Subject: [PATCH] fix(ci): tolerate empty source link canary response --- .../tests/test_alert_chain_smoke_metric.py | 47 +++++++++++ scripts/alert_chain_smoke_test.py | 81 +++++++++++++++++-- 2 files changed, 122 insertions(+), 6 deletions(-) diff --git a/apps/api/tests/test_alert_chain_smoke_metric.py b/apps/api/tests/test_alert_chain_smoke_metric.py index 3fcf9328..28864473 100644 --- a/apps/api/tests/test_alert_chain_smoke_metric.py +++ b/apps/api/tests/test_alert_chain_smoke_metric.py @@ -430,6 +430,53 @@ class AlertChainSmokeMetricTest(unittest.TestCase): self.assertIn(["target_incident_id", "INC-20260505-25E744"], tags) self.assertEqual(calls[0]["headers"]["X-AwoooP-Operator-Key"], "secret") + def test_source_link_canary_accepts_empty_2xx_for_downstream_readback(self): + def fake_post(url, payload, *, headers=None, timeout=None): + self.assertTrue(url.endswith("/api/v1/webhooks/sentry/error")) + self.assertEqual(payload["data"]["issue"]["title"], "AwoooPSourceLinkCanary") + return alert_chain_smoke_test.HttpGetResult(204, "") + + original_post = alert_chain_smoke_test.http_post_json + try: + alert_chain_smoke_test.http_post_json = fake_post + result = alert_chain_smoke_test.send_source_link_canary( + "https://awoooi.example", + target_incident_id="INC-20260505-25E744", + operator_key="secret", + operator_id="gitea-e2e-health", + run_ref="run/123", + ) + finally: + alert_chain_smoke_test.http_post_json = original_post + + self.assertTrue(result.passed) + self.assertIn("source-correlation smoke must verify readback", result.message) + + def test_source_link_canary_reports_http_error_before_json_parse(self): + def fake_post(url, payload, *, headers=None, timeout=None): + self.assertTrue(url.endswith("/api/v1/webhooks/sentry/error")) + return alert_chain_smoke_test.HttpGetResult( + 502, + "bad gateway", + ) + + original_post = alert_chain_smoke_test.http_post_json + try: + alert_chain_smoke_test.http_post_json = fake_post + result = alert_chain_smoke_test.send_source_link_canary( + "https://awoooi.example", + target_incident_id="INC-20260505-25E744", + operator_key="secret", + operator_id="gitea-e2e-health", + run_ref="run/123", + ) + finally: + alert_chain_smoke_test.http_post_json = original_post + + self.assertFalse(result.passed) + self.assertIn("sentry HTTP 502", result.message) + self.assertIn("bad gateway", result.message) + if __name__ == "__main__": unittest.main() diff --git a/scripts/alert_chain_smoke_test.py b/scripts/alert_chain_smoke_test.py index f0d71014..3cd6050d 100644 --- a/scripts/alert_chain_smoke_test.py +++ b/scripts/alert_chain_smoke_test.py @@ -137,6 +137,22 @@ def _http_error_message(error: Exception) -> str: return str(error) +def _response_body_preview(text: str, limit: int = 240) -> str: + cleaned = " ".join((text or "").split()) + if not cleaned: + return "" + if len(cleaned) <= limit: + return cleaned + return f"{cleaned[:limit]}..." + + +def _response_json(resp: HttpGetResult) -> dict[str, Any] | None: + text = (resp.text or "").strip() + if not text: + return None + return resp.json() + + def _api_health_probe_summary(attempt: int) -> str: return ( f"attempts={attempt}/{API_HEALTH_ATTEMPTS}, " @@ -782,18 +798,43 @@ def send_source_provider_upstream_canary( }, timeout=TIMEOUT, ) - data = resp.json() - except (URLError, TimeoutError, OSError, json.JSONDecodeError) as e: + except (URLError, TimeoutError, OSError) as e: return CheckResult( "Source Provider Upstream Canary", False, f"{provider} upstream canary failed: {_http_error_message(e)}", ) if resp.status_code >= 400: + try: + data = _response_json(resp) + except json.JSONDecodeError: + data = None + detail = ( + data.get("detail") + if isinstance(data, dict) + else _response_body_preview(resp.text) + ) return CheckResult( "Source Provider Upstream Canary", False, - f"{provider} HTTP {resp.status_code}: {data.get('detail', resp.text) if isinstance(data, dict) else resp.text}", + f"{provider} HTTP {resp.status_code}: {detail}", + ) + try: + data = _response_json(resp) + except json.JSONDecodeError: + return CheckResult( + "Source Provider Upstream Canary", + False, + ( + f"{provider} upstream canary returned non-JSON " + f"HTTP {resp.status_code}: {_response_body_preview(resp.text)}" + ), + ) + if data is None: + return CheckResult( + "Source Provider Upstream Canary", + False, + f"{provider} upstream canary returned empty HTTP {resp.status_code}", ) validation_error = _validate_upstream_canary_response(provider, data) if validation_error: @@ -850,18 +891,46 @@ def send_source_link_canary( }, timeout=TIMEOUT, ) - data = resp.json() - except (URLError, TimeoutError, OSError, json.JSONDecodeError) as e: + except (URLError, TimeoutError, OSError) as e: return CheckResult( "Source Link Canary", False, f"sentry source-link canary failed: {_http_error_message(e)}", ) if resp.status_code >= 400: + try: + data = _response_json(resp) + except json.JSONDecodeError: + data = None + detail = ( + data.get("detail") + if isinstance(data, dict) + else _response_body_preview(resp.text) + ) return CheckResult( "Source Link Canary", False, - f"sentry HTTP {resp.status_code}: {data.get('detail', resp.text) if isinstance(data, dict) else resp.text}", + f"sentry HTTP {resp.status_code}: {detail}", + ) + if not (resp.text or "").strip(): + return CheckResult( + "Source Link Canary", + True, + ( + "accepted sentry source-link canary post with empty " + f"HTTP {resp.status_code}; source-correlation smoke must verify readback" + ), + ) + try: + data = resp.json() + except json.JSONDecodeError: + return CheckResult( + "Source Link Canary", + False, + ( + "sentry source-link canary returned non-JSON " + f"HTTP {resp.status_code}: {_response_body_preview(resp.text)}" + ), ) validation_error = _validate_upstream_canary_response("sentry", data) if validation_error: