""" 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