#!/usr/bin/env python3 """ Phase 6.4 全鏈路測試腳本 ======================== 功能: 1. 觸發假告警 (建立 Incident) 2. 呼叫 /proposal 端點 (產生決策) 3. 呼叫 /approvals/pending (模擬前端撈取待簽核清單) 4. 證明這條鏈路完全暢通 使用方式: cd apps/api python scripts/test_phase64_proposal.py 驗收標準: - Incident 成功建立 - Proposal 成功生成 - Proposal 出現在 /approvals/pending 清單中 - 前端零改動即可渲染 """ import asyncio import json from datetime import datetime import httpx # API 端點 API_BASE = "http://localhost:8000" SIGNALS_ENDPOINT = f"{API_BASE}/api/v1/webhooks/signals" INCIDENTS_ENDPOINT = f"{API_BASE}/api/v1/incidents" APPROVALS_ENDPOINT = f"{API_BASE}/api/v1/approvals/pending" async def send_test_alert() -> dict | None: """發送測試告警""" alert = { "alert_name": "PodCrashLoopBackOff", "severity": "critical", # P0 "source": "prometheus", "namespace": "production", "target": "api-gateway", "fingerprint": f"fp_test_{datetime.now().strftime('%H%M%S')}", "labels": { "namespace": "production", "pod": "api-gateway-abc123", }, "annotations": { "summary": "Pod api-gateway is in CrashLoopBackOff state", }, } async with httpx.AsyncClient() as client: try: response = await client.post( SIGNALS_ENDPOINT, json=alert, timeout=10.0, ) if response.status_code == 200: return response.json() else: print(f" [ERROR] status_code: {response.status_code}") print(f" [ERROR] response: {response.text}") return None except Exception as e: print(f" [ERROR] {e}") return None async def wait_for_incident(namespace: str, timeout: int = 10) -> str | None: """等待 Incident 被建立並返回 incident_id""" async with httpx.AsyncClient() as client: for _ in range(timeout): try: response = await client.get( INCIDENTS_ENDPOINT, timeout=5.0, ) if response.status_code == 200: data = response.json() for incident in data.get("incidents", []): # 找到我們的測試 Incident if "api-gateway" in incident.get("affected_services", []): return incident.get("incident_id") except Exception: pass await asyncio.sleep(1) return None async def generate_proposal(incident_id: str) -> dict | None: """生成 Decision Proposal""" async with httpx.AsyncClient() as client: try: response = await client.post( f"{INCIDENTS_ENDPOINT}/{incident_id}/proposal", timeout=10.0, ) if response.status_code == 200: return response.json() else: print(f" [ERROR] status_code: {response.status_code}") print(f" [ERROR] response: {response.text}") return None except Exception as e: print(f" [ERROR] {e}") return None async def get_pending_approvals() -> dict | None: """取得待簽核清單""" async with httpx.AsyncClient() as client: try: response = await client.get( APPROVALS_ENDPOINT, timeout=10.0, ) if response.status_code == 200: return response.json() else: print(f" [ERROR] status_code: {response.status_code}") return None except Exception as e: print(f" [ERROR] {e}") return None async def main(): """主測試流程""" print("=" * 70) print("Phase 6.4 全鏈路測試: Incident → Proposal → Pending Approvals") print("=" * 70) print(f"時間: {datetime.now().isoformat()}") print() # 0. 健康檢查 print("[0] 檢查 API 健康狀態...") async with httpx.AsyncClient() as client: try: health = await client.get(f"{API_BASE}/api/v1/health", timeout=5.0) print(f" API status: {health.status_code}") except Exception as e: print(f" API 連線失敗: {e}") print(" 請確認 API 已啟動: docker compose up -d") return # 1. 發送測試告警 print("\n" + "-" * 70) print("[1] 發送測試告警 (建立 Incident)") print("-" * 70) result = await send_test_alert() if not result: print(" [FAIL] 無法發送告警") return print(f" message_id: {result.get('message_id', 'N/A')}") print(f" success: {result.get('success', False)}") # 2. 等待 Incident 建立 print("\n" + "-" * 70) print("[2] 等待 Consumer 處理並建立 Incident (最多 10 秒)") print("-" * 70) incident_id = await wait_for_incident("production") if not incident_id: print(" [FAIL] 無法找到測試 Incident") print(" 請檢查 API 日誌: docker logs awoooi-api --tail 50") return print(f" incident_id: {incident_id}") print(" [OK] Incident 已建立") # 3. 生成 Proposal print("\n" + "-" * 70) print("[3] 呼叫 /proposal 端點生成決策") print("-" * 70) proposal_result = await generate_proposal(incident_id) if not proposal_result or not proposal_result.get("success"): print(f" [FAIL] 無法生成 Proposal") print(f" message: {proposal_result.get('message') if proposal_result else 'N/A'}") return proposal = proposal_result.get("proposal", {}) print(f" proposal_id: {proposal.get('id', 'N/A')}") print(f" action: {proposal.get('action', 'N/A')[:60]}...") print(f" risk_level: {proposal.get('risk_level', 'N/A')}") print(f" required_signatures: {proposal.get('required_signatures', 'N/A')}") print(f" incident_status: {proposal_result.get('incident_status', 'N/A')}") print(" [OK] Proposal 已生成") # 4. 驗證 /approvals/pending print("\n" + "-" * 70) print("[4] 呼叫 /approvals/pending 驗證前端相容性") print("-" * 70) pending = await get_pending_approvals() if not pending: print(" [FAIL] 無法取得待簽核清單") return print(f" count: {pending.get('count', 0)}") # 尋找我們的 Proposal found = False for approval in pending.get("approvals", []): if approval.get("id") == proposal.get("id"): found = True print(f" [FOUND] Proposal 出現在待簽核清單中!") print() print(" === PendingApprovalsResponse JSON ===") print(json.dumps({ "count": pending.get("count"), "target_approval": approval, }, indent=2, ensure_ascii=False, default=str)) break if not found: print(" [WARN] Proposal 未出現在待簽核清單中") print(f" (可能因為 risk_level=LOW 已自動批准)") # 5. 最終驗證 print("\n" + "=" * 70) print("驗證結果") print("=" * 70) checks = [ ("Incident 建立", incident_id is not None), ("Proposal 生成", proposal_result.get("success", False)), ("風險評估", proposal.get("risk_level") is not None), ("狀態推進 (MITIGATING)", proposal_result.get("incident_status") == "mitigating"), ("前端相容 (/approvals/pending)", pending is not None), ] all_passed = True for name, passed in checks: status = "✅ PASS" if passed else "❌ FAIL" print(f"[{status}] {name}") if not passed: all_passed = False print() print("=" * 70) if all_passed: print("🎉 Phase 6.4 全鏈路測試 PASSED!") print(" 大腦已具備決策輸出能力!") print(" Decision Proposal API 已鑄造完成!") else: print("💥 Phase 6.4 全鏈路測試 FAILED!") print(" 請檢查上述失敗項目") print("=" * 70) if __name__ == "__main__": asyncio.run(main())