""" Tests for followup_watch gate integration in release_check_runner. """ import os import sys import asyncio import json from datetime import datetime, timedelta from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest ROOT = Path(__file__).resolve().parent.parent ROUTER = ROOT / "services" / "router" if str(ROUTER) not in sys.path: sys.path.insert(0, str(ROUTER)) class MockToolResult: def __init__(self, success, result=None, error=None): self.success = success self.result = result self.error = error class MockToolManager: """Minimal mock for tool_manager.execute_tool, customizable per test.""" def __init__(self, followup_data=None, always_pass_others=True): self.followup_data = followup_data or { "open_incidents": [], "overdue_followups": [], "stats": {"open_incidents": 0, "overdue": 0, "total_open_followups": 0}, } self.always_pass_others = always_pass_others self.calls = [] async def execute_tool(self, tool_name, args, agent_id="test"): self.calls.append((tool_name, args.get("action"))) if tool_name == "oncall_tool" and args.get("action") == "incident_followups_summary": return MockToolResult(True, self.followup_data) if self.always_pass_others: return MockToolResult(True, { "pass": True, "blocking_count": 0, "breaking_count": 0, "unmitigated_high_count": 0, "summary": "ok", }) return MockToolResult(False, error="skipped") @pytest.fixture def reset_policy_cache(): from release_check_runner import _reload_gate_policy _reload_gate_policy() yield _reload_gate_policy() def _run_check(tm, inputs, agent="test"): from release_check_runner import run_release_check return asyncio.run(run_release_check(tm, inputs, agent)) class TestFollowupWatchGateWarn: """followup_watch in warn mode: release passes regardless.""" def test_release_passes_with_open_p1(self, reset_policy_cache): data = { "open_incidents": [ {"id": "inc_1", "severity": "P1", "status": "open", "started_at": "2025-01-01", "title": "Outage"} ], "overdue_followups": [], "stats": {"open_incidents": 1, "overdue": 0, "total_open_followups": 0}, } tm = MockToolManager(followup_data=data) with patch("release_check_runner.load_gate_policy") as mock_policy: mock_policy.return_value = { "_profile": "dev", "_default_mode": "warn", "followup_watch": {"mode": "warn", "fail_on": ["P0", "P1"]}, "privacy_watch": {"mode": "off"}, "cost_watch": {"mode": "off"}, "get": lambda name: {"mode": "warn"}, } result = _run_check(tm, {"service_name": "gateway"}) assert result["pass"] is True gate_names = [g["name"] for g in result["gates"]] assert "followup_watch" in gate_names fw_gate = next(g for g in result["gates"] if g["name"] == "followup_watch") assert fw_gate["status"] == "pass" assert "Open critical incidents" in " ".join(result["recommendations"]) def test_release_passes_with_overdue(self, reset_policy_cache): data = { "open_incidents": [], "overdue_followups": [ {"incident_id": "inc_1", "title": "Fix it", "due_date": "2025-01-01", "priority": "P1", "owner": "sofiia"} ], "stats": {"open_incidents": 0, "overdue": 1, "total_open_followups": 1}, } tm = MockToolManager(followup_data=data) with patch("release_check_runner.load_gate_policy") as mock_policy: mock_policy.return_value = { "_profile": "dev", "_default_mode": "warn", "followup_watch": {"mode": "warn", "fail_on": ["P0", "P1"]}, "privacy_watch": {"mode": "off"}, "cost_watch": {"mode": "off"}, "get": lambda name: {"mode": "warn"}, } result = _run_check(tm, {"service_name": "gateway"}) assert result["pass"] is True assert "Overdue follow-ups" in " ".join(result["recommendations"]) class TestFollowupWatchGateStrict: """followup_watch in strict mode: blocks release on P0/P1 or overdue.""" def test_release_blocked_by_open_p1(self, reset_policy_cache): data = { "open_incidents": [ {"id": "inc_1", "severity": "P1", "status": "open", "started_at": "2025-01-01", "title": "Outage"} ], "overdue_followups": [], "stats": {"open_incidents": 1, "overdue": 0, "total_open_followups": 0}, } tm = MockToolManager(followup_data=data) with patch("release_check_runner.load_gate_policy") as mock_policy: mock_policy.return_value = { "_profile": "staging", "_default_mode": "warn", "followup_watch": {"mode": "strict", "fail_on": ["P0", "P1"]}, "privacy_watch": {"mode": "off"}, "cost_watch": {"mode": "off"}, "get": lambda name: {"mode": "warn"}, } result = _run_check(tm, {"service_name": "gateway", "fail_fast": True}) assert result["pass"] is False def test_release_blocked_by_overdue_followups(self, reset_policy_cache): data = { "open_incidents": [], "overdue_followups": [ {"incident_id": "inc_1", "title": "Migrate DB", "due_date": "2025-01-01", "priority": "P1", "owner": "sofiia"} ], "stats": {"open_incidents": 0, "overdue": 1, "total_open_followups": 1}, } tm = MockToolManager(followup_data=data) with patch("release_check_runner.load_gate_policy") as mock_policy: mock_policy.return_value = { "_profile": "staging", "_default_mode": "warn", "followup_watch": {"mode": "strict", "fail_on": ["P0", "P1"]}, "privacy_watch": {"mode": "off"}, "cost_watch": {"mode": "off"}, "get": lambda name: {"mode": "warn"}, } result = _run_check(tm, {"service_name": "gateway", "fail_fast": True}) assert result["pass"] is False def test_release_passes_when_no_issues(self, reset_policy_cache): data = { "open_incidents": [], "overdue_followups": [], "stats": {"open_incidents": 0, "overdue": 0, "total_open_followups": 0}, } tm = MockToolManager(followup_data=data) with patch("release_check_runner.load_gate_policy") as mock_policy: mock_policy.return_value = { "_profile": "staging", "_default_mode": "warn", "followup_watch": {"mode": "strict", "fail_on": ["P0", "P1"]}, "privacy_watch": {"mode": "off"}, "cost_watch": {"mode": "off"}, "get": lambda name: {"mode": "warn"}, } result = _run_check(tm, {"service_name": "gateway"}) assert result["pass"] is True class TestFollowupWatchGateOff: """followup_watch in off mode: gate is skipped entirely.""" def test_gate_skipped_when_off(self, reset_policy_cache): tm = MockToolManager() with patch("release_check_runner.load_gate_policy") as mock_policy: mock_policy.return_value = { "_profile": "dev", "_default_mode": "warn", "followup_watch": {"mode": "off"}, "privacy_watch": {"mode": "off"}, "cost_watch": {"mode": "off"}, "get": lambda name: {"mode": "off"}, } result = _run_check(tm, {"service_name": "gateway"}) gate_names = [g["name"] for g in result["gates"]] assert "followup_watch" not in gate_names called_actions = [c[1] for c in tm.calls] assert "incident_followups_summary" not in called_actions