All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 17m34s
Task 2: AlertGroupingService — Redis 5分鐘滑動視窗,防告警風暴 - apps/api/src/services/alert_grouping_service.py (新增) - webhooks.py 整合:指紋生成後/LLM前短路子告警 - Threshold=3,Graceful Degradation,16 tests Task 3: approval_execution.py 執行失敗重試 - MAX_RETRY=2, RETRY_DELAY_SECONDS=30 - _is_transient_error() 瞬態/永久分類,永久錯誤不重試 - Timeline 記錄重試進度,成功後標注重試次數,29 tests Task 4: report_generation_service.py 自動報告 - 日度巡檢報告:每日 08:00 台北時間,Telegram SRE 群組推送 - Postmortem:Incident resolved + duration > 10 分鐘自動觸發 - main.py lifespan 掛載 run_daily_report_loop(),30 tests 測試: 600 → 675 通過 (+75),0 failed Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
138 lines
5.0 KiB
Python
138 lines
5.0 KiB
Python
"""
|
||
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
|
||
|
||
|
||
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_below_threshold_not_grouped(self):
|
||
"""未達閾值:count=2, threshold=3 → is_grouped=False"""
|
||
result = GroupingResult(
|
||
is_grouped=False,
|
||
group_key="PodCrash:awoooi-prod",
|
||
count=2,
|
||
parent_fingerprint="fp-001",
|
||
is_parent=False,
|
||
)
|
||
assert result.is_grouped is False
|
||
|
||
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 TestAlertGroupingServiceConstants:
|
||
"""測試服務常量設定"""
|
||
|
||
def test_window_seconds(self):
|
||
"""視窗應為 5 分鐘 (300 秒)"""
|
||
assert AlertGroupingService.WINDOW_SECONDS == 300
|
||
|
||
def test_group_threshold(self):
|
||
"""聚合閾值應為 3"""
|
||
assert AlertGroupingService.GROUP_THRESHOLD == 3
|
||
|
||
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:")
|