""" Tests for release_check recurrence_watch gate (warn/strict/off behavior via GatePolicy). """ from __future__ import annotations import asyncio import os import sys from pathlib import Path from typing import Dict, Optional from unittest.mock import AsyncMock, MagicMock import pytest ROUTER_DIR = Path(__file__).parent.parent / "services" / "router" REPO_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(ROUTER_DIR)) sys.path.insert(0, str(REPO_ROOT)) os.environ.setdefault("REPO_ROOT", str(REPO_ROOT)) os.environ["AUDIT_BACKEND"] = "memory" os.environ["INCIDENT_BACKEND"] = "memory" # ─── Helpers ───────────────────────────────────────────────────────────────── class _FR: def __init__(self, data, success=True, error=None): self.success = success self.result = data self.error = error def _recurrence_result( high_sigs=None, high_kinds=None, warn_sigs=None, warn_kinds=None, max_sev="P3", total=0, ): return _FR({ "high_recurrence": { "signatures": high_sigs or [], "kinds": high_kinds or [], }, "warn_recurrence": { "signatures": warn_sigs or [], "kinds": warn_kinds or [], }, "max_severity_seen": max_sev, "total_incidents": total, }) def _make_tool_side_effect( high_sigs=None, high_kinds=None, warn_sigs=None, warn_kinds=None, max_sev="P3", total=0, recurrence_error=False, ): async def _exec(tool_name, args, agent_id=None): if tool_name == "pr_reviewer_tool": return _FR({"approved": True, "verdict": "LGTM", "issues": []}) if tool_name == "config_linter_tool": return _FR({"pass": True, "errors": [], "warnings": []}) if tool_name == "dependency_scanner_tool": return _FR({"pass": True, "summary": "ok", "vulnerabilities": []}) if tool_name == "contract_tool": return _FR({"pass": True, "breaking_changes": [], "warnings": []}) if tool_name == "threatmodel_tool": return _FR({"risk_level": "low", "threats": []}) if tool_name == "data_governance_tool": return _FR({"pass": True, "findings": [], "recommendations": [], "stats": {}}) if tool_name == "cost_analyzer_tool": return _FR({"anomalies": [], "anomaly_count": 0}) if tool_name == "observability_tool": return _FR({"violations": [], "metrics": {}, "thresholds": {}, "skipped": True}) if tool_name == "oncall_tool": action = args.get("action", "") if action == "incident_followups_summary": return _FR({"stats": {"open_incidents": 0, "overdue": 0, "total_open_followups": 0}, "open_incidents": [], "overdue_followups": []}) return _FR({}) if tool_name == "incident_intelligence_tool": if recurrence_error: return _FR({}, success=False, error="store unavailable") return _recurrence_result( high_sigs=high_sigs, high_kinds=high_kinds, warn_sigs=warn_sigs, warn_kinds=warn_kinds, max_sev=max_sev, total=total, ) return _FR({}) return _exec async def _run( inputs: Dict, high_sigs=None, high_kinds=None, warn_sigs=None, warn_kinds=None, max_sev="P3", total=0, recurrence_error=False, ): from release_check_runner import run_release_check, _reload_gate_policy _reload_gate_policy() tm = MagicMock() tm.execute_tool = AsyncMock(side_effect=_make_tool_side_effect( high_sigs=high_sigs, high_kinds=high_kinds, warn_sigs=warn_sigs, warn_kinds=warn_kinds, max_sev=max_sev, total=total, recurrence_error=recurrence_error, )) return await run_release_check(tm, inputs, agent_id="sofiia") # ─── Warn mode ──────────────────────────────────────────────────────────────── def test_recurrence_warn_mode_passes(): """dev profile: warn mode — high recurrence adds recommendation but pass=True.""" high_kinds = [{"kind": "error_rate", "count": 8, "services": ["gateway"]}] report = asyncio.run(_run( { "diff_text": "x", "gate_profile": "dev", "run_recurrence_watch": True, "fail_fast": False, "service_name": "gateway", }, high_kinds=high_kinds, max_sev="P1", total=8, )) assert report["pass"] is True, "warn mode must not block release" gate_names = [g["name"] for g in report["gates"]] assert "recurrence_watch" in gate_names rw = next(g for g in report["gates"] if g["name"] == "recurrence_watch") assert rw["status"] == "pass" assert rw.get("has_high_recurrence") is True assert any("recurrence" in r.lower() or "gateway" in r.lower() for r in report.get("recommendations", [])) def test_recurrence_warn_adds_recommendation_for_warn_level(): """Warn-level recurrence (not high) also adds recommendations in warn mode.""" warn_sigs = [{"signature": "aabbccdd1234", "count": 3, "services": ["router"], "last_seen": "2026-02-20T10:00:00", "severity_min": "P2"}] report = asyncio.run(_run( {"diff_text": "x", "gate_profile": "dev", "run_recurrence_watch": True, "fail_fast": False, "service_name": "router"}, warn_sigs=warn_sigs, max_sev="P2", total=3, )) assert report["pass"] is True rw = next((g for g in report["gates"] if g["name"] == "recurrence_watch"), None) assert rw is not None assert rw.get("has_warn_recurrence") is True # ─── Strict mode ───────────────────────────────────────────────────────────── def test_recurrence_strict_blocks_on_high_and_p1(): """staging: strict mode — high recurrence with P1 incident → release fails.""" high_kinds = [{"kind": "error_rate", "count": 8, "services": ["gateway"]}] report = asyncio.run(_run( { "diff_text": "x", "gate_profile": "staging", "run_recurrence_watch": True, "fail_fast": False, "service_name": "gateway", }, high_kinds=high_kinds, max_sev="P1", total=8, )) assert report["pass"] is False, "staging strict: high recurrence with P1 must fail" rw = next(g for g in report["gates"] if g["name"] == "recurrence_watch") assert rw.get("has_high_recurrence") is True def test_recurrence_strict_passes_when_no_high(): """staging: strict mode — warn-only recurrence (no high) → release passes.""" warn_kinds = [{"kind": "latency", "count": 4, "services": ["router"]}] report = asyncio.run(_run( { "diff_text": "x", "gate_profile": "staging", "run_recurrence_watch": True, "fail_fast": False, "service_name": "router", }, warn_kinds=warn_kinds, max_sev="P2", total=4, )) assert report["pass"] is True, "staging strict: warn-only recurrence should not block" def test_recurrence_strict_passes_when_high_but_low_severity(): """staging: strict mode — high recurrence but only P2/P3 → pass (fail_on P0/P1 only).""" high_sigs = [{"signature": "aabb1122ccdd", "count": 7, "services": ["svc"], "last_seen": "2026-02-20T12:00:00", "severity_min": "P2"}] report = asyncio.run(_run( { "diff_text": "x", "gate_profile": "staging", "run_recurrence_watch": True, "fail_fast": False, "service_name": "svc", }, high_sigs=high_sigs, max_sev="P2", total=7, )) assert report["pass"] is True, "staging strict: high recurrence with P2 should NOT block" # ─── Off mode ───────────────────────────────────────────────────────────────── def test_recurrence_off_mode_skips(): """run_recurrence_watch=False → gate not called, not in output.""" high_kinds = [{"kind": "error_rate", "count": 99, "services": ["gateway"]}] report = asyncio.run(_run( { "diff_text": "x", "gate_profile": "staging", "run_recurrence_watch": False, "fail_fast": False, }, high_kinds=high_kinds, max_sev="P0", total=99, )) assert report["pass"] is True gate_names = [g["name"] for g in report["gates"]] assert "recurrence_watch" not in gate_names def test_recurrence_watch_mode_override_off(): """recurrence_watch_mode=off input override skips gate even in staging.""" high_kinds = [{"kind": "error_rate", "count": 50, "services": ["svc"]}] report = asyncio.run(_run( { "diff_text": "x", "gate_profile": "staging", "run_recurrence_watch": True, "recurrence_watch_mode": "off", "fail_fast": False, }, high_kinds=high_kinds, max_sev="P0", total=50, )) assert report["pass"] is True gate_names = [g["name"] for g in report["gates"]] assert "recurrence_watch" not in gate_names # ─── Non-fatal error behavior ───────────────────────────────────────────────── def test_recurrence_watch_error_is_nonfatal(): """If intelligence tool fails → gate skips non-fatally, release still passes.""" report = asyncio.run(_run( { "diff_text": "x", "gate_profile": "staging", "run_recurrence_watch": True, "fail_fast": False, }, recurrence_error=True, )) assert report["pass"] is True, "Error in recurrence_watch must not block release" rw = next((g for g in report["gates"] if g["name"] == "recurrence_watch"), None) if rw: assert rw.get("skipped") is True # ─── Prod profile ──────────────────────────────────────────────────────────── def test_recurrence_prod_profile_is_warn(): """prod profile: recurrence_watch mode=warn → no blocking even with P0.""" high_kinds = [{"kind": "slo_breach", "count": 20, "services": ["gateway"]}] report = asyncio.run(_run( { "diff_text": "x", "gate_profile": "prod", "run_recurrence_watch": True, "fail_fast": False, }, high_kinds=high_kinds, max_sev="P0", total=20, )) assert report["pass"] is True, "prod profile: recurrence_watch is warn-only"