181 lines
7.3 KiB
Python
181 lines
7.3 KiB
Python
"""
|
||
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 "<b>RUNBOOK REVIEW|待審核</b>" in card
|
||
assert "<code>INC-20260506-E54736</code>" in card
|
||
assert "🧾 <b>內容摘要</b>" 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
|