Files
awoooi/apps/api/scripts/test_phase64_proposal.py
OG T 196d269b92 feat: add all application source code
- apps/api: FastAPI backend with Dockerfile
- apps/web: Next.js frontend with Dockerfile
- apps/sensor: Signal collection agent
- packages: shared packages

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-22 18:57:44 +08:00

262 lines
8.3 KiB
Python
Executable File

#!/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())