#!/usr/bin/env bash # e2e-manual-test.sh — 手動端到端驗證腳本(AB13) # # 目的:真實 spawn visionA-backend(api-server + remote-proxy)+ 一個 fake # local-tool HTTP server,搭配 visiona-agent(或手動 curl 模擬)驗證整條 # 雲端版架構跑得起來。 # # 這個 script 不進 CI。CI 用的自動化 e2e 已在 # visionA-backend/cmd/api-server/e2e_full_flow_test.go # 用 single-process in-memory integration 的方式覆蓋(跨 Go module 太重, # 不適合 go test)。 # # 使用情境: # - 本機開發時手動驗證 binary build 出來是不是真的能跑 # - 交付雛形前的 smoke test # - Debug 真實網路 / TLS / CORS 問題(in-memory test 無法覆蓋) # # 用法: # # # Terminal 1 — 啟動 backend + fake local-tool # bash visionA-backend/scripts/e2e-manual-test.sh backend # # # Terminal 2 — 取得 pairing token,然後手動把它貼進 visiona-agent UI # bash visionA-backend/scripts/e2e-manual-test.sh token # # # Terminal 3(可選) — agent 連上後,模擬前端打 API # bash visionA-backend/scripts/e2e-manual-test.sh forward # # 或直接 `bash ... all` 跑完整流程(backend + fake local,但 agent 還是得手動跑)。 # # 對應文件: # - .autoflow/04-architecture/visiona-agent-tdd.md §11(e2e testing) # - .autoflow/04-architecture/tunnel.md §3(資料流) set -euo pipefail # ---------------------------------------------------------------------- # 設定 # ---------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKEND_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # Port 預設值刻意避開: # - 3721 (local-tool 預設) # - 常見開發 port (3000/8080/5173) # 使用者自己有衝突時可用環境變數覆寫。 API_HOST="${API_HOST:-127.0.0.1}" API_PORT="${API_PORT:-13721}" TUNNEL_PORT="${TUNNEL_PORT:-13800}" PROXY_INTERNAL_PORT="${PROXY_INTERNAL_PORT:-13801}" FAKE_LOCAL_PORT="${FAKE_LOCAL_PORT:-38721}" API_URL="http://${API_HOST}:${API_PORT}" RELAY_WS_URL="ws://${API_HOST}:${TUNNEL_PORT}" # 背景 PID 清單(trap 清理用) PIDS=() cleanup() { echo "" echo "[e2e] 清理中..." # Bash 3.2(macOS 預設)展開空 array 時會觸發 nounset;用 +u 暫時放寬。 set +u for pid in "${PIDS[@]}"; do if kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true fi done set -u wait 2>/dev/null || true echo "[e2e] 清理完成" } trap cleanup INT TERM EXIT # ---------------------------------------------------------------------- # 子命令 # ---------------------------------------------------------------------- # 啟 fake local-tool:用 Python 內建 http.server 回固定 JSON。 # 避免再建一個 Go binary,讓使用者少裝東西。 start_fake_local() { echo "[e2e] 啟動 fake local-tool(port ${FAKE_LOCAL_PORT})..." python3 - </dev/null) echo "[e2e] 啟動 remote-proxy(tunnel=${TUNNEL_PORT}, internal=${PROXY_INTERNAL_PORT})..." ( cd "$BACKEND_DIR" VISIONA_TUNNEL_PORT=$TUNNEL_PORT \ VISIONA_PROXY_INTERNAL_PORT=$PROXY_INTERNAL_PORT \ VISIONA_LOG_LEVEL=info \ ./bin/remote-proxy ) & PIDS+=($!) sleep 0.5 echo "[e2e] 啟動 api-server(port=${API_PORT})..." ( cd "$BACKEND_DIR" VISIONA_HOST=$API_HOST \ VISIONA_API_PORT=$API_PORT \ VISIONA_PROXY_INTERNAL_URL="http://127.0.0.1:${PROXY_INTERNAL_PORT}" \ VISIONA_RELAY_PUBLIC_URL="$RELAY_WS_URL" \ VISIONA_LOG_LEVEL=info \ ./bin/api-server ) & PIDS+=($!) # bash 3.2 (macOS 預設) 不支援負數索引;用 length-1 API_PID="${PIDS[$((${#PIDS[@]}-1))]}" # 等 api-server ready — 同時檢查我們 spawn 的 PID 還活著,避免 port 被別人佔。 echo -n "[e2e] 等 api-server 就緒" ready=0 for i in $(seq 1 30); do if ! kill -0 "$API_PID" 2>/dev/null; then echo "" echo "[e2e] ✗ api-server 進程已結束(可能 port ${API_PORT} 被佔用,試試 API_PORT=xxx $0 backend)" exit 1 fi if curl -sf "$API_URL/healthz" >/dev/null 2>&1; then ready=1 echo " ✓" break fi echo -n "." sleep 0.3 done if [ "$ready" = "0" ]; then echo "" echo "[e2e] ✗ api-server 在 9 秒內沒就緒" exit 1 fi } cmd_backend() { start_fake_local start_backend echo "" echo "===================================================================" echo "[e2e] backend 已就緒。下一步:" echo "" echo " 1) 在另一個 terminal 執行:" echo " bash $0 token" echo " 取得 pairing token" echo "" echo " 2) 啟動 visiona-agent(wails dev 或 binary),把 pairing token 貼進 UI" echo " 並設定環境:" echo " VISIONA_RELAY_HTTP_URL=$API_URL" echo " VISIONA_PAIRING_MOCK=false" echo " VISIONA_LOCAL_ADDR=127.0.0.1:${FAKE_LOCAL_PORT}" echo "" echo " 3) agent online 後,在另一 terminal 執行:" echo " bash $0 forward" echo " 模擬前端打 API,看能不能透過 tunnel forward 到 fake local-tool" echo "" echo " Ctrl+C 結束 backend" echo "===================================================================" echo "" wait } cmd_token() { echo "[e2e] 向 $API_URL 要 pairing token..." if ! resp=$(curl -sf -X POST "$API_URL/api/pairing/token" 2>/dev/null); then echo "[e2e] ✗ api-server 沒回應(是否先跑 \`bash $0 backend\`?)" exit 1 fi if [ -z "$resp" ]; then echo "[e2e] ✗ api-server 回空回應" exit 1 fi # 安全萃取 token:JSON 結構不對時印友善錯誤而非 Python stack trace。 # 用 .get 鏈避免 KeyError;空字串再由 shell 檢查。 token=$(echo "$resp" | python3 -c ' import sys, json try: data = json.load(sys.stdin) except json.JSONDecodeError: sys.exit(0) print(data.get("data", {}).get("token", "") if isinstance(data, dict) else "") ' 2>/dev/null) if [ -z "$token" ]; then echo "[e2e] ✗ 無法從 api-server 回應萃取 pairing token" echo "[e2e] 原始回應:" echo "$resp" | head -c 500 echo "" echo "[e2e] 預期格式:{\"data\": {\"token\": \"...\"}}" exit 1 fi echo "" echo "pairing_token: $token" echo "" echo "把這個 token 貼到 agent UI 的「配對」欄位。" } cmd_forward() { echo "[e2e] 打 $API_URL/api/devices/scan(需要 agent 已連上 tunnel)..." resp=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST "$API_URL/api/devices/scan" \ -H "Content-Type: application/json") status=$(echo "$resp" | grep HTTP_STATUS | cut -d: -f2) body=$(echo "$resp" | sed '/HTTP_STATUS/d') echo "HTTP $status" echo "body:" echo "$body" | python3 -m json.tool 2>/dev/null || echo "$body" if [ "$status" = "200" ]; then echo "" echo "[e2e] ✓ 完整 e2e 鏈路通過:" echo " browser → api-server → remote-proxy → tunnel → agent → fake local-tool" if echo "$body" | grep -q "e2e-fake-local"; then echo "[e2e] ✓ 確認 response 來自 fake local-tool(X-Backend-Source 比對通過)" fi elif [ "$status" = "502" ]; then echo "" echo "[e2e] ✗ 502 — tunnel 未建立。可能原因:" echo " - agent 還沒啟動 / 還沒配對" echo " - session token 被清掉(backend 重啟過)" echo " - VISIONA_RELAY_HTTP_URL 沒指對" else echo "" echo "[e2e] ✗ 意外的狀態碼 $status" fi } cmd_health() { echo "[e2e] GET $API_URL/api/system/health" curl -sf "$API_URL/api/system/health" | python3 -m json.tool } usage() { cat <