""" tests/test_release_check_risk_delta_watch.py — Unit tests for risk_delta_watch gate. Tests: - warn mode: gate passes with recommendations when delta >= warn_delta - strict mode: should_fail=True for p0_services when delta >= fail_delta - missing history → skipped (non-fatal) - tool error → skipped (non-fatal) """ import asyncio import datetime import sys import pytest from pathlib import Path from unittest.mock import AsyncMock, MagicMock sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "services" / "router")) from release_check_runner import _run_risk_delta_watch from risk_history_store import MemoryRiskHistoryStore, RiskSnapshot, set_risk_history_store def _snap(service, env, score, hours_ago=0) -> RiskSnapshot: ts = (datetime.datetime.utcnow() - datetime.timedelta(hours=hours_ago)).isoformat() return RiskSnapshot(ts=ts, service=service, env=env, score=score, band="medium") def _make_tm(score: int, service: str = "gateway", fail_execute: bool = False): """Stub ToolManager returning a fixed risk score.""" tm = MagicMock() if fail_execute: tm.execute_tool = AsyncMock(side_effect=RuntimeError("timeout")) else: result = MagicMock() result.success = True result.error = None result.result = {"service": service, "env": "prod", "score": score, "band": "medium"} tm.execute_tool = AsyncMock(return_value=result) return tm # ─── Warn mode ──────────────────────────────────────────────────────────────── class TestRiskDeltaWarnMode: def _run(self, **kwargs): return asyncio.run(_run_risk_delta_watch(**kwargs)) def test_delta_below_warn_is_clean(self, tmp_path): """delta 5 < warn 10 → no recommendations.""" store = MemoryRiskHistoryStore() store.write_snapshot([ _snap("gateway", "prod", 50, hours_ago=25), _snap("gateway", "prod", 55, hours_ago=1), # delta=5 ]) set_risk_history_store(store) tm = _make_tm(score=55) ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="gateway", env="prod", ) assert ok is True assert gate["status"] == "pass" assert not gate.get("skipped") assert gate.get("delta") == 5 assert gate.get("regression_warn") is False assert gate.get("recommendations", []) == [] def test_delta_at_warn_threshold_adds_recommendation(self, tmp_path): """delta 10 == warn_delta 10 → warn=True, rec added.""" store = MemoryRiskHistoryStore() store.write_snapshot([ _snap("gateway", "prod", 40, hours_ago=25), _snap("gateway", "prod", 50, hours_ago=1), # delta=10 ]) set_risk_history_store(store) tm = _make_tm(score=50) ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="gateway", env="prod", ) assert ok is True assert gate["regression_warn"] is True assert len(gate.get("recommendations", [])) > 0 def test_delta_above_fail_adds_strong_recommendation(self, tmp_path): """delta 25 >= fail 20 → regression_fail=True, recommendations urgent.""" store = MemoryRiskHistoryStore() store.write_snapshot([ _snap("gateway", "prod", 30, hours_ago=25), _snap("gateway", "prod", 55, hours_ago=1), # delta=25 ]) set_risk_history_store(store) tm = _make_tm(score=55) ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="gateway", env="prod", ) assert ok is True # gate helper always returns ok=True assert gate["regression_fail"] is True assert any("FAIL" in r or "fail" in r.lower() for r in gate.get("recommendations", [])) # ─── Strict mode: should_fail for p0_services ──────────────────────────────── class TestRiskDeltaStrictMode: def _run(self, **kwargs): return asyncio.run(_run_risk_delta_watch(**kwargs)) def test_should_fail_set_for_p0_service_with_high_delta(self, tmp_path): """gateway is p0, delta 25 >= fail 20 → should_fail=True.""" store = MemoryRiskHistoryStore() store.write_snapshot([ _snap("gateway", "prod", 30, hours_ago=25), _snap("gateway", "prod", 55, hours_ago=1), ]) set_risk_history_store(store) tm = _make_tm(score=55, service="gateway") ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="gateway", env="prod", ) assert gate["should_fail"] is True assert gate["is_p0"] is True def test_should_fail_false_for_non_p0_service(self, tmp_path): """memory-service is not p0 → should_fail=False even if delta >= fail.""" store = MemoryRiskHistoryStore() store.write_snapshot([ _snap("memory-service", "prod", 10, hours_ago=25), _snap("memory-service", "prod", 40, hours_ago=1), # delta=30 ]) set_risk_history_store(store) tm = _make_tm(score=40, service="memory-service") ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="memory-service", env="prod", ) assert gate["should_fail"] is False assert gate["is_p0"] is False def test_custom_fail_delta_respected(self, tmp_path): """Override fail_delta=30; delta 25 < 30 → should_fail=False.""" store = MemoryRiskHistoryStore() store.write_snapshot([ _snap("gateway", "prod", 30, hours_ago=25), _snap("gateway", "prod", 55, hours_ago=1), # delta=25 ]) set_risk_history_store(store) tm = _make_tm(score=55, service="gateway") ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="gateway", env="prod", fail_delta=30, ) assert gate["should_fail"] is False # 25 < 30 def test_effective_thresholds_in_gate(self, tmp_path): store = MemoryRiskHistoryStore() store.write_snapshot([_snap("gateway", "prod", 40, hours_ago=25), _snap("gateway", "prod", 60, hours_ago=1)]) set_risk_history_store(store) tm = _make_tm(score=60) ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="gateway", env="prod", warn_delta=5, fail_delta=15, ) assert gate["effective_warn_delta"] == 5 assert gate["effective_fail_delta"] == 15 # ─── Non-fatal (skipped) ────────────────────────────────────────────────────── class TestRiskDeltaNonFatal: def _run(self, **kwargs): return asyncio.run(_run_risk_delta_watch(**kwargs)) def test_no_history_skips_gracefully(self, tmp_path): """Empty history store → skipped=True, ok=True, no crash.""" store = MemoryRiskHistoryStore() set_risk_history_store(store) tm = _make_tm(score=60) ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="gateway", env="prod", ) assert ok is True assert gate.get("skipped") is True assert gate["status"] == "pass" assert any("baseline" in r.lower() or "history" in r.lower() for r in gate.get("recommendations", [])) def test_tool_error_skips_gracefully(self, tmp_path): """risk_engine_tool raises → skipped, never blocks.""" store = MemoryRiskHistoryStore() set_risk_history_store(store) tm = _make_tm(score=0, fail_execute=True) ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="gateway", env="prod", ) assert ok is True assert gate.get("skipped") is True def test_no_service_name_skips(self, tmp_path): """Empty service_name → skipped immediately.""" store = MemoryRiskHistoryStore() set_risk_history_store(store) tm = MagicMock() tm.execute_tool = AsyncMock() ok, gate = self._run( tool_manager=tm, agent_id="ops", service_name="", env="prod", ) assert ok is True assert gate.get("skipped") is True tm.execute_tool.assert_not_called()