#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" CLIENT_DIR="$ROOT_DIR/client" MODE="${1:-full}" DEV_SERVER_PID="" DEV_SERVER_LOG="" PLAYWRIGHT_RESULT_FILE="" PLAYWRIGHT_STDERR_LOG="" usage() { printf '%s\n' \ "Usage: bash scripts/qa/run-local-candidate.sh [full|focused|list]" \ "" \ " full 完整基础门禁 + 路由反例 + 本地 Vite 行为测试(默认)" \ " focused 路由反例 + 本地 Vite 行为测试,不代表完整候选通过" \ " list 只解析 Playwright 测试资产,不代表行为通过" } cleanup() { local exit_code="$?" if [[ -n "$DEV_SERVER_PID" ]] && kill -0 "$DEV_SERVER_PID" 2>/dev/null; then kill "$DEV_SERVER_PID" 2>/dev/null || true wait "$DEV_SERVER_PID" 2>/dev/null || true fi if [[ "$exit_code" -ne 0 ]]; then local vite_log_lines=0 local playwright_log_lines=0 if [[ -n "$DEV_SERVER_LOG" && -f "$DEV_SERVER_LOG" ]]; then vite_log_lines="$(wc -l < "$DEV_SERVER_LOG" | tr -d ' ')" fi if [[ -n "$PLAYWRIGHT_STDERR_LOG" && -f "$PLAYWRIGHT_STDERR_LOG" ]]; then playwright_log_lines="$(wc -l < "$PLAYWRIGHT_STDERR_LOG" | tr -d ' ')" fi echo "[local-candidate][ERROR] safe_summary exit=${exit_code} vite_log_lines=${vite_log_lines} playwright_log_lines=${playwright_log_lines}" >&2 fi [[ -z "$DEV_SERVER_LOG" ]] || rm -f "$DEV_SERVER_LOG" [[ -z "$PLAYWRIGHT_RESULT_FILE" ]] || rm -f "$PLAYWRIGHT_RESULT_FILE" [[ -z "$PLAYWRIGHT_STDERR_LOG" ]] || rm -f "$PLAYWRIGHT_STDERR_LOG" return "$exit_code" } trap cleanup EXIT case "$MODE" in full|focused|list) ;; help|-h|--help) usage exit 0 ;; *) usage >&2 exit 2 ;; esac cd "$ROOT_DIR" echo "=== [1/4] 路由 manifest 与反例测试 ===" node --test \ scripts/qa/tests/check-minimal-profile.test.mjs \ scripts/qa/tests/check-flow-coverage.test.mjs \ scripts/qa/tests/validate-loopback-url.test.mjs \ scripts/qa/tests/verify-local-candidate-results.test.mjs node scripts/qa/check-flow-coverage.mjs --local-candidate --json if [[ "$MODE" == "full" ]]; then echo "=== [2/4] 完整本地基础门禁 ===" bash scripts/check-minimal.sh local-candidate else echo "=== [2/4] 完整本地基础门禁(focused/list 跳过) ===" fi mapfile -t CANDIDATE_SPECS < <( node -e ' const manifest = require("./scripts/qa/route-manifest.json"); const expected = new Set(manifest.coreBundles ?? []); const flows = manifest.localCandidateFlows ?? []; const observed = new Set(flows.map((flow) => flow.bundle)); if (expected.size !== 10 || expected.size !== observed.size || [...expected].some((bundle) => !observed.has(bundle))) { throw new Error("localCandidateFlows 未完整覆盖 coreBundles"); } const witnessSpecs = flows.map((flow) => flow.witnessSpec); const specs = [...new Set([...witnessSpecs, ...(manifest.acceptanceSpecs ?? [])])]; for (const spec of specs) { if (typeof spec !== "string" || !spec.startsWith("client/tests/e2e/") || spec.includes("/manual/") || /live/i.test(spec)) { throw new Error(`非法本地候选 spec: ${String(spec)}`); } console.log(spec.slice("client/".length)); } ' ) if [[ "${#CANDIDATE_SPECS[@]}" -eq 0 ]]; then echo "[local-candidate][ERROR] route-manifest 未登记 localCandidateFlows" >&2 exit 1 fi if [[ "$MODE" == "list" ]]; then echo "=== [3/4] Playwright 资产解析(不代表行为通过) ===" ( cd "$CLIENT_DIR" npx playwright test "${CANDIDATE_SPECS[@]}" --list ) echo "=== [4/4] LIST_ONLY ===" exit 0 fi BASE_URL="${E2E_BASE_URL:-}" if [[ -z "$BASE_URL" ]]; then PORT="$(node scripts/qa/validate-loopback-url.mjs --port "${LOCAL_CANDIDATE_PORT:-5178}")" BASE_URL="$(node scripts/qa/validate-loopback-url.mjs "http://127.0.0.1:${PORT}")" DEV_SERVER_LOG="${TMPDIR:-/tmp}/hyb-oa-local-candidate-vite-${PORT}-$$.log" ( cd "$CLIENT_DIR" npm run dev -- --host 127.0.0.1 --port "$PORT" --strictPort ) >"$DEV_SERVER_LOG" 2>&1 & DEV_SERVER_PID="$!" VITE_READY=0 for _ in $(seq 1 60); do if curl --fail --silent --show-error "$BASE_URL/login" >/dev/null 2>&1; then VITE_READY=1 break fi if ! kill -0 "$DEV_SERVER_PID" 2>/dev/null; then echo "[local-candidate][ERROR] 本地 Vite 启动失败" >&2 exit 1 fi sleep 0.25 done if [[ "$VITE_READY" -ne 1 ]]; then echo "[local-candidate][ERROR] 本地 Vite 就绪超时" >&2 exit 1 fi else BASE_URL="$(node scripts/qa/validate-loopback-url.mjs "$BASE_URL")" fi echo "=== [3/4] 本地合成浏览器行为 ===" PLAYWRIGHT_RESULT_FILE="$(mktemp "${TMPDIR:-/tmp}/hyb-oa-local-candidate-results.XXXXXX.json")" PLAYWRIGHT_STDERR_LOG="$(mktemp "${TMPDIR:-/tmp}/hyb-oa-local-candidate-playwright.XXXXXX.log")" PLAYWRIGHT_STATUS=0 ( cd "$CLIENT_DIR" E2E_MODE=local E2E_BASE_URL="$BASE_URL" \ npx playwright test "${CANDIDATE_SPECS[@]}" --workers=1 --reporter=json ) >"$PLAYWRIGHT_RESULT_FILE" 2>"$PLAYWRIGHT_STDERR_LOG" || PLAYWRIGHT_STATUS="$?" RESULT_GATE_STATUS=0 node scripts/qa/verify-local-candidate-results.mjs \ --manifest scripts/qa/route-manifest.json \ --results "$PLAYWRIGHT_RESULT_FILE" || RESULT_GATE_STATUS="$?" if [[ "$PLAYWRIGHT_STATUS" -ne 0 || "$RESULT_GATE_STATUS" -ne 0 ]]; then echo "[local-candidate][ERROR] browser_exit=${PLAYWRIGHT_STATUS} witness_result_gate=${RESULT_GATE_STATUS}" >&2 exit 1 fi if [[ "$MODE" == "full" ]]; then echo "=== [4/4] LOCAL_CANDIDATE_PASS ===" else echo "=== [4/4] FOCUSED_BROWSER_PASS ===" fi