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
266 lines
11 KiB
Python
266 lines
11 KiB
Python
"""
|
|
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"
|