""" classify_alert_early() 分類函數單元測試 — ADR-073 Phase 2-2 + ADR-075 覆蓋 13 條分類規則、10 種告警分類: config_drift (TYPE-4D) alertchain_health, flywheel_health (TYPE-8M) — beats severity info, backup (TYPE-1) host_resource, infrastructure, kubernetes, database, storage, devops_tool, external_site, ssl_cert, general (TYPE-3) 2026-04-12 ogt (ADR-073 P2-2 測試補強 + ADR-075 七類新增) """ import pytest from src.services.incident_service import classify_alert_early # --------------------------------------------------------------------------- # # TYPE-4D: Config Drift # --------------------------------------------------------------------------- # class TestConfigDrift: def test_configuration_drift(self): ac, nt = classify_alert_early("ConfigurationDrift", "critical", {}) assert nt == "TYPE-4D" assert ac == "config_drift" def test_kube_config_drift(self): ac, nt = classify_alert_early("KubeConfigDrift", "warning", {}) assert nt == "TYPE-4D" assert ac == "config_drift" def test_config_drift_case_sensitive(self): # 不在白名單裡的變體 → 不應命中 config_drift ac, nt = classify_alert_early("configurationdrift", "critical", {}) assert ac != "config_drift" # --------------------------------------------------------------------------- # # TYPE-1: Info / Heartbeat # --------------------------------------------------------------------------- # class TestInfoAlerts: def test_severity_info(self): ac, nt = classify_alert_early("SomeAlert", "info", {}) assert nt == "TYPE-1" assert ac == "info" def test_severity_none(self): ac, nt = classify_alert_early("SomeAlert", "none", {}) assert nt == "TYPE-1" assert ac == "info" def test_backup_keyword_info_only(self): # severity=info → severity 規則先命中,TYPE-1 ac, nt = classify_alert_early("BackupJobComplete", "info", {}) assert nt == "TYPE-1" def test_backup_keyword_warning_not_type1(self): # BackupJobFailed severity=warning → 繼續走 prefix 規則,不應是 TYPE-1 ac, nt = classify_alert_early("BackupJobFailed", "warning", {}) assert nt == "TYPE-3" def test_watchdog_heartbeat(self): # Watchdog (Alertmanager 心跳) severity=none → severity 規則先命中,TYPE-1 ac, nt = classify_alert_early("Watchdog", "none", {}) assert nt == "TYPE-1" def test_deadmansswitch_heartbeat(self): # DeadMansSwitch 心跳 → TYPE-1(補入 2026-04-12 ogt) ac, nt = classify_alert_early("DeadMansSwitch", "warning", {}) assert ac == "backup" assert nt == "TYPE-1" def test_backup_critical_not_type1(self): # critical backup 告警應走各自 prefix,不是純資訊 ac, nt = classify_alert_early("BACKUP_MISSING", "critical", {}) assert nt == "TYPE-3" # --------------------------------------------------------------------------- # # TYPE-3: Infrastructure (Docker / Host) # --------------------------------------------------------------------------- # class TestInfrastructure: def test_docker_prefix(self): ac, nt = classify_alert_early("DockerContainerOOM", "critical", {}) assert nt == "TYPE-3" assert ac == "infrastructure" # Docker → 保留 infrastructure def test_docker_restart(self): ac, nt = classify_alert_early("DockerContainerRestarting", "warning", {}) assert ac == "infrastructure" # ADR-075: Host* 從 infrastructure 分離為 host_resource def test_host_prefix_is_host_resource(self): ac, nt = classify_alert_early("HostHighCpuLoad", "warning", {}) assert nt == "TYPE-3" assert ac == "host_resource" def test_host_down(self): ac, nt = classify_alert_early("HostDown", "critical", {}) assert ac == "host_resource" def test_host_memory(self): ac, nt = classify_alert_early("HostOutOfMemory", "warning", {}) assert ac == "host_resource" def test_host_disk(self): ac, nt = classify_alert_early("HostOutOfDiskSpace", "warning", {}) assert ac == "host_resource" @pytest.mark.parametrize("alertname", [ "SystemdRunnerRestartSpike", "SystemdRunnerWatchdogEnabled", "SystemdRunnerMissingResourceQuota", ]) def test_systemd_runner_is_host_resource_not_heartbeat(self, alertname): ac, nt = classify_alert_early(alertname, "warning", {}) assert nt == "TYPE-3" assert ac == "host_resource" # --------------------------------------------------------------------------- # # ADR-075: alertchain_health (TYPE-8M) # --------------------------------------------------------------------------- # class TestAlertchainHealth: @pytest.mark.parametrize("alertname", [ "AlertChainBroken_Alertmanager", "AlertChainBroken_Sentry", "NoAlertsReceived2Hours", "AlertChainUnhealthy", "NoAlertsReceived", "PrometheusNotConnectedToAlertmanager", ]) def test_alertchain_alerts(self, alertname): ac, nt = classify_alert_early(alertname, "critical", {}) assert ac == "alertchain_health" assert nt == "TYPE-8M" def test_alertchain_beats_severity_info(self): # 即使 severity=info,AlertChainBroken 也必須是 alertchain_health ac, nt = classify_alert_early("AlertChainBroken_Alertmanager", "info", {}) assert ac == "alertchain_health" assert nt == "TYPE-8M" # --------------------------------------------------------------------------- # # ADR-075: flywheel_health (TYPE-8M) # --------------------------------------------------------------------------- # class TestFlywheelHealth: def test_auto_repair_low_success(self): ac, nt = classify_alert_early("AutoRepairLowSuccessRate", "warning", {}) assert ac == "flywheel_health" assert nt == "TYPE-8M" def test_permanent_fix_required(self): ac, nt = classify_alert_early("PermanentFixRequired", "warning", {}) assert ac == "flywheel_health" assert nt == "TYPE-8M" def test_flywheel_prefix(self): ac, nt = classify_alert_early("FlywheelPlaybookZero", "critical", {}) assert ac == "flywheel_health" assert nt == "TYPE-8M" def test_flywheel_beats_severity_info(self): ac, nt = classify_alert_early("AutoRepairLowSuccessRate", "info", {}) assert ac == "flywheel_health" assert nt == "TYPE-8M" # --------------------------------------------------------------------------- # # ADR-075: storage (TYPE-3) # --------------------------------------------------------------------------- # class TestStorage: def test_minio_down(self): ac, nt = classify_alert_early("MinIODown", "critical", {}) assert ac == "storage" assert nt == "TYPE-3" # --------------------------------------------------------------------------- # # ADR-075: devops_tool (TYPE-3) # --------------------------------------------------------------------------- # class TestDevopsTool: @pytest.mark.parametrize("alertname", [ "OpenClawDown", "SignOzDown", "GiteaDown", "HarborDown", "SentryDown", "AlertmanagerDown", "KaliScannerDown", "GiteaCIPipelineFailed", ]) def test_devops_tools(self, alertname): ac, nt = classify_alert_early(alertname, "critical", {}) assert ac == "devops_tool" assert nt == "TYPE-3" # --------------------------------------------------------------------------- # # ADR-075: external_site (TYPE-3) # --------------------------------------------------------------------------- # class TestExternalSite: @pytest.mark.parametrize("alertname", [ "MoWoooWorkDown", "TsenyangWebsiteDown", "StockWoooWorkDown", "BitanWoooWorkDown", ]) def test_external_sites(self, alertname): ac, nt = classify_alert_early(alertname, "critical", {}) assert ac == "external_site" assert nt == "TYPE-3" # --------------------------------------------------------------------------- # # ADR-075: ssl_cert (TYPE-3) # --------------------------------------------------------------------------- # class TestSslCert: def test_external_site_ssl(self): ac, nt = classify_alert_early("ExternalSiteSSLExpiringSoon", "warning", {}) assert ac == "ssl_cert" assert nt == "TYPE-3" def test_tls_cert(self): ac, nt = classify_alert_early("TLSCertExpiryCritical", "critical", {}) assert ac == "ssl_cert" assert nt == "TYPE-3" # --------------------------------------------------------------------------- # # TYPE-3: Kubernetes # --------------------------------------------------------------------------- # class TestKubernetes: @pytest.mark.parametrize("alertname", [ "KubePodCrashLooping", "PodHighMemory", "DeploymentReplicasMismatch", "NodeNotReady", "ArgoCDSyncFailed", ]) def test_k8s_prefixes(self, alertname): ac, nt = classify_alert_early(alertname, "critical", {}) assert nt == "TYPE-3" assert ac == "kubernetes" def test_velero_backup_failed_is_kubernetes(self): # VeleroBackupFailed severity=critical → backup 規則不命中,走 Velero prefix → kubernetes TYPE-3 ac, nt = classify_alert_early("VeleroBackupFailed", "critical", {}) assert nt == "TYPE-3" assert ac == "kubernetes" def test_velero_backup_success_info_is_type1(self): # VeleroBackupSuccess severity=info → TYPE-1 ac, nt = classify_alert_early("VeleroBackupSuccess", "info", {}) assert nt == "TYPE-1" # --------------------------------------------------------------------------- # # TYPE-3: Database # --------------------------------------------------------------------------- # class TestDatabase: def test_postgres(self): ac, nt = classify_alert_early("PostgresDown", "critical", {}) assert nt == "TYPE-3" assert ac == "database" def test_redis(self): ac, nt = classify_alert_early("RedisMemoryHigh", "warning", {}) assert nt == "TYPE-3" assert ac == "database" # --------------------------------------------------------------------------- # # TYPE-3: General (fallback) # --------------------------------------------------------------------------- # class TestGeneral: def test_unknown_alert(self): ac, nt = classify_alert_early("SomeUnknownAlert", "warning", {}) assert nt == "TYPE-3" assert ac == "general" def test_empty_alertname(self): ac, nt = classify_alert_early("", "warning", {}) assert nt == "TYPE-3" assert ac == "general" # --------------------------------------------------------------------------- # # 優先順序驗證 — config_drift 和 info 應優先於 prefix 規則 # --------------------------------------------------------------------------- # class TestPriority: def test_config_drift_beats_severity(self): # ConfigurationDrift 即使 severity=warning 也應命中 config_drift ac, nt = classify_alert_early("ConfigurationDrift", "warning", {}) assert ac == "config_drift" def test_info_severity_beats_docker_prefix(self): # Docker 前綴 + severity=info → info 規則先命中 ac, nt = classify_alert_early("DockerContainerOOM", "info", {}) assert ac == "info"