feat: harden runtime import and frontend verification workflows
This commit is contained in:
549
scripts/test/verify_frontend_smoke.sh
Executable file
549
scripts/test/verify_frontend_smoke.sh
Executable file
@@ -0,0 +1,549 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
PORTAL_ROOT="$ROOT_DIR/deploy/tksea-portal"
|
||||
ARTIFACT_DIR="${ARTIFACT_DIR:-$(mktemp -d "/tmp/sub2api-cn-relay-manager-frontend-smoke-XXXXXX")}"
|
||||
WORK_DIR="$(mktemp -d "/tmp/sub2api-cn-relay-manager-frontend-smoke-work-XXXXXX")"
|
||||
PORT_FILE="$WORK_DIR/server-port.txt"
|
||||
SERVER_LOG="$WORK_DIR/server.log"
|
||||
SERVER_SCRIPT="$WORK_DIR/frontend_smoke_server.py"
|
||||
CHROMIUM_BIN="${CHROMIUM_BIN:-}"
|
||||
USER_DATA_DIR="$WORK_DIR/chromium-profile"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${SERVER_PID:-}" ]]; then
|
||||
kill "$SERVER_PID" >/dev/null 2>&1 || true
|
||||
wait "$SERVER_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_contains_file() {
|
||||
local file="$1"
|
||||
local needle="$2"
|
||||
if ! grep -Fq "$needle" "$file"; then
|
||||
fail "expected [$needle] in $file"
|
||||
fi
|
||||
}
|
||||
|
||||
find_chromium() {
|
||||
if [[ -n "$CHROMIUM_BIN" ]]; then
|
||||
printf '%s\n' "$CHROMIUM_BIN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local candidate
|
||||
for candidate in chromium chromium-browser google-chrome google-chrome-stable; do
|
||||
if command -v "$candidate" >/dev/null 2>&1; then
|
||||
command -v "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
CHROMIUM_BIN="$(find_chromium)" || fail "missing chromium-compatible browser; set CHROMIUM_BIN explicitly"
|
||||
[[ -x "$CHROMIUM_BIN" ]] || fail "chromium binary is not executable: $CHROMIUM_BIN"
|
||||
[[ -d "$PORTAL_ROOT" ]] || fail "missing portal root: $PORTAL_ROOT"
|
||||
mkdir -p "$ARTIFACT_DIR" "$USER_DATA_DIR"
|
||||
|
||||
cat >"$SERVER_SCRIPT" <<'PY'
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
ROOT = Path(os.environ["PORTAL_ROOT"]).resolve()
|
||||
PORT_FILE = Path(os.environ["PORT_FILE"])
|
||||
|
||||
|
||||
def json_response(handler, payload, status=200):
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
handler.send_response(status)
|
||||
handler.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
handler.send_header("Content-Length", str(len(body)))
|
||||
handler.end_headers()
|
||||
handler.wfile.write(body)
|
||||
|
||||
|
||||
def text_response(handler, payload, status=200):
|
||||
body = payload.encode("utf-8")
|
||||
handler.send_response(status)
|
||||
handler.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
handler.send_header("Content-Length", str(len(body)))
|
||||
handler.end_headers()
|
||||
handler.wfile.write(body)
|
||||
|
||||
|
||||
def sample_portal_logical_groups():
|
||||
return {
|
||||
"logical_groups": [
|
||||
{
|
||||
"logical_group_id": "smoke-portal-group",
|
||||
"display_name": "Smoke Portal Group",
|
||||
"description": "用于最小前端 smoke 的逻辑分组样本。",
|
||||
"public_models": [{"public_model": "gpt-5.4"}],
|
||||
"active_route_count": 1,
|
||||
"visibility_scope": "public",
|
||||
"package_tier": "standard",
|
||||
"usage_scenario": "浏览器级 smoke 验证",
|
||||
"recommendation": "先确认页面可打开,再验证目录与导航。",
|
||||
"next_step_hint": "如需完整验收,再跑真实宿主 acceptance。",
|
||||
"purchase_cta_label": "申请测试 Key",
|
||||
"purchase_cta_url": "/portal/",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def sample_groups_available():
|
||||
return [
|
||||
{
|
||||
"id": 101,
|
||||
"name": "OpenAI 中转默认分组",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def sample_subscriptions():
|
||||
return [
|
||||
{
|
||||
"id": 501,
|
||||
"group_id": 101,
|
||||
"status": "active",
|
||||
"expires_at": "2099-12-31T00:00:00Z",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def sample_keys():
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Smoke Key",
|
||||
"group_id": 101,
|
||||
"key": "sk-smoke-visible-key",
|
||||
"status": "active",
|
||||
"created_at": "2099-01-01T00:00:00Z",
|
||||
"expires_at": "2099-12-31T00:00:00Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def sample_admin_session():
|
||||
return {
|
||||
"authenticated": True,
|
||||
"login_enabled": True,
|
||||
"username": "smoke-admin",
|
||||
"expires_at": "2099-12-31T00:00:00Z",
|
||||
}
|
||||
|
||||
|
||||
def sample_logical_groups():
|
||||
return {
|
||||
"logical_groups": [
|
||||
{
|
||||
"logical_group_id": "smoke-lg-001",
|
||||
"display_name": "Smoke Logical Group",
|
||||
"status": "active",
|
||||
"description": "Smoke logical group",
|
||||
"shadow_group_id": "shadow-smoke-group",
|
||||
"shadow_host_id": "shadow-smoke-host",
|
||||
"routes": [],
|
||||
"public_models": [],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def sample_route_health():
|
||||
return {
|
||||
"route_health": [
|
||||
{
|
||||
"route_id": "smoke-route-primary",
|
||||
"logical_group_id": "smoke-lg-001",
|
||||
"runtime_status": "healthy",
|
||||
"priority": 10,
|
||||
"weight": 100,
|
||||
"public_model_count": 1,
|
||||
"recent_failover_count": 0,
|
||||
"last_error_class": "",
|
||||
"cooldown_reason": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def sample_provider_accounts():
|
||||
return {
|
||||
"provider_accounts": [
|
||||
{
|
||||
"id": 2001,
|
||||
"display_name": "Smoke Provider Account",
|
||||
"provider_id": "smoke-provider",
|
||||
"host_id": "host-smoke-001",
|
||||
"status": "active",
|
||||
"binding_state": "assigned",
|
||||
"binding_candidate_count": 1,
|
||||
"logical_group_id": "smoke-lg-001",
|
||||
"route_id": "smoke-route-primary",
|
||||
"shadow_group_id": "shadow-smoke-group",
|
||||
"shadow_host_id": "shadow-smoke-host",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def sample_packs():
|
||||
return {
|
||||
"packs": [
|
||||
{
|
||||
"pack_id": "openai-cn-pack",
|
||||
"display_name": "OpenAI CN Pack",
|
||||
"provider_count": 1,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def sample_hosts():
|
||||
return {
|
||||
"hosts": [
|
||||
{
|
||||
"host_id": "host-smoke-001",
|
||||
"name": "Smoke Host",
|
||||
"base_url": "https://host-smoke.example.com",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def sample_pack_providers():
|
||||
return {
|
||||
"providers": [
|
||||
{
|
||||
"provider_id": "smoke-provider",
|
||||
"display_name": "Smoke Provider",
|
||||
"platform": "openai",
|
||||
"base_url": "https://provider-smoke.example.com/v1",
|
||||
"smoke_test_model": "gpt-5.4",
|
||||
"supported_models": ["gpt-5.4"],
|
||||
"host_overlays": 0,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def sample_provider_drafts():
|
||||
return {"provider_drafts": []}
|
||||
|
||||
|
||||
def sample_batch_run():
|
||||
return {
|
||||
"run": {
|
||||
"run_id": "smoke-run-001",
|
||||
"status": "succeeded",
|
||||
"matched_account_state_summary": {"created": 1},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def sample_batch_items():
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"provider_id": "smoke-provider",
|
||||
"matched_account_state": "created",
|
||||
"account_resolution": "created",
|
||||
"provision_reused": False,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
return
|
||||
|
||||
def serve_static(self, rel_path):
|
||||
file_path = (ROOT / rel_path).resolve()
|
||||
if not file_path.exists() or ROOT not in file_path.parents and file_path != ROOT:
|
||||
self.send_error(404, "not found")
|
||||
return
|
||||
content = file_path.read_bytes()
|
||||
mime_type, _ = mimetypes.guess_type(str(file_path))
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", f"{mime_type or 'text/plain'}; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
if path == "/healthz":
|
||||
text_response(self, "ok")
|
||||
return
|
||||
|
||||
if path == "/portal":
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/portal/")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
if path == "/portal/admin":
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/portal/admin/")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
if path == "/portal/":
|
||||
self.serve_static("index.html")
|
||||
return
|
||||
|
||||
if path == "/portal/admin/":
|
||||
self.serve_static("admin/index.html")
|
||||
return
|
||||
|
||||
if path.startswith("/portal/"):
|
||||
rel_path = path[len("/portal/"):]
|
||||
self.serve_static(rel_path)
|
||||
return
|
||||
|
||||
if path == "/portal-proxy/api/v1/auth/me":
|
||||
json_response(
|
||||
self,
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": 42,
|
||||
"email": "smoke-user@example.com",
|
||||
"allowed_groups": [101],
|
||||
},
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if path == "/portal-proxy/api/v1/groups/available":
|
||||
json_response(self, {"code": 0, "data": sample_groups_available()})
|
||||
return
|
||||
|
||||
if path == "/portal-proxy/api/v1/subscriptions":
|
||||
json_response(self, {"code": 0, "data": sample_subscriptions()})
|
||||
return
|
||||
|
||||
if path == "/portal-proxy/api/v1/keys":
|
||||
json_response(self, {"code": 0, "data": sample_keys()})
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/portal/logical-groups":
|
||||
json_response(self, sample_portal_logical_groups())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/admin/session":
|
||||
json_response(self, sample_admin_session())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/logical-groups":
|
||||
json_response(self, sample_logical_groups())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/routing/routes/health":
|
||||
json_response(self, sample_route_health())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/provider-accounts":
|
||||
json_response(self, sample_provider_accounts())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/packs":
|
||||
json_response(self, sample_packs())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/hosts":
|
||||
json_response(self, sample_hosts())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/provider-drafts":
|
||||
json_response(self, sample_provider_drafts())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/batch-import/runs/smoke-run-001":
|
||||
json_response(self, sample_batch_run())
|
||||
return
|
||||
|
||||
if path == "/portal-admin-api/api/batch-import/runs/smoke-run-001/items":
|
||||
json_response(self, sample_batch_items())
|
||||
return
|
||||
|
||||
if path.startswith("/portal-admin-api/api/packs/") and path.endswith("/providers"):
|
||||
json_response(self, sample_pack_providers())
|
||||
return
|
||||
|
||||
if path.startswith("/portal-admin-api/api/provider-accounts/") and path.endswith("/binding-candidates"):
|
||||
json_response(
|
||||
self,
|
||||
{
|
||||
"binding_candidates": [
|
||||
{
|
||||
"logical_group_id": "smoke-lg-001",
|
||||
"route_id": "smoke-route-primary",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if path.startswith("/portal-admin-api/api/"):
|
||||
json_response(self, {"ok": True, "path": path, "query": query})
|
||||
return
|
||||
|
||||
self.send_error(404, "not found")
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
if path == "/portal-admin-api/api/admin/session/login":
|
||||
json_response(self, sample_admin_session())
|
||||
return
|
||||
if path == "/portal-admin-api/api/admin/session/logout":
|
||||
json_response(self, {"ok": True})
|
||||
return
|
||||
if path == "/portal-admin-api/api/batch-import/runs":
|
||||
json_response(
|
||||
self,
|
||||
{
|
||||
"run": {
|
||||
"run_id": "smoke-run-001",
|
||||
"status": "created",
|
||||
}
|
||||
},
|
||||
)
|
||||
return
|
||||
json_response(self, {"ok": True, "path": path})
|
||||
|
||||
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
PORT_FILE.write_text(str(server.server_port), encoding="utf-8")
|
||||
server.serve_forever()
|
||||
PY
|
||||
|
||||
PORTAL_ROOT="$PORTAL_ROOT" PORT_FILE="$PORT_FILE" python3 "$SERVER_SCRIPT" >"$SERVER_LOG" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
for _ in $(seq 1 50); do
|
||||
if [[ -s "$PORT_FILE" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [[ ! -s "$PORT_FILE" ]]; then
|
||||
if [[ -s "$SERVER_LOG" ]]; then
|
||||
cat "$SERVER_LOG" >&2 || true
|
||||
fi
|
||||
fail "frontend smoke server did not start"
|
||||
fi
|
||||
SERVER_PORT="$(cat "$PORT_FILE")"
|
||||
BASE_URL="http://127.0.0.1:$SERVER_PORT"
|
||||
|
||||
for _ in $(seq 1 50); do
|
||||
if curl -fsS "$BASE_URL/healthz" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
curl -fsS "$BASE_URL/healthz" >/dev/null 2>&1 || fail "frontend smoke server is not healthy"
|
||||
|
||||
dump_dom() {
|
||||
local label="$1"
|
||||
local url="$2"
|
||||
local output="$ARTIFACT_DIR/${label}.dom.html"
|
||||
"$CHROMIUM_BIN" \
|
||||
--headless \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--user-data-dir="$USER_DATA_DIR/$label" \
|
||||
--virtual-time-budget=10000 \
|
||||
--dump-dom \
|
||||
"$url" >"$output" 2>"$ARTIFACT_DIR/${label}.stderr.txt"
|
||||
printf '%s\n' "$output"
|
||||
}
|
||||
|
||||
portal_dom="$(dump_dom "00-portal" "$BASE_URL/portal/")"
|
||||
admin_home_dom="$(dump_dom "01-admin-home" "$BASE_URL/portal/admin/")"
|
||||
logical_groups_dom="$(dump_dom "02-logical-groups" "$BASE_URL/portal/admin/logical-groups.html")"
|
||||
route_health_dom="$(dump_dom "03-route-health" "$BASE_URL/portal/admin/route-health.html")"
|
||||
accounts_dom="$(dump_dom "04-accounts" "$BASE_URL/portal/admin/accounts.html")"
|
||||
providers_dom="$(dump_dom "05-providers" "$BASE_URL/portal/admin/providers.html")"
|
||||
batch_dom="$(dump_dom "06-batch-import" "$BASE_URL/portal/admin-batch-import.html")"
|
||||
compat_batch_dom="$(dump_dom "07-batch-import-compat" "$BASE_URL/portal/admin/batch-import.html")"
|
||||
|
||||
assert_contains_file "$portal_dom" "Sub2API 多模型接入中心"
|
||||
assert_contains_file "$portal_dom" "Smoke Portal Group"
|
||||
assert_contains_file "$portal_dom" "逻辑分组目录"
|
||||
assert_contains_file "$portal_dom" "申请测试 Key"
|
||||
|
||||
assert_contains_file "$admin_home_dom" "Admin Portal"
|
||||
assert_contains_file "$admin_home_dom" "/portal/admin/providers.html"
|
||||
assert_contains_file "$admin_home_dom" "/portal/admin/accounts.html"
|
||||
|
||||
assert_contains_file "$logical_groups_dom" "Logical Group Admin"
|
||||
assert_contains_file "$logical_groups_dom" "smoke-admin"
|
||||
assert_contains_file "$logical_groups_dom" "Smoke Logical Group"
|
||||
|
||||
assert_contains_file "$route_health_dom" "Route Health Admin"
|
||||
assert_contains_file "$route_health_dom" "smoke-admin"
|
||||
assert_contains_file "$route_health_dom" "smoke-route-primary"
|
||||
|
||||
assert_contains_file "$accounts_dom" "Provider Accounts Admin"
|
||||
assert_contains_file "$accounts_dom" "smoke-admin"
|
||||
assert_contains_file "$accounts_dom" "Smoke Provider Account"
|
||||
|
||||
assert_contains_file "$providers_dom" "Provider Admin"
|
||||
assert_contains_file "$providers_dom" "smoke-admin"
|
||||
assert_contains_file "$providers_dom" "保存到服务端"
|
||||
|
||||
assert_contains_file "$batch_dom" "Batch Import Admin"
|
||||
assert_contains_file "$batch_dom" "smoke-admin"
|
||||
assert_contains_file "$batch_dom" "matched_account_state"
|
||||
|
||||
assert_contains_file "$compat_batch_dom" "Batch Import Admin"
|
||||
assert_contains_file "$compat_batch_dom" "smoke-admin"
|
||||
|
||||
cat >"$ARTIFACT_DIR/99-summary.json" <<EOF
|
||||
{
|
||||
"server_port": $SERVER_PORT,
|
||||
"portal_url": "$BASE_URL/portal/",
|
||||
"admin_urls": [
|
||||
"$BASE_URL/portal/admin/",
|
||||
"$BASE_URL/portal/admin/logical-groups.html",
|
||||
"$BASE_URL/portal/admin/route-health.html",
|
||||
"$BASE_URL/portal/admin/accounts.html",
|
||||
"$BASE_URL/portal/admin/providers.html",
|
||||
"$BASE_URL/portal/admin-batch-import.html",
|
||||
"$BASE_URL/portal/admin/batch-import.html"
|
||||
],
|
||||
"session_username": "smoke-admin",
|
||||
"result": "pass"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "PASS: frontend browser smoke passed"
|
||||
echo "artifact dir: $ARTIFACT_DIR"
|
||||
Reference in New Issue
Block a user