""" Phase 25: Auto-Harvesting 測試 ================================ ADR-052 / Phase 25 P1: NemotronRunbookGenerator + AntiPattern Gate 測試策略: Source code inspection (禁止 Mock,禁止 import 模組) - 驗證 runbook_generator.py 實作結構 - 驗證 auto_repair_service.py fire-and-forget 模式 - 驗證 knowledge.py 模型欄位 - 驗證 knowledge_service.py check_anti_pattern 邏輯 建立時間: 2026-04-05 (台北時區) 建立者: Claude Code (Phase 25 Auto-Harvesting 測試) """ from pathlib import Path from types import SimpleNamespace # Source file paths _BASE = Path(__file__).parent.parent / "src" _RUNBOOK_GEN = _BASE / "services" / "runbook_generator.py" _AUTO_REPAIR = _BASE / "services" / "auto_repair_service.py" _PLAYBOOK = _BASE / "models" / "playbook.py" _KNOWLEDGE = _BASE / "models" / "knowledge.py" _KNOWLEDGE_SVC = _BASE / "services" / "knowledge_service.py" # ============================================================================= # TestRunbookGeneratorModule — 模組存在與結構 # ============================================================================= class TestRunbookGeneratorModule: """驗證 runbook_generator.py 模組存在並有正確結構""" def test_runbook_generator_module_exists(self): """src/services/runbook_generator.py 存在""" assert _RUNBOOK_GEN.exists(), f"找不到 {_RUNBOOK_GEN}" def test_runbook_generator_class_exists(self): """NemotronRunbookGenerator class 已定義""" source = _RUNBOOK_GEN.read_text() assert "class NemotronRunbookGenerator" in source def test_generate_runbook_method_exists(self): """generate_runbook() 方法存在""" source = _RUNBOOK_GEN.read_text() assert "async def generate_runbook" in source or "def generate_runbook" in source def test_generate_anti_pattern_method_exists(self): """generate_anti_pattern() 方法存在""" source = _RUNBOOK_GEN.read_text() assert "async def generate_anti_pattern" in source or "def generate_anti_pattern" in source def test_runbook_uses_auto_runbook_type(self): """runbook_generator.py 使用 'auto_runbook' entry type""" source = _RUNBOOK_GEN.read_text() assert "auto_runbook" in source def test_anti_pattern_uses_anti_pattern_type(self): """runbook_generator.py 使用 'anti_pattern' entry type""" source = _RUNBOOK_GEN.read_text() assert "anti_pattern" in source def test_runbook_status_is_draft(self): """成功修復 → AUTO_RUNBOOK 狀態為 DRAFT(需人工審核)""" source = _RUNBOOK_GEN.read_text() assert "DRAFT" in source or "draft" in source def test_anti_pattern_status_is_published(self): """失敗修復 → ANTI_PATTERN 狀態為 PUBLISHED(直接發布)""" source = _RUNBOOK_GEN.read_text() assert "PUBLISHED" in source or "published" in source def test_runbook_uses_nvidia_chat(self): """runbook_generator.py 使用 nvidia provider""" source = _RUNBOOK_GEN.read_text() assert "nvidia" in source def test_minimal_fallback_exists(self): """Nemotron 失敗時有 fallback 邏輯""" source = _RUNBOOK_GEN.read_text() assert "fallback" in source def test_runbook_review_card_is_structured_html(self): """Telegram Runbook 審核訊息必須是可掃描治理卡片,不直接傾倒 Markdown 原文""" from src.services.runbook_generator import format_runbook_review_card incident = SimpleNamespace( incident_id="INC-20260506-E54736", affected_services=["node-exporter-110"], ) content = ( "## 症狀描述\n" "Incident INC-20260506-E54736,受影響服務:node-exporter-110\n\n" "## 執行步驟\n" "- Step 1: ssh{host} echo '=== LOAD ===' -> FAILED: Unsupported scheme\n" ) card = format_runbook_review_card(incident, "ff5eff01-7243-44bf", content) assert "RUNBOOK REVIEW|待審核" in card assert "INC-20260506-E54736" in card assert "🧾 內容摘要" in card assert "placeholder 或不支援的執行步驟" in card assert "## 症狀描述" not in card assert "ssh{host}" not in card # ============================================================================= # TestAutoRepairService — fire-and-forget 與 GC 防洩漏 # ============================================================================= class TestAutoRepairService: """驗證 auto_repair_service.py 的 fire-and-forget 模式與 GC 防洩漏""" def test_fire_and_forget_pattern(self): """auto_repair_service.py 使用 asyncio.create_task() 實現 fire-and-forget""" source = _AUTO_REPAIR.read_text() assert "create_task" in source def test_pending_tasks_gc_prevention(self): """auto_repair_service.py 持有 _pending_tasks 防止 Task 被 GC 回收""" source = _AUTO_REPAIR.read_text() assert "_pending_tasks" in source def test_anti_pattern_gate_in_auto_repair(self): """auto_repair_service.py 有 check_anti_pattern 攔截呼叫(AntiPattern Gate)""" source = _AUTO_REPAIR.read_text() assert "check_anti_pattern" in source # ============================================================================= # TestKnowledgeModels — 知識庫模型結構 # ============================================================================= class TestKnowledgeModels: """驗證 models/knowledge.py 的 EntryType 與 KnowledgeEntry 欄位""" def test_auto_runbook_entrytype_value(self): """EntryType 有 AUTO_RUNBOOK = 'auto_runbook'""" source = _KNOWLEDGE.read_text() assert 'AUTO_RUNBOOK' in source assert '"auto_runbook"' in source or "'auto_runbook'" in source def test_anti_pattern_entrytype_value(self): """EntryType 有 ANTI_PATTERN = 'anti_pattern'""" source = _KNOWLEDGE.read_text() assert 'ANTI_PATTERN' in source assert '"anti_pattern"' in source or "'anti_pattern'" in source def test_knowledge_entry_has_symptoms_hash(self): """KnowledgeEntry 有 symptoms_hash 欄位(AntiPattern 閉環用)""" source = _KNOWLEDGE.read_text() assert "symptoms_hash" in source # ============================================================================= # TestPlaybookModel — 症狀 hash 計算 # ============================================================================= class TestPlaybookModel: """驗證 models/playbook.py 的 compute_hash 方法""" def test_symptom_hash_compute_method(self): """SymptomPattern 有 compute_hash() 方法""" source = _PLAYBOOK.read_text() assert "compute_hash" in source # ============================================================================= # TestKnowledgeService — check_anti_pattern 查詢 # ============================================================================= class TestKnowledgeService: """驗證 knowledge_service.py 的 check_anti_pattern 方法""" def test_check_anti_pattern_in_knowledge_service(self): """knowledge_service.py 有 check_anti_pattern 方法""" source = _KNOWLEDGE_SVC.read_text() assert "check_anti_pattern" in source