""" Phase 25: Config Drift Detection 測試 ======================================= ADR-052 / Phase 25 P2: DriftDetector + NemotronDriftInterpreter + DriftRemediator 測試策略: Source code inspection (禁止 Mock,禁止 import 模組) - 驗證 drift_detector.py 結構(DriftDetector, GitStateReader, K8sStateReader) - 驗證 drift_interpreter.py (NemotronDriftInterpreter, read-only) - 驗證 drift_remediator.py (kubectl apply / git push) - 驗證 api/v1/drift.py 端點 - 驗證 ai_router.py DIAGNOSE local-only 鏈路 建立時間: 2026-04-05 (台北時區) 建立者: Claude Code (Phase 25 Drift Detection 測試) """ from pathlib import Path # Source file paths _BASE = Path(__file__).parent.parent / "src" _DETECTOR = _BASE / "services" / "drift_detector.py" _ANALYZER = _BASE / "services" / "drift_analyzer.py" _INTERPRETER = _BASE / "services" / "drift_interpreter.py" _REMEDIATOR = _BASE / "services" / "drift_remediator.py" _DRIFT_API = _BASE / "api" / "v1" / "drift.py" _AI_ROUTER = _BASE / "services" / "ai_router.py" # ============================================================================= # TestDriftDetector — 偵測器結構 # ============================================================================= class TestDriftDetector: """驗證 drift_detector.py 的核心結構""" def test_drift_detector_module_exists(self): """src/services/drift_detector.py 存在""" assert _DETECTOR.exists(), f"找不到 {_DETECTOR}" def test_drift_detector_class_exists(self): """DriftDetector class 已定義""" source = _DETECTOR.read_text() assert "class DriftDetector" in source def test_drift_scan_method_exists(self): """DriftDetector 有 scan() 方法""" source = _DETECTOR.read_text() assert "async def scan" in source or "def scan" in source def test_git_state_reader_exists(self): """GitStateReader class 已定義""" source = _DETECTOR.read_text() assert "class GitStateReader" in source def test_k8s_state_reader_exists(self): """K8sStateReader class 已定義""" source = _DETECTOR.read_text() assert "class K8sStateReader" in source def test_drift_level_high_defined(self): """DriftLevel.HIGH 已定義""" source = _DETECTOR.read_text() assert "HIGH" in source def test_drift_level_medium_defined(self): """DriftLevel.MEDIUM 已定義""" source = _DETECTOR.read_text() assert "MEDIUM" in source # ============================================================================= # TestDriftInterpreter — 解讀器 (read-only,不執行修復) # ============================================================================= class TestDriftInterpreter: """驗證 drift_interpreter.py 的 NemotronDriftInterpreter,且不含 kubectl apply""" def test_drift_interpreter_module_exists(self): """src/services/drift_interpreter.py 存在""" assert _INTERPRETER.exists(), f"找不到 {_INTERPRETER}" def test_drift_interpreter_class_exists(self): """NemotronDriftInterpreter class 已定義""" source = _INTERPRETER.read_text() assert "class NemotronDriftInterpreter" in source def test_drift_interpreter_no_fix_commands(self): """drift_interpreter.py 不含 'kubectl apply'(只分析,不執行修復)""" source = _INTERPRETER.read_text() assert "kubectl apply" not in source, ( "drift_interpreter.py 不應含 kubectl apply — 解讀器只做 read-only 分析" ) # ============================================================================= # TestDriftRemediator — 修復器 (kubectl apply / git push) # ============================================================================= class TestDriftRemediator: """驗證 drift_remediator.py 的 rollback 與 adopt 操作""" def test_drift_remediator_module_exists(self): """src/services/drift_remediator.py 存在""" assert _REMEDIATOR.exists(), f"找不到 {_REMEDIATOR}" def test_drift_remediator_rollback_uses_kubectl(self): """rollback() 使用 kubectl apply 覆蓋回 Git 狀態""" source = _REMEDIATOR.read_text() assert "kubectl apply" in source def test_drift_remediator_adopt_uses_git_push(self): """adopt() 使用 git push 承認變更並更新 Git""" source = _REMEDIATOR.read_text() assert "git push" in source # ============================================================================= # TestDriftApi — API 端點 # ============================================================================= class TestDriftApi: """驗證 api/v1/drift.py 端點路徑""" def test_drift_api_router_exists(self): """src/api/v1/drift.py 存在""" assert _DRIFT_API.exists(), f"找不到 {_DRIFT_API}" def test_drift_scan_endpoint_exists(self): """drift.py 有 /scan 端點""" source = _DRIFT_API.read_text() assert '"/scan"' in source or "'/scan'" in source def test_drift_reports_endpoint_exists(self): """drift.py 有 /reports 端點""" source = _DRIFT_API.read_text() assert '"/reports"' in source or "'/reports'" in source # ============================================================================= # TestAiRouterDiagnose — DIAGNOSE local-only 鏈路 # ============================================================================= class TestAiRouterDiagnose: """驗證 ai_router.py 的 DIAGNOSE 隱私邊界 local-only fallback""" def test_local_fallback_chain_exists(self): """ai_router.py 有 _local_fallback_chain(DIAGNOSE 隱私邊界用)""" source = _AI_ROUTER.read_text() assert "_local_fallback_chain" in source def test_diagnose_uses_local_chain(self): """DIAGNOSE task type 使用 local fallback chain""" source = _AI_ROUTER.read_text() assert "DIAGNOSE" in source # DIAGNOSE 分支必須引用 _local_fallback_chain idx_diagnose = source.find("DIAGNOSE") remaining = source[idx_diagnose:] assert "_local_fallback_chain" in remaining