Files
awoooi/apps/api/tests/test_telegram_adr050.py
Your Name f84482299b
All checks were successful
CD Pipeline / tests (push) Successful in 1m15s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 3m28s
CD Pipeline / post-deploy-checks (push) Successful in 1m38s
feat(telegram): surface awooop agent evidence chain
2026-05-25 16:35:27 +08:00

239 lines
9.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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