test(phase25): Phase 25 P1/P2 source code inspection tests (36 tests)

- test_phase25_auto_harvesting.py: 18 tests for NemotronRunbookGenerator,
  AntiPattern gate, fire-and-forget pattern, symptoms_hash
- test_phase25_drift_detection.py: 18 tests for DriftDetector, NemotronDriftInterpreter
  (read-only), DriftRemediator, local fallback chain for DIAGNOSE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-05 00:14:50 +08:00
parent cd5547f5eb
commit 4bc4757fdc
2 changed files with 313 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
"""
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_chainDIAGNOSE 隱私邊界用)"""
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