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
204 lines
7.9 KiB
Python
204 lines
7.9 KiB
Python
"""
|
|
Tests for postmortem_draft_graph.
|
|
|
|
Mocks the GatewayClient — no real network calls.
|
|
"""
|
|
|
|
import asyncio
|
|
import base64
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
from tests.conftest import MockGatewayClient, _run
|
|
|
|
|
|
# ─── Mock data ────────────────────────────────────────────────────────────────
|
|
|
|
_INCIDENT_DATA = {
|
|
"id": "inc_20260223_1000_abc123",
|
|
"service": "router",
|
|
"env": "prod",
|
|
"severity": "P1",
|
|
"status": "open",
|
|
"title": "Router OOM",
|
|
"summary": "Router pods running out of memory under high load",
|
|
"started_at": "2026-02-23T10:00:00Z",
|
|
"ended_at": None,
|
|
"created_by": "sofiia",
|
|
"events": [
|
|
{"ts": "2026-02-23T10:01:00Z", "type": "note", "message": "Memory usage >90%"},
|
|
{"ts": "2026-02-23T10:10:00Z", "type": "action", "message": "Restarted pods"},
|
|
],
|
|
"artifacts": [],
|
|
}
|
|
|
|
_INCIDENT_WITH_TRIAGE = {
|
|
**_INCIDENT_DATA,
|
|
"artifacts": [
|
|
{"kind": "triage_report", "format": "json", "path": "ops/incidents/inc_test/triage_report.json"},
|
|
],
|
|
}
|
|
|
|
_OVERVIEW_DATA = {
|
|
"status": "degraded",
|
|
"alerts": [{"name": "OOMKilled", "severity": "critical"}],
|
|
}
|
|
|
|
_HEALTH_DATA = {"status": "unhealthy", "error": "OOM"}
|
|
|
|
_KB_DATA = {"results": [
|
|
{"path": "docs/runbooks/oom.md", "content": "## OOM Runbook\n- Check memory limits\n- Restart pods"}
|
|
]}
|
|
|
|
|
|
# ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
class TestPostmortemDraftGraph:
|
|
"""Happy path: incident exists, triage exists, postmortem generated."""
|
|
|
|
def test_happy_path_with_triage(self):
|
|
from app.graphs.postmortem_draft_graph import build_postmortem_draft_graph
|
|
|
|
mock_gw = MockGatewayClient()
|
|
mock_gw.register("oncall_tool", "incident_get", _INCIDENT_WITH_TRIAGE)
|
|
mock_gw.register("oncall_tool", "incident_attach_artifact", {"artifact": {"path": "test", "sha256": "abc"}})
|
|
mock_gw.register("oncall_tool", "incident_append_event", {"event": {"ts": "now", "type": "followup"}})
|
|
|
|
graph = build_postmortem_draft_graph()
|
|
|
|
with patch("app.graphs.postmortem_draft_graph.GatewayClient", return_value=mock_gw):
|
|
result = _run(graph.ainvoke({
|
|
"run_id": "gr_test_01",
|
|
"agent_id": "sofiia",
|
|
"workspace_id": "ws1",
|
|
"user_id": "u1",
|
|
"input": {
|
|
"incident_id": "inc_20260223_1000_abc123",
|
|
"service": "router",
|
|
},
|
|
}))
|
|
|
|
assert result["graph_status"] == "succeeded"
|
|
pm = result["result"]
|
|
assert pm["incident_id"] == "inc_20260223_1000_abc123"
|
|
assert pm["artifacts_count"] >= 2 # md + json
|
|
assert "postmortem" in result["postmortem_md"].lower()
|
|
|
|
def test_triage_missing_triggers_generation(self):
|
|
"""When incident has no triage artifact, the graph generates one."""
|
|
from app.graphs.postmortem_draft_graph import build_postmortem_draft_graph
|
|
|
|
mock_gw = MockGatewayClient()
|
|
mock_gw.register("oncall_tool", "incident_get", _INCIDENT_DATA) # no triage artifact
|
|
mock_gw.register("observability_tool", "service_overview", _OVERVIEW_DATA)
|
|
mock_gw.register("oncall_tool", "service_health", _HEALTH_DATA)
|
|
mock_gw.register("kb_tool", "search", _KB_DATA)
|
|
mock_gw.register("oncall_tool", "incident_attach_artifact", {"artifact": {"path": "t", "sha256": "x"}})
|
|
mock_gw.register("oncall_tool", "incident_append_event", {"event": {}})
|
|
|
|
graph = build_postmortem_draft_graph()
|
|
|
|
with patch("app.graphs.postmortem_draft_graph.GatewayClient", return_value=mock_gw):
|
|
result = _run(graph.ainvoke({
|
|
"run_id": "gr_test_02",
|
|
"agent_id": "sofiia",
|
|
"workspace_id": "ws1",
|
|
"user_id": "u1",
|
|
"input": {"incident_id": "inc_20260223_1000_abc123"},
|
|
}))
|
|
|
|
assert result["graph_status"] == "succeeded"
|
|
assert result.get("triage_was_generated") is True
|
|
# Should have triage + postmortem artifacts (3 total)
|
|
assert result["result"]["artifacts_count"] >= 2
|
|
|
|
def test_incident_not_found_fails_gracefully(self):
|
|
from app.graphs.postmortem_draft_graph import build_postmortem_draft_graph
|
|
|
|
mock_gw = MockGatewayClient()
|
|
mock_gw.register("oncall_tool", "incident_get", None, error="Incident not found")
|
|
|
|
graph = build_postmortem_draft_graph()
|
|
|
|
with patch("app.graphs.postmortem_draft_graph.GatewayClient", return_value=mock_gw):
|
|
result = _run(graph.ainvoke({
|
|
"run_id": "gr_test_03",
|
|
"agent_id": "sofiia",
|
|
"workspace_id": "ws1",
|
|
"user_id": "u1",
|
|
"input": {"incident_id": "inc_nonexistent"},
|
|
}))
|
|
|
|
assert result["graph_status"] == "failed"
|
|
assert "not found" in (result.get("error") or "").lower()
|
|
|
|
def test_missing_incident_id_fails(self):
|
|
from app.graphs.postmortem_draft_graph import build_postmortem_draft_graph
|
|
|
|
mock_gw = MockGatewayClient()
|
|
graph = build_postmortem_draft_graph()
|
|
|
|
with patch("app.graphs.postmortem_draft_graph.GatewayClient", return_value=mock_gw):
|
|
result = _run(graph.ainvoke({
|
|
"run_id": "gr_test_04",
|
|
"agent_id": "sofiia",
|
|
"workspace_id": "ws1",
|
|
"user_id": "u1",
|
|
"input": {},
|
|
}))
|
|
|
|
assert result["graph_status"] == "failed"
|
|
assert "incident_id" in (result.get("validation_error") or "").lower()
|
|
|
|
def test_gateway_error_on_followup_nonfatal(self):
|
|
"""If follow-up append fails, graph still succeeds."""
|
|
from app.graphs.postmortem_draft_graph import build_postmortem_draft_graph
|
|
|
|
mock_gw = MockGatewayClient()
|
|
mock_gw.register("oncall_tool", "incident_get", _INCIDENT_WITH_TRIAGE)
|
|
mock_gw.register("oncall_tool", "incident_attach_artifact", {"artifact": {"path": "t", "sha256": "x"}})
|
|
mock_gw.register("oncall_tool", "incident_append_event", None, error="gateway timeout")
|
|
|
|
graph = build_postmortem_draft_graph()
|
|
|
|
with patch("app.graphs.postmortem_draft_graph.GatewayClient", return_value=mock_gw):
|
|
result = _run(graph.ainvoke({
|
|
"run_id": "gr_test_05",
|
|
"agent_id": "sofiia",
|
|
"workspace_id": "ws1",
|
|
"user_id": "u1",
|
|
"input": {"incident_id": "inc_20260223_1000_abc123"},
|
|
}))
|
|
|
|
assert result["graph_status"] == "succeeded"
|
|
# followups may be 0 due to error, but graph still completed
|
|
assert result["result"]["followups_count"] == 0
|
|
|
|
def test_correlation_ids_present(self):
|
|
from app.graphs.postmortem_draft_graph import build_postmortem_draft_graph
|
|
|
|
mock_gw = MockGatewayClient()
|
|
mock_gw.register("oncall_tool", "incident_get", _INCIDENT_WITH_TRIAGE)
|
|
mock_gw.register("oncall_tool", "incident_attach_artifact", {"artifact": {}})
|
|
mock_gw.register("oncall_tool", "incident_append_event", {"event": {}})
|
|
|
|
graph = build_postmortem_draft_graph()
|
|
|
|
with patch("app.graphs.postmortem_draft_graph.GatewayClient", return_value=mock_gw):
|
|
_run(graph.ainvoke({
|
|
"run_id": "gr_corr_01",
|
|
"agent_id": "sofiia",
|
|
"workspace_id": "ws1",
|
|
"user_id": "u1",
|
|
"input": {"incident_id": "inc_20260223_1000_abc123"},
|
|
}))
|
|
|
|
# All calls should have graph_run_id
|
|
for call in mock_gw.calls:
|
|
assert call["graph_run_id"] == "gr_corr_01"
|