feat(platform): add new services, tools, tests and crews modules
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
This commit is contained in:
185
tests/test_release_check_risk_watch.py
Normal file
185
tests/test_release_check_risk_watch.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
tests/test_release_check_risk_watch.py — Unit tests for risk_watch release gate.
|
||||
|
||||
Tests:
|
||||
- warn mode: gate passes but adds recommendations when score >= warn_at
|
||||
- strict mode: gate fails when score >= fail_at for p0_services
|
||||
- non-fatal error: skipped gracefully, never blocks release
|
||||
"""
|
||||
import asyncio
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "services" / "router"))
|
||||
|
||||
|
||||
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _make_risk_report(service, score, band=None, reasons=None, recs=None, warn_at=50, fail_at=80):
|
||||
"""Build a minimal RiskReport dict matching risk_engine output."""
|
||||
from risk_engine import score_to_band, _builtin_defaults
|
||||
p = _builtin_defaults()
|
||||
b = band or score_to_band(score, p)
|
||||
return {
|
||||
"service": service,
|
||||
"env": "prod",
|
||||
"score": score,
|
||||
"band": b,
|
||||
"thresholds": {"warn_at": warn_at, "fail_at": fail_at},
|
||||
"components": {
|
||||
"open_incidents": {"P0": 0, "P1": 0, "points": 0},
|
||||
"recurrence": {"points": 0},
|
||||
"followups": {"points": 0},
|
||||
"slo": {"violations": 0, "points": 0},
|
||||
"alerts_loop": {"violations": 0, "points": 0},
|
||||
"escalations": {"count_24h": 0, "points": 0},
|
||||
},
|
||||
"reasons": reasons or [],
|
||||
"recommendations": recs or [],
|
||||
"updated_at": "2026-02-23T00:00:00",
|
||||
}
|
||||
|
||||
|
||||
def _make_tool_manager(score, service="gateway", warn_at=50, fail_at=80,
|
||||
fail_execute=False):
|
||||
"""Stub ToolManager that returns a pre-built RiskReport."""
|
||||
tm = MagicMock()
|
||||
if fail_execute:
|
||||
tm.execute_tool = AsyncMock(side_effect=RuntimeError("connection timeout"))
|
||||
else:
|
||||
result = MagicMock()
|
||||
result.success = True
|
||||
result.error = None
|
||||
result.result = _make_risk_report(service, score,
|
||||
warn_at=warn_at, fail_at=fail_at)
|
||||
tm.execute_tool = AsyncMock(return_value=result)
|
||||
return tm
|
||||
|
||||
|
||||
# ─── Import the helper directly ──────────────────────────────────────────────
|
||||
|
||||
from release_check_runner import _run_risk_watch
|
||||
|
||||
|
||||
# ─── Warn mode tests ─────────────────────────────────────────────────────────
|
||||
|
||||
class TestRiskWatchWarnMode:
|
||||
def test_score_below_warn_at_is_clean(self):
|
||||
"""Score 30 < warn_at 50 — gate passes, no recommendations."""
|
||||
tm = _make_tool_manager(score=30)
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway", env="prod")
|
||||
)
|
||||
assert ok is True
|
||||
assert gate["status"] == "pass"
|
||||
assert not gate.get("skipped")
|
||||
assert gate.get("recommendations", []) == []
|
||||
|
||||
def test_score_at_warn_at_adds_recommendation(self):
|
||||
"""Score 50 == warn_at 50 — passes but includes recommendations."""
|
||||
tm = _make_tool_manager(score=50, service="gateway")
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway", env="prod")
|
||||
)
|
||||
assert ok is True
|
||||
assert gate["status"] == "pass"
|
||||
assert len(gate.get("recommendations", [])) > 0
|
||||
|
||||
def test_score_above_warn_at_still_passes_in_warn_mode(self):
|
||||
"""In warn mode the gate always passes (overall_pass is controlled by caller)."""
|
||||
tm = _make_tool_manager(score=75)
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway", env="prod")
|
||||
)
|
||||
# _run_risk_watch itself always returns ok=True; caller drives strict logic
|
||||
assert ok is True
|
||||
assert gate["score"] == 75
|
||||
assert gate["band"] in ("high", "critical")
|
||||
|
||||
def test_warn_threshold_override(self):
|
||||
"""Caller can override warn_at via parameter."""
|
||||
tm = _make_tool_manager(score=40, warn_at=30)
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway",
|
||||
env="prod", warn_at=30)
|
||||
)
|
||||
assert gate["effective_warn_at"] == 30
|
||||
assert gate["score"] == 40 # >= 30, so recommendations should fire
|
||||
|
||||
|
||||
# ─── Strict mode tests ───────────────────────────────────────────────────────
|
||||
|
||||
class TestRiskWatchStrictMode:
|
||||
def test_score_above_fail_at_should_be_caught_by_caller(self):
|
||||
"""
|
||||
_run_risk_watch returns the gate data; the caller (release_check_runner)
|
||||
applies strict-mode logic. Verify effective_fail_at is correct.
|
||||
"""
|
||||
tm = _make_tool_manager(score=85, fail_at=80, service="gateway")
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway", env="prod")
|
||||
)
|
||||
assert gate["score"] == 85
|
||||
assert gate["effective_fail_at"] == 80
|
||||
# caller would check: score >= effective_fail_at → block in strict mode
|
||||
assert gate["score"] >= gate["effective_fail_at"]
|
||||
|
||||
def test_fail_threshold_override(self):
|
||||
"""Caller-supplied fail_at overrides policy value."""
|
||||
tm = _make_tool_manager(score=70, fail_at=80, service="gateway")
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway",
|
||||
env="staging", fail_at=65)
|
||||
)
|
||||
assert gate["effective_fail_at"] == 65 # override in effect
|
||||
assert gate["score"] >= gate["effective_fail_at"]
|
||||
|
||||
def test_score_below_fail_at_is_safe(self):
|
||||
tm = _make_tool_manager(score=60, fail_at=80, service="gateway")
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway", env="staging")
|
||||
)
|
||||
assert gate["score"] < gate["effective_fail_at"] # would not block
|
||||
|
||||
|
||||
# ─── Non-fatal error tests ────────────────────────────────────────────────────
|
||||
|
||||
class TestRiskWatchNonFatal:
|
||||
def test_tool_error_returns_skip(self):
|
||||
"""When risk_engine_tool raises, gate is skipped and ok=True."""
|
||||
tm = _make_tool_manager(score=0, fail_execute=True)
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway", env="prod")
|
||||
)
|
||||
assert ok is True
|
||||
assert gate.get("skipped") is True
|
||||
assert gate["status"] == "pass"
|
||||
|
||||
def test_tool_failure_result_returns_skip(self):
|
||||
"""When tool result.success=False, gate is skipped."""
|
||||
tm = MagicMock()
|
||||
result = MagicMock()
|
||||
result.success = False
|
||||
result.error = "tool unavailable"
|
||||
result.result = None
|
||||
tm.execute_tool = AsyncMock(return_value=result)
|
||||
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="gateway", env="prod")
|
||||
)
|
||||
assert ok is True
|
||||
assert gate.get("skipped") is True
|
||||
|
||||
def test_no_service_name_returns_skip(self):
|
||||
"""Missing service_name → skip, no calls made."""
|
||||
tm = MagicMock()
|
||||
tm.execute_tool = AsyncMock()
|
||||
|
||||
ok, gate = asyncio.run(
|
||||
_run_risk_watch(tm, "ops", service_name="", env="prod")
|
||||
)
|
||||
assert ok is True
|
||||
assert gate.get("skipped") is True
|
||||
tm.execute_tool.assert_not_called()
|
||||
Reference in New Issue
Block a user