Files
awoooi/packages/lewooogo-brain/tests/test_incident_engine.py
OG T cb5d0ecfe4 feat(phase-6.4g-6.5b): API Synaptic Integration + Dual-State WarRoom UI
Phase 6.4g (API 突觸對接):
- lewooogo-brain dependency binding in apps/api/pyproject.toml
- POST /api/v1/incidents/{id}/propose route (proposals.py)
- Guardrails integration (8/8 tests passed)

Phase 6.5a (視覺皮層建置):
- DualStateIncidentCard.tsx with Nothing.tech visual compliance
- Ping radar animation for alert state
- Tier-based decision layer UI (AI 執行中 / 等待親核)

Phase 6.5b (神經網路串接):
- Main warroom page integration (page.tsx)
- IncidentResponse → DualState mapper function
- Empty state: "系統穩定。0 活躍異常。"

Tests:
- test_guardrails.py (8/8)
- test_incident_engine.py (6/6)
- test_skill_loader.py (6/6)
- Frontend build: 0 errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-23 11:58:28 +08:00

322 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
IncidentEngine 單元測試
========================
Phase 6.4e 驗證點 2
使用 Mock MemoryProvider 驗證 IncidentEngine 能正確處理告警信號
"""
import sys
from pathlib import Path
from datetime import datetime, timezone
from typing import Any
# 添加 src 到 Python Path
src_path = Path(__file__).parent.parent / "src"
sys.path.insert(0, str(src_path))
# =============================================================================
# Mock Memory Provider (完全隔離,不依賴外部)
# =============================================================================
class MockIncidentMemory:
"""Mock 記憶體提供者 - 純記憶體實作"""
def __init__(self):
self._incidents: dict[str, Any] = {}
self._ns_index: dict[str, str] = {} # namespace → incident_id
self._target_index: dict[str, str] = {} # target → incident_id
async def load_incident(self, incident_id: str):
"""載入 Incident"""
return self._incidents.get(incident_id)
async def save_incident(self, incident, ttl_seconds: int = 604800) -> bool:
"""儲存 Incident"""
self._incidents[incident.incident_id] = incident
return True
async def persist_incident(self, incident) -> bool:
"""持久化 (Mock 直接返回成功)"""
return True
async def find_related_incident(
self,
namespace: str,
target: str,
window_minutes: int = 30,
):
"""尋找相關 Incident"""
# 檢查 namespace 索引
if namespace in self._ns_index:
incident_id = self._ns_index[namespace]
incident = self._incidents.get(incident_id)
if incident and incident.status.value in ["investigating", "mitigating"]:
return incident
# 檢查 target 索引
if target in self._target_index:
incident_id = self._target_index[target]
incident = self._incidents.get(incident_id)
if incident and incident.status.value in ["investigating", "mitigating"]:
return incident
return None
async def update_index(
self,
incident_id: str,
namespace: str,
target: str,
) -> bool:
"""更新索引"""
self._ns_index[namespace] = incident_id
self._target_index[target] = incident_id
return True
class MockBlastRadiusAnalyzer:
"""Mock 爆炸半徑分析器"""
def analyze(self, target: str) -> list[str]:
"""返回受影響服務 (Mock 固定回應)"""
return [target, f"{target}-dependent"]
# =============================================================================
# 測試案例
# =============================================================================
def test_incident_engine_import():
"""測試:能正確 import IncidentEngine"""
from lewooogo_brain.engines.incident_engine import IncidentEngine
from lewooogo_brain.interfaces.incident_processor import IIncidentProcessor
assert issubclass(IncidentEngine, IIncidentProcessor)
print("✅ IncidentEngine import 成功,實作 IIncidentProcessor")
def test_incident_engine_create_incident():
"""測試:處理新告警時創建 Incident"""
import asyncio
from lewooogo_brain.engines.incident_engine import IncidentEngine
memory = MockIncidentMemory()
analyzer = MockBlastRadiusAnalyzer()
engine = IncidentEngine(memory=memory, blast_analyzer=analyzer)
signal_data = {
"source": "prometheus",
"alert_name": "HighCPUUsage",
"severity": "critical",
"namespace": "awoooi-prod",
"target": "awoooi-api",
"message": "CPU usage exceeded 90%",
"labels": {"app": "awoooi-api"},
}
async def run_test():
incident = await engine.process_signal(signal_data)
return incident
incident = asyncio.get_event_loop().run_until_complete(run_test())
assert incident is not None, "Failed to create incident"
assert incident.incident_id.startswith("INC-"), f"Invalid incident ID: {incident.incident_id}"
assert incident.severity.value == "P0", f"Expected P0, got {incident.severity.value}"
assert len(incident.signals) == 1, f"Expected 1 signal, got {len(incident.signals)}"
assert "awoooi-api" in incident.affected_services
print(f"✅ Incident 創建成功:")
print(f" - ID: {incident.incident_id}")
print(f" - Severity: {incident.severity.value}")
print(f" - Signals: {len(incident.signals)}")
print(f" - Affected: {incident.affected_services}")
def test_incident_engine_aggregate_signals():
"""測試:相關告警聚合到同一 Incident"""
import asyncio
from lewooogo_brain.engines.incident_engine import IncidentEngine
memory = MockIncidentMemory()
engine = IncidentEngine(memory=memory)
# 第一個告警
signal1 = {
"source": "prometheus",
"alert_name": "HighCPUUsage",
"severity": "warning",
"namespace": "awoooi-prod",
"target": "awoooi-api",
"message": "CPU at 80%",
}
# 相同 namespace/target 的第二個告警
signal2 = {
"source": "grafana",
"alert_name": "HighMemoryUsage",
"severity": "critical",
"namespace": "awoooi-prod",
"target": "awoooi-api",
"message": "Memory at 95%",
}
async def run_test():
incident1 = await engine.process_signal(signal1)
incident2 = await engine.process_signal(signal2)
return incident1, incident2
incident1, incident2 = asyncio.get_event_loop().run_until_complete(run_test())
assert incident1 is not None
assert incident2 is not None
assert incident1.incident_id == incident2.incident_id, "Signals should aggregate"
assert len(incident2.signals) == 2, f"Expected 2 signals, got {len(incident2.signals)}"
# 嚴重度應升級為 P0 (critical)
assert incident2.severity.value == "P0", f"Severity should escalate to P0"
print(f"✅ 告警聚合成功:")
print(f" - Incident ID: {incident2.incident_id}")
print(f" - Total Signals: {len(incident2.signals)}")
print(f" - Final Severity: {incident2.severity.value}")
def test_incident_engine_deduplication():
"""測試:相同 Fingerprint 的告警去重"""
import asyncio
from lewooogo_brain.engines.incident_engine import IncidentEngine
memory = MockIncidentMemory()
engine = IncidentEngine(memory=memory)
# 兩個完全相同的告警
signal = {
"source": "prometheus",
"alert_name": "PodCrashLooping",
"severity": "critical",
"namespace": "awoooi-prod",
"target": "awoooi-worker",
"message": "Pod restart count > 5",
}
async def run_test():
incident1 = await engine.process_signal(signal)
incident2 = await engine.process_signal(signal) # 重複
return incident1, incident2
incident1, incident2 = asyncio.get_event_loop().run_until_complete(run_test())
assert incident1 is not None
assert incident2 is not None
assert incident1.incident_id == incident2.incident_id
# 重複告警應被去重signal 數量仍為 1
assert len(incident2.signals) == 1, f"Expected 1 signal (dedup), got {len(incident2.signals)}"
print(f"✅ 告警去重成功:")
print(f" - Signals after dedup: {len(incident2.signals)}")
def test_incident_engine_update_status():
"""測試:更新 Incident 狀態"""
import asyncio
from lewooogo_brain.engines.incident_engine import IncidentEngine
from lewooogo_brain.interfaces.incident_processor import IncidentStatus
memory = MockIncidentMemory()
engine = IncidentEngine(memory=memory)
signal = {
"source": "test",
"alert_name": "TestAlert",
"severity": "warning",
"namespace": "test",
"target": "test-service",
}
async def run_test():
incident = await engine.process_signal(signal)
assert incident.status == IncidentStatus.INVESTIGATING
success = await engine.update_status(incident.incident_id, IncidentStatus.RESOLVED)
assert success, "Failed to update status"
updated = await engine.get_incident(incident.incident_id)
return updated
updated = asyncio.get_event_loop().run_until_complete(run_test())
assert updated is not None
assert updated.status == IncidentStatus.RESOLVED
assert updated.resolved_at is not None
print(f"✅ 狀態更新成功:")
print(f" - Status: {updated.status.value}")
print(f" - Resolved At: {updated.resolved_at}")
def test_incident_engine_no_external_deps():
"""測試IncidentEngine 不依賴任何外部模組"""
import importlib
import lewooogo_brain.engines.incident_engine as module
# 取得所有 import
source = Path(module.__file__).read_text()
# 禁止的 import patterns
forbidden = [
"from src.core",
"from src.db",
"from src.services",
"import redis",
"from redis",
"import sqlalchemy",
"from sqlalchemy",
]
violations = []
for pattern in forbidden:
if pattern in source:
violations.append(pattern)
assert len(violations) == 0, f"Found forbidden imports: {violations}"
print("✅ 無外部依賴,完全積木化")
if __name__ == "__main__":
print("=" * 60)
print("🧪 IncidentEngine 單元測試")
print("=" * 60)
tests = [
test_incident_engine_import,
test_incident_engine_create_incident,
test_incident_engine_aggregate_signals,
test_incident_engine_deduplication,
test_incident_engine_update_status,
test_incident_engine_no_external_deps,
]
passed = 0
failed = 0
for test in tests:
print(f"\n🔬 {test.__name__}")
try:
test()
passed += 1
except AssertionError as e:
print(f"❌ FAILED: {e}")
failed += 1
except Exception as e:
print(f"❌ ERROR: {type(e).__name__}: {e}")
failed += 1
print("\n" + "=" * 60)
print(f"📊 結果: {passed} 通過, {failed} 失敗")
print("=" * 60)
if failed > 0:
sys.exit(1)