- Python: ruff --fix 修復 280 個 lint 錯誤 - lewooogo-core: src/ 目錄未追蹤,導致 CI eslint 失敗 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
262 lines
8.2 KiB
Python
Executable File
262 lines
8.2 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(" [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(" [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(" (可能因為 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())
|