New router intelligence modules (26 files): alert_ingest/store, audit_store, architecture_pressure, backlog_generator/store, cost_analyzer, data_governance, dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment, platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files), signature_state_store, sofiia_auto_router, tool_governance New services: - sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static - memory-service: integration_endpoints, integrations, voice_endpoints, static UI - aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents) - sofiia-supervisor: new supervisor service - aistalk-bridge-lite: Telegram bridge lite - calendar-service: CalDAV calendar service with reminders - mlx-stt-service / mlx-tts-service: Apple Silicon speech services - binance-bot-monitor: market monitor service - node-worker: STT/TTS memory providers New tools (9): agent_email, browser_tool, contract_tool, observability_tool, oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus, farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine, session_context, style_adapter, telemetry) Tests: 85+ test files for all new modules Made-with: Cursor
209 lines
8.1 KiB
Python
209 lines
8.1 KiB
Python
"""
|
|
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
|