""" AlertGroupingService 單元測試 ============================== ADR-076: 告警聚合引擎 — 告警風暴防禦 🔴🔴 遵循「禁止 Mock 測試鐵律」 - build_group_key / GroupingResult 邏輯測試:純 Python,無需 Redis - Redis 整合部分標記 @pytest.mark.integration,正常 CI 跳過 建立: 2026-04-14 (台北時區) Claude Haiku 4.5 """ import pytest from src.services.alert_grouping_service import ( AlertGroupingService, GroupingResult, _decode_redis_member, ) class TestBuildGroupKey: """測試聚合分組 key 生成邏輯""" def test_basic_key(self): """基本 alertname + namespace → group_key""" key = AlertGroupingService.build_group_key("PodCrashLoopBackOff", "awoooi-prod") assert key == "PodCrashLoopBackOff:awoooi-prod" def test_strips_numeric_suffix(self): """帶數字後綴的 alertname 應取前綴""" key = AlertGroupingService.build_group_key("PodCrashLoopBackOff-3", "awoooi-prod") assert key == "PodCrashLoopBackOff:awoooi-prod" def test_strips_long_numeric_suffix(self): """帶長數字後綴的 alertname 應取前綴""" key = AlertGroupingService.build_group_key("HostHighCpuLoad-1234567", "default") assert key == "HostHighCpuLoad:default" def test_same_prefix_same_key(self): """相同前綴、相同 namespace → 相同 group_key(聚合生效)""" key1 = AlertGroupingService.build_group_key("PodOOMKilled-1", "awoooi-prod") key2 = AlertGroupingService.build_group_key("PodOOMKilled-2", "awoooi-prod") key3 = AlertGroupingService.build_group_key("PodOOMKilled-3", "awoooi-prod") assert key1 == key2 == key3 def test_different_namespace_different_key(self): """相同 alertname、不同 namespace → 不同 group_key""" key1 = AlertGroupingService.build_group_key("PodCrash", "awoooi-prod") key2 = AlertGroupingService.build_group_key("PodCrash", "awoooi-staging") assert key1 != key2 def test_different_alertname_different_key(self): """不同 alertname、相同 namespace → 不同 group_key""" key1 = AlertGroupingService.build_group_key("PodCrash", "awoooi-prod") key2 = AlertGroupingService.build_group_key("HostHighCpu", "awoooi-prod") assert key1 != key2 def test_empty_namespace(self): """namespace 為空字串時應正常處理""" key = AlertGroupingService.build_group_key("PodCrash", "") assert key == "PodCrash:" def test_no_suffix_unchanged(self): """無數字後綴的 alertname 應保持不變""" key = AlertGroupingService.build_group_key("HostHighCpuLoad", "default") assert key == "HostHighCpuLoad:default" class TestGroupingResultDataclass: """測試 GroupingResult dataclass""" def test_child_alert(self): """子告警:is_grouped=True, is_parent=False""" result = GroupingResult( is_grouped=True, group_key="PodCrash:awoooi-prod", count=5, parent_fingerprint="fp-001", is_parent=False, ) assert result.is_grouped is True assert result.is_parent is False assert result.count == 5 def test_parent_alert(self): """父告警:is_grouped=False, is_parent=True""" result = GroupingResult( is_grouped=False, group_key="PodCrash:awoooi-prod", count=1, parent_fingerprint="fp-001", is_parent=True, ) assert result.is_grouped is False assert result.is_parent is True def test_second_alert_is_now_grouped(self): """門檻為 2:第一張保留主卡,第二張同組告警開始收斂。""" result = GroupingResult( is_grouped=True, group_key="PodCrash:awoooi-prod", count=2, parent_fingerprint="fp-001", is_parent=False, ) assert result.is_grouped is True def test_group_key_format(self): """group_key 格式應為 {alertname_prefix}:{namespace}""" result = GroupingResult( is_grouped=True, group_key="PodOOMKilled:awoooi-prod", count=4, parent_fingerprint=None, is_parent=False, ) assert ":" in result.group_key parts = result.group_key.split(":") assert len(parts) == 2 class TestRedisMemberDecode: """測試 Redis zrange member 正規化。""" def test_decode_bytes_member(self): assert _decode_redis_member(b"fp-parent", "fallback") == "fp-parent" def test_decode_none_uses_fallback(self): assert _decode_redis_member(None, "fallback") == "fallback" class TestAlertGroupingServiceConstants: """測試服務常量設定""" def test_window_seconds(self): """視窗應為 5 分鐘 (300 秒)""" assert AlertGroupingService.WINDOW_SECONDS == 300 def test_group_threshold(self): """聚合閾值應為 2:保留第一張主卡,第二張同組告警開始收斂。""" assert AlertGroupingService.GROUP_THRESHOLD == 2 def test_ttl_seconds(self): """TTL 應長於視窗""" assert AlertGroupingService.TTL_SECONDS > AlertGroupingService.WINDOW_SECONDS def test_redis_key_prefix(self): """Redis key 前綴應符合規範""" assert AlertGroupingService.PREFIX_WINDOW.startswith("alert_group:") assert AlertGroupingService.PREFIX_META.startswith("alert_group:")