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:
319
tests/test_risk_engine.py
Normal file
319
tests/test_risk_engine.py
Normal file
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
tests/test_risk_engine.py — Unit tests for the Service Risk Index Engine.
|
||||
|
||||
Tests scoring components, band classification, service threshold overrides,
|
||||
and full RiskReport assembly — all deterministic, no I/O required.
|
||||
"""
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure router module path is on sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "services" / "router"))
|
||||
|
||||
from risk_engine import (
|
||||
load_risk_policy,
|
||||
_builtin_defaults,
|
||||
_reload_policy,
|
||||
score_to_band,
|
||||
get_service_thresholds,
|
||||
_score_open_incidents,
|
||||
_score_recurrence,
|
||||
_score_followups,
|
||||
_score_slo,
|
||||
_score_alerts_loop,
|
||||
_score_escalations,
|
||||
compute_service_risk,
|
||||
compute_risk_dashboard,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_policy_cache():
|
||||
"""Reset the in-memory policy cache before each test."""
|
||||
_reload_policy()
|
||||
yield
|
||||
_reload_policy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def policy():
|
||||
return _builtin_defaults()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def weights(policy):
|
||||
return policy["weights"]
|
||||
|
||||
|
||||
# ─── Band classification ──────────────────────────────────────────────────────
|
||||
|
||||
class TestBands:
|
||||
def test_low(self, policy):
|
||||
assert score_to_band(0, policy) == "low"
|
||||
assert score_to_band(20, policy) == "low"
|
||||
|
||||
def test_medium(self, policy):
|
||||
assert score_to_band(21, policy) == "medium"
|
||||
assert score_to_band(50, policy) == "medium"
|
||||
|
||||
def test_high(self, policy):
|
||||
assert score_to_band(51, policy) == "high"
|
||||
assert score_to_band(80, policy) == "high"
|
||||
|
||||
def test_critical(self, policy):
|
||||
assert score_to_band(81, policy) == "critical"
|
||||
assert score_to_band(200, policy) == "critical"
|
||||
|
||||
|
||||
# ─── Open incidents scoring ───────────────────────────────────────────────────
|
||||
|
||||
class TestOpenIncidents:
|
||||
def test_no_incidents(self, weights):
|
||||
pts, comp, reasons = _score_open_incidents([], weights)
|
||||
assert pts == 0
|
||||
assert comp["P0"] == 0
|
||||
assert reasons == []
|
||||
|
||||
def test_single_p0(self, weights):
|
||||
incs = [{"id": "i1", "severity": "P0", "status": "open"}]
|
||||
pts, comp, reasons = _score_open_incidents(incs, weights)
|
||||
assert pts == 50
|
||||
assert comp["P0"] == 1
|
||||
assert "P0" in reasons[0]
|
||||
|
||||
def test_p1_p2_combined(self, weights):
|
||||
incs = [
|
||||
{"id": "i1", "severity": "P1", "status": "open"},
|
||||
{"id": "i2", "severity": "P2", "status": "open"},
|
||||
{"id": "i3", "severity": "P2", "status": "open"},
|
||||
]
|
||||
pts, comp, reasons = _score_open_incidents(incs, weights)
|
||||
assert pts == 25 + 10 + 10 # 45
|
||||
assert comp["P1"] == 1
|
||||
assert comp["P2"] == 2
|
||||
|
||||
def test_unknown_severity(self, weights):
|
||||
incs = [{"id": "i1", "severity": "P9", "status": "open"}]
|
||||
pts, comp, reasons = _score_open_incidents(incs, weights)
|
||||
assert pts == 0
|
||||
|
||||
|
||||
# ─── Recurrence scoring ───────────────────────────────────────────────────────
|
||||
|
||||
class TestRecurrence:
|
||||
def _make_rec(self, high_sigs=0, high_kinds=0, warn_sigs=0, warn_kinds=0):
|
||||
return {
|
||||
"high_recurrence": {
|
||||
"signatures": [f"sig_{i}" for i in range(high_sigs)],
|
||||
"kinds": [f"kind_{i}" for i in range(high_kinds)],
|
||||
},
|
||||
"warn_recurrence": {
|
||||
"signatures": [f"wsig_{i}" for i in range(warn_sigs)],
|
||||
"kinds": [f"wkind_{i}" for i in range(warn_kinds)],
|
||||
},
|
||||
}
|
||||
|
||||
def test_no_recurrence(self, weights):
|
||||
pts, comp, reasons = _score_recurrence({}, weights)
|
||||
assert pts == 0
|
||||
assert reasons == []
|
||||
|
||||
def test_one_high_signature(self, weights):
|
||||
rec = self._make_rec(high_sigs=1)
|
||||
pts, comp, reasons = _score_recurrence(rec, weights)
|
||||
assert pts == 20 # signature_high_7d = 20
|
||||
assert comp["high_signatures_7d"] == 1
|
||||
assert any("High recurrence signatures" in r for r in reasons)
|
||||
|
||||
def test_high_kinds_adds_points(self, weights):
|
||||
rec = self._make_rec(high_kinds=2)
|
||||
pts, comp, _ = _score_recurrence(rec, weights)
|
||||
assert pts == 15 * 2 # kind_high_7d = 15 each
|
||||
|
||||
def test_warn_signature(self, weights):
|
||||
rec = self._make_rec(warn_sigs=1)
|
||||
pts, comp, _ = _score_recurrence(rec, weights)
|
||||
assert pts == 10 # signature_warn_7d = 10
|
||||
|
||||
|
||||
# ─── Follow-ups scoring ───────────────────────────────────────────────────────
|
||||
|
||||
class TestFollowups:
|
||||
def test_no_followups(self, weights):
|
||||
pts, comp, reasons = _score_followups({}, weights)
|
||||
assert pts == 0
|
||||
|
||||
def test_overdue_p0(self, weights):
|
||||
data = {"overdue_followups": [{"priority": "P0"}]}
|
||||
pts, comp, reasons = _score_followups(data, weights)
|
||||
assert pts == 20
|
||||
assert comp["P0"] == 1
|
||||
assert "P0" in reasons[0]
|
||||
|
||||
def test_overdue_p1(self, weights):
|
||||
data = {"overdue_followups": [{"priority": "P1"}]}
|
||||
pts, comp, reasons = _score_followups(data, weights)
|
||||
assert pts == 12
|
||||
assert comp["P1"] == 1
|
||||
|
||||
def test_overdue_mixed(self, weights):
|
||||
data = {
|
||||
"overdue_followups": [
|
||||
{"priority": "P0"},
|
||||
{"priority": "P1"},
|
||||
{"priority": "P2"},
|
||||
]
|
||||
}
|
||||
pts, comp, _ = _score_followups(data, weights)
|
||||
assert pts == 20 + 12 + 6 # 38
|
||||
|
||||
|
||||
# ─── SLO scoring ─────────────────────────────────────────────────────────────
|
||||
|
||||
class TestSlo:
|
||||
def test_no_violations(self, weights):
|
||||
pts, comp, reasons = _score_slo({"violations": []}, weights)
|
||||
assert pts == 0
|
||||
assert reasons == []
|
||||
|
||||
def test_one_violation(self, weights):
|
||||
pts, comp, reasons = _score_slo({"violations": [{"metric": "error_rate"}]}, weights)
|
||||
assert pts == 10
|
||||
assert comp["violations"] == 1
|
||||
assert reasons
|
||||
|
||||
def test_two_violations(self, weights):
|
||||
slo = {"violations": [{"m": "latency"}, {"m": "error"}]}
|
||||
pts, comp, _ = _score_slo(slo, weights)
|
||||
assert pts == 20
|
||||
assert comp["violations"] == 2
|
||||
|
||||
def test_skipped(self, weights):
|
||||
pts, comp, _ = _score_slo({"violations": [], "skipped": True}, weights)
|
||||
assert pts == 0
|
||||
assert comp["skipped"] is True
|
||||
|
||||
|
||||
# ─── Alert-loop SLO scoring ───────────────────────────────────────────────────
|
||||
|
||||
class TestAlertsLoop:
|
||||
def test_no_violations(self, weights):
|
||||
pts, comp, _ = _score_alerts_loop({}, weights)
|
||||
assert pts == 0
|
||||
|
||||
def test_one_loop_violation(self, weights):
|
||||
pts, comp, reasons = _score_alerts_loop({"violations": [{"type": "missed_slo"}]}, weights)
|
||||
assert pts == 10
|
||||
assert reasons
|
||||
|
||||
|
||||
# ─── Escalation scoring ──────────────────────────────────────────────────────
|
||||
|
||||
class TestEscalations:
|
||||
def test_no_escalations(self, weights):
|
||||
pts, comp, _ = _score_escalations(0, weights)
|
||||
assert pts == 0
|
||||
|
||||
def test_warn_level(self, weights):
|
||||
pts, comp, reasons = _score_escalations(1, weights)
|
||||
assert pts == 5 # warn level
|
||||
assert comp["count_24h"] == 1
|
||||
assert reasons
|
||||
|
||||
def test_high_level(self, weights):
|
||||
pts, comp, reasons = _score_escalations(3, weights)
|
||||
assert pts == 12 # high level
|
||||
|
||||
def test_high_level_more(self, weights):
|
||||
pts, comp, _ = _score_escalations(10, weights)
|
||||
assert pts == 12 # capped at high
|
||||
|
||||
|
||||
# ─── Full compute_service_risk ────────────────────────────────────────────────
|
||||
|
||||
class TestComputeServiceRisk:
|
||||
def test_zero_risk_empty_inputs(self, policy):
|
||||
report = compute_service_risk(
|
||||
"gateway", "prod",
|
||||
open_incidents=[],
|
||||
recurrence_7d={},
|
||||
followups_data={},
|
||||
slo_data={"violations": []},
|
||||
alerts_loop_slo={},
|
||||
escalation_count_24h=0,
|
||||
policy=policy,
|
||||
)
|
||||
assert report["score"] == 0
|
||||
assert report["band"] == "low"
|
||||
assert report["service"] == "gateway"
|
||||
assert report["env"] == "prod"
|
||||
assert isinstance(report["reasons"], list)
|
||||
assert isinstance(report["recommendations"], list)
|
||||
assert "updated_at" in report
|
||||
|
||||
def test_p0_open_incident(self, policy):
|
||||
report = compute_service_risk(
|
||||
"gateway", "prod",
|
||||
open_incidents=[{"id": "i1", "severity": "P0", "status": "open"}],
|
||||
policy=policy,
|
||||
)
|
||||
assert report["score"] == 50
|
||||
assert report["band"] == "medium"
|
||||
assert report["components"]["open_incidents"]["P0"] == 1
|
||||
|
||||
def test_full_high_risk(self, policy):
|
||||
"""Combining several signals pushes score into 'high' band."""
|
||||
report = compute_service_risk(
|
||||
"gateway", "prod",
|
||||
open_incidents=[
|
||||
{"id": "i1", "severity": "P1", "status": "open"},
|
||||
{"id": "i2", "severity": "P2", "status": "open"},
|
||||
],
|
||||
recurrence_7d={
|
||||
"high_recurrence": {"signatures": ["bucket_A"], "kinds": []},
|
||||
"warn_recurrence": {"signatures": [], "kinds": []},
|
||||
},
|
||||
followups_data={"overdue_followups": [{"priority": "P1"}]},
|
||||
slo_data={"violations": [{"metric": "error_rate"}]},
|
||||
escalation_count_24h=1,
|
||||
policy=policy,
|
||||
)
|
||||
# P1=25, P2=10, high_sig_7d=20, overdue_P1=12, slo=10, esc_warn=5 → 82
|
||||
assert report["score"] >= 70
|
||||
assert report["band"] in ("high", "critical")
|
||||
assert len(report["recommendations"]) > 0
|
||||
|
||||
def test_recommendations_present_for_high_score(self, policy):
|
||||
report = compute_service_risk(
|
||||
"router", "prod",
|
||||
open_incidents=[{"id": "i1", "severity": "P0", "status": "open"}],
|
||||
slo_data={"violations": [{"m": "latency"}]},
|
||||
policy=policy,
|
||||
)
|
||||
assert any("P0" in r or "SLO" in r for r in report["recommendations"])
|
||||
|
||||
|
||||
# ─── Service threshold overrides ─────────────────────────────────────────────
|
||||
|
||||
class TestServiceOverrides:
|
||||
def test_gateway_fail_at_75(self):
|
||||
"""risk_policy.yml defines gateway.risk_watch.fail_at = 75."""
|
||||
policy = load_risk_policy()
|
||||
thresholds = get_service_thresholds("gateway", policy)
|
||||
assert thresholds["fail_at"] == 75
|
||||
|
||||
def test_router_fail_at_80(self):
|
||||
policy = load_risk_policy()
|
||||
thresholds = get_service_thresholds("router", policy)
|
||||
assert thresholds["fail_at"] == 80
|
||||
|
||||
def test_unknown_service_default(self):
|
||||
policy = load_risk_policy()
|
||||
thresholds = get_service_thresholds("unknown-svc", policy)
|
||||
assert thresholds["fail_at"] >= 75 # must have a value
|
||||
|
||||
def test_threshold_reflected_in_report(self):
|
||||
policy = load_risk_policy()
|
||||
report = compute_service_risk("gateway", "prod", policy=policy)
|
||||
assert report["thresholds"]["fail_at"] == 75
|
||||
Reference in New Issue
Block a user