239 lines
9.6 KiB
Python
239 lines
9.6 KiB
Python
"""
|
||
ADR-050 Telegram 互動式 Incident 管理 2.0 測試
|
||
==============================================
|
||
驗證 6鍵 Inline Keyboard + detail/reanalyze/history 訊息格式
|
||
|
||
測試策略 (遵循 feedback_no_mock_testing.md):
|
||
- 直接測試訊息格式化邏輯與 keyboard 結構
|
||
- 透過 source code 驗證實作正確性(不依賴 Runtime)
|
||
- 不發送實際 Telegram 訊息,不呼叫外部 API
|
||
|
||
版本: v1.0
|
||
建立: 2026-04-01 (台北時區)
|
||
建立者: Claude Code (ADR-050 P2 Tests)
|
||
"""
|
||
|
||
|
||
# =============================================================================
|
||
# Test: _build_inline_keyboard 6鍵結構
|
||
# =============================================================================
|
||
|
||
class TestInlineKeyboardStructure:
|
||
"""驗證 ADR-050 6鍵 Inline Keyboard 結構"""
|
||
|
||
def _read_gateway(self) -> str:
|
||
with open("src/services/telegram_gateway.py", encoding="utf-8") as f:
|
||
return f.read()
|
||
|
||
def test_keyboard_row1_approve_button_exists(self):
|
||
"""第一行有 ✅ 批准 按鈕"""
|
||
assert "✅ 批准" in self._read_gateway()
|
||
|
||
def test_keyboard_row1_reject_button_exists(self):
|
||
"""第一行有 ❌ 拒絕 按鈕"""
|
||
assert "❌ 拒絕" in self._read_gateway()
|
||
|
||
def test_keyboard_row1_silence_button_exists(self):
|
||
"""第一行有 🔕 靜默 按鈕"""
|
||
assert "🔕 靜默" in self._read_gateway()
|
||
|
||
def test_keyboard_row2_detail_uses_incident_id_format(self):
|
||
"""📋 詳情 按鈕使用 detail:{incident_id} 格式"""
|
||
source = self._read_gateway()
|
||
assert '📋 詳情' in source
|
||
assert 'f"detail:{incident_id}"' in source
|
||
|
||
def test_keyboard_row2_reanalyze_uses_incident_id_format(self):
|
||
"""🔄 重診 按鈕使用 reanalyze:{incident_id} 格式"""
|
||
source = self._read_gateway()
|
||
assert '🔄 重診' in source
|
||
assert 'f"reanalyze:{incident_id}"' in source
|
||
|
||
def test_keyboard_row2_history_uses_incident_id_format(self):
|
||
"""📊 歷史 按鈕使用 history:{incident_id} 格式"""
|
||
source = self._read_gateway()
|
||
assert '📊 歷史' in source
|
||
assert 'f"history:{incident_id}"' in source
|
||
|
||
def test_reanalyze_no_longer_placeholder(self):
|
||
"""reanalyze 不再是 placeholder(功能開發中已移除)"""
|
||
assert "功能開發中" not in self._read_gateway(), (
|
||
"reanalyze 仍是 placeholder!應已實作真實邏輯。"
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# Test: detail 訊息格式
|
||
# =============================================================================
|
||
|
||
class TestDetailMessageFormat:
|
||
"""驗證 _send_incident_detail 訊息格式與資料來源"""
|
||
|
||
def _read_gateway(self) -> str:
|
||
with open("src/services/telegram_gateway.py", encoding="utf-8") as f:
|
||
return f.read()
|
||
|
||
def test_detail_method_exists(self):
|
||
"""_send_incident_detail 方法存在"""
|
||
assert "_send_incident_detail" in self._read_gateway()
|
||
|
||
def test_detail_uses_real_db(self):
|
||
"""detail 從真實 DB 取得資料(IncidentRepository)"""
|
||
source = self._read_gateway()
|
||
assert "get_incident_repository" in source
|
||
assert "repo.get_by_id(incident_id)" in source
|
||
|
||
def test_detail_handles_not_found(self):
|
||
"""detail 處理找不到事件的情況"""
|
||
assert "找不到事件" in self._read_gateway()
|
||
|
||
def test_detail_shows_incident_id(self):
|
||
"""detail 訊息顯示 INC-ID"""
|
||
assert "incident.incident_id" in self._read_gateway()
|
||
|
||
def test_detail_shows_severity(self):
|
||
"""detail 訊息顯示嚴重度"""
|
||
assert "incident.severity" in self._read_gateway()
|
||
|
||
def test_detail_includes_truth_chain_gateway_summary(self):
|
||
"""detail 顯示 AwoooP truth-chain / MCP Gateway / automation quality / agent evidence 摘要"""
|
||
source = self._read_gateway()
|
||
assert "fetch_truth_chain" in source
|
||
assert "_format_gateway_summary_lines" in source
|
||
assert "_format_automation_quality_lines" in source
|
||
assert "_format_awooop_agent_evidence_lines" in source
|
||
assert "AI Agent 證據鏈" in source
|
||
assert "MCP Gateway" in source
|
||
assert "自動化品質" in source
|
||
|
||
|
||
# =============================================================================
|
||
# Test: history 訊息格式
|
||
# =============================================================================
|
||
|
||
class TestHistoryMessageFormat:
|
||
"""驗證 _send_incident_history 訊息格式與資料來源"""
|
||
|
||
def _read_gateway(self) -> str:
|
||
with open("src/services/telegram_gateway.py", encoding="utf-8") as f:
|
||
return f.read()
|
||
|
||
def test_history_method_exists(self):
|
||
"""_send_incident_history 方法存在"""
|
||
assert "_send_incident_history" in self._read_gateway()
|
||
|
||
def test_history_uses_real_frequency_stats(self):
|
||
"""history 使用真實頻率統計資料"""
|
||
source = self._read_gateway()
|
||
assert "count_1h" in source
|
||
assert "count_24h" in source
|
||
assert "count_7d" in source
|
||
assert "count_30d" in source
|
||
|
||
def test_history_handles_no_stats(self):
|
||
"""history 處理無頻率統計資料的情況 (Phase 27: 舊 incident 無 DB 快照)"""
|
||
source = self._read_gateway()
|
||
# Phase 27 雙層策略: 無快照時顯示說明,而非舊版 "無頻率統計資料"
|
||
assert "無建立時快照" in source or "無頻率統計資料" in source
|
||
|
||
def test_history_includes_db_truth_chain_fallback(self):
|
||
"""history 也顯示 DB truth-chain,避免 Redis TTL 缺口讓處置階段失真"""
|
||
source = self._read_gateway()
|
||
assert "DB Truth-chain" in source
|
||
assert "incident_history_truth_chain_summary_failed" in source
|
||
assert "_format_automation_quality_lines" in source
|
||
assert "_format_awooop_agent_evidence_lines" in source
|
||
|
||
|
||
# =============================================================================
|
||
# Test: reanalyze 訊息格式
|
||
# =============================================================================
|
||
|
||
class TestReanalyzeMessageFormat:
|
||
"""驗證 _send_reanalyze_result 訊息格式"""
|
||
|
||
def _read_gateway(self) -> str:
|
||
with open("src/services/telegram_gateway.py", encoding="utf-8") as f:
|
||
return f.read()
|
||
|
||
def test_reanalyze_result_method_exists(self):
|
||
"""_send_reanalyze_result 方法存在"""
|
||
assert "_send_reanalyze_result" in self._read_gateway()
|
||
|
||
def test_reanalyze_calls_trigger_reanalysis(self):
|
||
"""reanalyze handler 呼叫 trigger_reanalysis"""
|
||
assert "trigger_reanalysis" in self._read_gateway()
|
||
|
||
def test_reanalyze_uses_lazy_import(self):
|
||
"""reanalyze 使用 lazy import 避免循環依賴"""
|
||
assert "from src.services.incident_service import get_incident_service" in self._read_gateway()
|
||
|
||
def test_reanalyze_scheduled_message(self):
|
||
"""reanalyze 有「重診已排程」訊息"""
|
||
assert "重診已排程" in self._read_gateway()
|
||
|
||
def test_reanalyze_in_progress_message(self):
|
||
"""reanalyze 有「重診進行中」訊息"""
|
||
assert "重診進行中" in self._read_gateway()
|
||
|
||
def test_reanalyze_failure_message(self):
|
||
"""reanalyze 有「重診失敗」訊息"""
|
||
assert "重診失敗" in self._read_gateway()
|
||
|
||
def test_reanalyze_uses_html_escape(self):
|
||
"""reanalyze 訊息使用 html.escape 防 XSS"""
|
||
source = self._read_gateway()
|
||
# _send_reanalyze_result 之後應有 html.escape 呼叫
|
||
reanalyze_section = source.split("_send_reanalyze_result")[1] if "_send_reanalyze_result" in source else ""
|
||
assert "html.escape" in reanalyze_section
|
||
|
||
|
||
# =============================================================================
|
||
# Test: trigger_reanalysis 邏輯結構
|
||
# =============================================================================
|
||
|
||
class TestTriggerReanalysisStructure:
|
||
"""驗證 trigger_reanalysis() 方法結構(不連接真實 Redis)"""
|
||
|
||
def _read_service(self) -> str:
|
||
with open("src/services/incident_service.py", encoding="utf-8") as f:
|
||
return f.read()
|
||
|
||
def test_method_exists(self):
|
||
"""trigger_reanalysis 方法存在於 IncidentService"""
|
||
assert "async def trigger_reanalysis" in self._read_service()
|
||
|
||
def test_uses_redis_setnx(self):
|
||
"""使用 Redis SETNX (nx=True) 去重"""
|
||
assert "nx=True" in self._read_service()
|
||
|
||
def test_dedup_key_format(self):
|
||
"""去重 key 格式為 reanalyze_dedup:{incident_id}"""
|
||
assert '"reanalyze_dedup:"' in self._read_service() or "reanalyze_dedup:" in self._read_service()
|
||
|
||
def test_ttl_is_600_seconds(self):
|
||
"""去重 TTL 為 600 秒(10 分鐘)"""
|
||
assert "600" in self._read_service()
|
||
|
||
def test_returns_triggered_key(self):
|
||
"""回傳值包含 triggered key"""
|
||
assert '"triggered"' in self._read_service()
|
||
|
||
def test_returns_message_key(self):
|
||
"""回傳值包含 message key"""
|
||
assert '"message"' in self._read_service()
|
||
|
||
def test_returns_already_analyzing_key(self):
|
||
"""回傳值包含 already_analyzing key"""
|
||
assert '"already_analyzing"' in self._read_service()
|
||
|
||
def test_deletes_key_on_not_found(self):
|
||
"""找不到 incident 時刪除去重 key(允許重試)"""
|
||
assert "redis_client.delete(dedup_key)" in self._read_service()
|
||
|
||
def test_fallback_to_episodic_memory(self):
|
||
"""Working Memory 找不到時 fallback 到 Episodic Memory (DB)"""
|
||
source = self._read_service()
|
||
assert "get_from_working_memory" in source
|
||
assert "get_from_episodic_memory" in source
|