"""
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