""" tests/test_risk_evidence_refs.py Unit tests for evidence refs in risk_attribution.py: - deploy cause includes alert_ref refs - followups include dedupe_key / incident_id refs - max_refs_per_cause enforced - top-level evidence_refs built correctly - incident_storm includes incident_ids """ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../services/router")) import datetime import pytest from risk_attribution import ( compute_attribution, build_evidence_refs, _detect_deploy, _detect_followups_overdue, _detect_incident_storm, _detect_slo, ) def _cutoff(hours: int = 24) -> str: return (datetime.datetime.utcnow() - datetime.timedelta(hours=hours)).isoformat() def _ts(minutes_ago: int = 5) -> str: return (datetime.datetime.utcnow() - datetime.timedelta(minutes=minutes_ago)).isoformat() _POLICY = { "weights": { "deploy": 30, "dependency": 25, "drift": 25, "incident_storm": 20, "slo_violation": 15, "followups_overdue": 10, "alert_loop_degraded": 10, }, "signals": { "deploy": {"kinds": ["deploy", "canary", "rollout"]}, "incident_storm": {"thresholds": {"occurrences_60m_warn": 10, "escalations_24h_warn": 2}}, "slo": {"require_active_violation": True}, }, "output": {"confidence_bands": {"high": 60, "medium": 35}}, "defaults": {"lookback_hours": 24, "max_causes": 5, "llm_mode": "off"}, "timeline": {"enabled": False}, "evidence_linking": {"enabled": True, "max_refs_per_cause": 5}, } class TestDeployCauseRefs: def test_deploy_cause_includes_alert_refs(self): alerts = [ {"alert_ref": "alrt_001", "kind": "deploy", "created_at": _ts(5), "service": "gateway"}, {"alert_ref": "alrt_002", "kind": "canary", "created_at": _ts(10), "service": "gateway"}, ] score, evid, refs = _detect_deploy(alerts, _cutoff(), _POLICY, max_refs=10) assert score == 30 alert_refs = [r["alert_ref"] for r in refs if "alert_ref" in r] assert "alrt_001" in alert_refs assert "alrt_002" in alert_refs def test_deploy_no_alerts_no_refs(self): score, evid, refs = _detect_deploy([], _cutoff(), _POLICY) assert score == 0 assert refs == [] def test_max_refs_per_cause_enforced(self): alerts = [ {"alert_ref": f"alrt_{i}", "kind": "deploy", "created_at": _ts(i + 1), "service": "svc"} for i in range(20) ] score, evid, refs = _detect_deploy(alerts, _cutoff(), _POLICY, max_refs=5) assert score == 30 assert len(refs) <= 5 class TestFollowupRefs: def test_followups_include_provided_refs(self): followup_refs = [ {"incident_id": "inc_001", "dedupe_key": "fu_k1"}, {"incident_id": "inc_002", "dedupe_key": "fu_k2"}, ] score, evid, refs = _detect_followups_overdue(2, _POLICY, followup_refs=followup_refs) assert score == 10 inc_ids = [r.get("incident_id") for r in refs] assert "inc_001" in inc_ids assert "inc_002" in inc_ids def test_followups_max_refs(self): followup_refs = [{"incident_id": f"inc_{i}"} for i in range(20)] score, evid, refs = _detect_followups_overdue(20, _POLICY, followup_refs=followup_refs, max_refs=4) assert len(refs) <= 4 def test_followups_zero_overdue_no_refs(self): score, evid, refs = _detect_followups_overdue(0, _POLICY) assert score == 0 assert refs == [] class TestIncidentStormRefs: def test_storm_includes_incident_ids(self): score, evid, refs = _detect_incident_storm( occurrences_60m=15, escalations_24h=3, policy=_POLICY, incident_ids=["inc_001", "inc_002"], max_refs=10, ) assert score == 20 incident_ids = [r["incident_id"] for r in refs] assert "inc_001" in incident_ids assert "inc_002" in incident_ids def test_storm_max_refs(self): ids = [f"inc_{i}" for i in range(20)] score, evid, refs = _detect_incident_storm(15, 3, _POLICY, incident_ids=ids, max_refs=3) assert len(refs) <= 3 class TestSloRefs: def test_slo_includes_metric_names(self): metrics = ["error_rate:gateway", "latency_p99:gateway"] score, evid, refs = _detect_slo(2, _POLICY, slo_metrics=metrics) assert score == 15 metric_names = [r["metric"] for r in refs] assert "error_rate:gateway" in metric_names def test_slo_max_refs(self): metrics = [f"metric_{i}" for i in range(20)] score, evid, refs = _detect_slo(5, _POLICY, slo_metrics=metrics, max_refs=3) assert len(refs) <= 3 class TestTopLevelEvidenceRefs: def test_build_evidence_refs_structure(self): alerts = [{"alert_ref": "alrt_1"}, {"alert_ref": "alrt_2"}] incidents = [{"id": "inc_1"}, {"id": "inc_2"}] gates = [{"run_id": "rc_001", "gate": "dependency_scan", "status": "fail", "artifact": "ops/reports/scan.md"}] followups = [{"incident_id": "inc_1", "dedupe_key": "fu_k1"}] refs = build_evidence_refs(alerts, incidents, gates, followup_refs=followups, policy=_POLICY) assert "alrt_1" in refs["alerts"] assert "alrt_2" in refs["alerts"] assert "inc_1" in refs["incidents"] assert "rc_001" in refs["release_checks"] assert "ops/reports/scan.md" in refs["artifacts"] assert len(refs["followups"]) == 1 def test_evidence_refs_max_refs(self): alerts = [{"alert_ref": f"a_{i}"} for i in range(30)] refs = build_evidence_refs(alerts, [], [], policy=_POLICY) assert len(refs["alerts"]) <= 5 # policy max_refs_per_cause = 5 def test_empty_inputs(self): refs = build_evidence_refs([], [], [], policy=_POLICY) assert refs["alerts"] == [] assert refs["incidents"] == [] assert refs["release_checks"] == [] assert refs["artifacts"] == [] class TestComputeAttributionRefsIntegration: def test_attribution_includes_cause_refs(self): alerts = [ {"alert_ref": "alrt_a1", "kind": "deploy", "created_at": _ts(5), "service": "gateway"}, ] result = compute_attribution( "gateway", "prod", alerts_24h=alerts, policy=_POLICY, ) deploy_cause = next((c for c in result["causes"] if c["type"] == "deploy"), None) assert deploy_cause is not None assert "alrt_a1" in str(deploy_cause.get("refs", [])) def test_attribution_includes_evidence_refs_top_level(self): alerts = [{"alert_ref": "alrt_x", "kind": "deploy", "created_at": _ts(5), "service": "svc"}] incidents = [{"id": "inc_42", "started_at": _ts(10), "service": "svc"}] result = compute_attribution( "svc", "prod", alerts_24h=alerts, incidents_24h=incidents, policy={**_POLICY, "timeline": {"enabled": False}, "evidence_linking": {"enabled": True, "max_refs_per_cause": 10}}, ) assert "evidence_refs" in result assert "alrt_x" in result["evidence_refs"]["alerts"] assert "inc_42" in result["evidence_refs"]["incidents"] def test_attribution_evidence_refs_disabled(self): policy = {**_POLICY, "timeline": {"enabled": False}, "evidence_linking": {"enabled": False, "max_refs_per_cause": 10}} result = compute_attribution("svc", "prod", policy=policy) assert result.get("evidence_refs") == {}