Files
awoooi/apps/api/tests/test_anomaly_counter.py
OG T d89f0520f9 fix(api): 修復 34 個 Ruff lint 錯誤
- 自動修復 import 排序、unused imports
- 手動修復 raise from、isinstance union、unused variable
- scripts/ 暫時保留 (非 CI 阻擋)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-29 15:27:49 +08:00

197 lines
6.9 KiB
Python

"""
AnomalyCounter 整合測試
=======================
ADR-037: 監控增強架構 - 異常頻率統計
🔴🔴 遵循「禁止 Mock 測試鐵律」- 使用真實 Redis
建立: 2026-03-29 (台北時區) Claude Code
"""
import pytest
from src.services.anomaly_counter import AnomalyCounter, AnomalyFrequency
class TestAnomalyCounterHashSignature:
"""測試異常簽名 Hash 生成"""
def test_same_input_same_hash(self):
"""相同輸入應產生相同 hash"""
sig1 = {"alert_name": "PodCrash", "service": "api"}
sig2 = {"alert_name": "PodCrash", "service": "api"}
assert AnomalyCounter.hash_signature(sig1) == AnomalyCounter.hash_signature(sig2)
def test_different_input_different_hash(self):
"""不同輸入應產生不同 hash"""
sig1 = {"alert_name": "PodCrash", "service": "api"}
sig2 = {"alert_name": "PodCrash", "service": "web"}
assert AnomalyCounter.hash_signature(sig1) != AnomalyCounter.hash_signature(sig2)
def test_ignores_extra_fields(self):
"""應忽略非關鍵欄位 (如 timestamp)"""
sig1 = {"alert_name": "PodCrash", "service": "api"}
sig2 = {"alert_name": "PodCrash", "service": "api", "timestamp": "2026-01-01"}
assert AnomalyCounter.hash_signature(sig1) == AnomalyCounter.hash_signature(sig2)
def test_alertname_alias(self):
"""應支援 alertname (Prometheus 格式) 別名"""
sig1 = {"alert_name": "PodCrash", "service": "api"}
sig2 = {"alertname": "PodCrash", "service": "api"}
assert AnomalyCounter.hash_signature(sig1) == AnomalyCounter.hash_signature(sig2)
def test_job_alias(self):
"""應支援 job (Prometheus 格式) 別名"""
sig1 = {"alert_name": "PodCrash", "service": "api"}
sig2 = {"alert_name": "PodCrash", "job": "api"}
assert AnomalyCounter.hash_signature(sig1) == AnomalyCounter.hash_signature(sig2)
class TestAnomalyFrequencyToDict:
"""測試 AnomalyFrequency.to_dict()"""
def test_to_dict_returns_all_fields(self):
"""to_dict 應返回所有欄位"""
from datetime import datetime
freq = AnomalyFrequency(
anomaly_key="abc123",
count_1h=3,
count_24h=10,
count_7d=50,
count_30d=200,
first_seen=datetime(2026, 3, 1),
last_seen=datetime(2026, 3, 29),
auto_repair_count=5,
permanent_fix_applied=False,
escalation_level="ESCALATE",
)
d = freq.to_dict()
assert d["anomaly_key"] == "abc123"
assert d["count_1h"] == 3
assert d["count_24h"] == 10
assert d["count_7d"] == 50
assert d["count_30d"] == 200
assert d["auto_repair_count"] == 5
assert d["permanent_fix_applied"] is False
assert d["escalation_level"] == "ESCALATE"
class TestAnomalyCounterEscalationLevel:
"""測試升級等級判斷"""
def test_no_escalation_below_threshold(self):
"""低於閾值不應升級"""
counter = AnomalyCounter.__new__(AnomalyCounter)
assert counter._get_escalation_level(2) is None
def test_repeat_at_threshold(self):
"""達到 REPEAT 閾值 (3 次)"""
counter = AnomalyCounter.__new__(AnomalyCounter)
assert counter._get_escalation_level(3) == "REPEAT"
def test_escalate_at_threshold(self):
"""達到 ESCALATE 閾值 (5 次)"""
counter = AnomalyCounter.__new__(AnomalyCounter)
assert counter._get_escalation_level(5) == "ESCALATE"
def test_permanent_fix_at_threshold(self):
"""達到 PERMANENT_FIX 閾值 (10 次)"""
counter = AnomalyCounter.__new__(AnomalyCounter)
assert counter._get_escalation_level(10) == "PERMANENT_FIX"
def test_highest_level_wins(self):
"""超過最高閾值應返回最高等級"""
counter = AnomalyCounter.__new__(AnomalyCounter)
assert counter._get_escalation_level(100) == "PERMANENT_FIX"
# =============================================================================
# 整合測試 (需要真實 Redis)
# =============================================================================
@pytest.mark.asyncio
@pytest.mark.integration
class TestAnomalyCounterIntegration:
"""
整合測試 - 需要真實 Redis
執行方式: pytest -m integration tests/test_anomaly_counter.py
"""
@pytest.fixture
async def counter(self):
"""取得 AnomalyCounter 實例"""
from src.core.redis_client import get_redis
redis = get_redis()
counter = AnomalyCounter(redis)
# 清理測試資料
test_key = "test_anomaly_key"
await redis.delete(f"anomaly:timeline:{test_key}")
await redis.delete(f"anomaly:repair_count:{test_key}")
await redis.delete(f"anomaly:permanent_fix:{test_key}")
await redis.delete(f"anomaly:metadata:{test_key}")
await redis.delete(f"anomaly:repair_history:{test_key}")
return counter
async def test_record_anomaly_returns_frequency(self, counter):
"""record_anomaly 應返回頻率統計"""
signature = {
"alert_name": "TestAlert",
"service": "test-service",
"namespace": "test",
}
freq = await counter.record_anomaly(signature)
assert freq.count_1h >= 1
assert freq.count_24h >= 1
assert freq.anomaly_key is not None
async def test_record_anomaly_increments_count(self, counter):
"""多次記錄應遞增計數"""
signature = {
"alert_name": "TestAlert",
"service": "test-service",
"namespace": "test",
}
freq1 = await counter.record_anomaly(signature)
_ = await counter.record_anomaly(signature)
freq3 = await counter.record_anomaly(signature)
assert freq3.count_1h == freq1.count_1h + 2
assert freq3.count_24h == freq1.count_24h + 2
async def test_record_repair_attempt(self, counter):
"""記錄修復嘗試"""
anomaly_key = "test_repair_key"
await counter.record_repair_attempt(anomaly_key, "restart_pod", True)
await counter.record_repair_attempt(anomaly_key, "restart_pod", False)
stats = await counter.get_repair_success_rate(anomaly_key, "restart_pod")
assert stats["total"] == 2
assert stats["success"] == 1
assert stats["success_rate"] == 0.5
async def test_should_skip_action_low_success_rate(self, counter):
"""低成功率動作應跳過"""
anomaly_key = "test_skip_key"
# 模擬多次失敗
await counter.record_repair_attempt(anomaly_key, "bad_action", False)
await counter.record_repair_attempt(anomaly_key, "bad_action", False)
await counter.record_repair_attempt(anomaly_key, "bad_action", False)
should_skip = await counter.should_skip_action(anomaly_key, "bad_action")
assert should_skip is True