feat: harden runtime import and frontend verification workflows
This commit is contained in:
@@ -20,6 +20,10 @@
|
||||
- 例如:
|
||||
- `real_host_acceptance.sh`
|
||||
- `import_remote43_provider.sh`
|
||||
- `verify_frontend_acceptance_matrix.sh`
|
||||
- `verify_portal_catalog_ui.sh`
|
||||
- `verify_public_portal_browser.sh`
|
||||
- `verify_accounts_admin_ui.sh`
|
||||
- `verify_provider_admin_actions.sh`
|
||||
- `check_deepseek_completion_split.sh`
|
||||
- `scripts/test/`
|
||||
@@ -27,6 +31,7 @@
|
||||
- 例如:
|
||||
- `test_real_host_scripts.sh`
|
||||
- `test_tksea_portal_assets.sh`
|
||||
- `verify_frontend_smoke.sh`
|
||||
- `verify_quality_gates.sh`
|
||||
|
||||
## 放置规则
|
||||
@@ -40,9 +45,12 @@
|
||||
```bash
|
||||
bash ./scripts/test/test_real_host_scripts.sh
|
||||
bash ./scripts/test/test_tksea_portal_assets.sh
|
||||
bash ./scripts/test/verify_frontend_smoke.sh
|
||||
bash ./scripts/test/verify_quality_gates.sh
|
||||
scripts/deploy/build_local_image.sh
|
||||
bash ./scripts/acceptance/real_host_acceptance.sh
|
||||
bash ./scripts/acceptance/verify_frontend_acceptance_matrix.sh
|
||||
bash ./scripts/acceptance/verify_public_portal_browser.sh
|
||||
bash ./scripts/acceptance/verify_provider_admin_actions.sh
|
||||
```
|
||||
|
||||
@@ -51,6 +59,8 @@ bash ./scripts/acceptance/verify_provider_admin_actions.sh
|
||||
`scripts/test/verify_quality_gates.sh` 是当前推荐的一键测试入口,职责是:
|
||||
|
||||
- 统一执行:
|
||||
- `bash ./scripts/test/test_tksea_portal_assets.sh`
|
||||
- `bash ./scripts/test/verify_frontend_smoke.sh`
|
||||
- `gofmt -l .`
|
||||
- `go vet ./...`
|
||||
- `go test -cover ./internal/...`
|
||||
|
||||
109
scripts/acceptance/verify_accounts_admin_ui.sh
Normal file
109
scripts/acceptance/verify_accounts_admin_ui.sh
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT_DIR/scripts/acceptance/route_acceptance_lib.sh"
|
||||
|
||||
ACCOUNTS_ACCEPTANCE_ROOT="${ACCOUNTS_ACCEPTANCE_ROOT:-$ROOT_DIR/artifacts/frontend-acceptance-matrix}"
|
||||
TS="${TS:-$(timestamp_token)}"
|
||||
ARTIFACT_DIR="${ARTIFACT_DIR:-$ACCOUNTS_ACCEPTANCE_ROOT/${TS}_accounts_admin_ui}"
|
||||
|
||||
ACCOUNTS_PAGE_URL="${ACCOUNTS_PAGE_URL:-https://sub.tksea.top/portal/admin/accounts.html}"
|
||||
ACCOUNT_ID="${ACCOUNT_ID:-}"
|
||||
HOST_ID_FILTER="${HOST_ID_FILTER:-}"
|
||||
PROVIDER_ID_FILTER="${PROVIDER_ID_FILTER:-}"
|
||||
BINDING_STATE_FILTER="${BINDING_STATE_FILTER:-}"
|
||||
LIMIT="${LIMIT:-50}"
|
||||
ALLOW_EMPTY_ACCOUNTS="${ALLOW_EMPTY_ACCOUNTS:-0}"
|
||||
|
||||
require_var CRM_BASE
|
||||
crm_auth_init
|
||||
ensure_artifact_dir
|
||||
curl_status_to_file "$ACCOUNTS_PAGE_URL" "$ARTIFACT_DIR/00-accounts-admin.html"
|
||||
|
||||
query="$(
|
||||
python3 - "$HOST_ID_FILTER" "$PROVIDER_ID_FILTER" "$BINDING_STATE_FILTER" "$LIMIT" <<'PY'
|
||||
import sys
|
||||
from urllib.parse import urlencode
|
||||
|
||||
host_id, provider_id, binding_state, limit = sys.argv[1:5]
|
||||
params = {}
|
||||
if host_id:
|
||||
params["host_id"] = host_id
|
||||
if provider_id:
|
||||
params["provider_id"] = provider_id
|
||||
if binding_state:
|
||||
params["binding_state"] = binding_state
|
||||
if limit:
|
||||
params["limit"] = limit
|
||||
print(urlencode(params))
|
||||
PY
|
||||
)"
|
||||
|
||||
list_path="/api/provider-accounts"
|
||||
if [[ -n "$query" ]]; then
|
||||
list_path="$list_path?$query"
|
||||
fi
|
||||
|
||||
save_json 01-provider-accounts "$(crm_curl_json GET "$list_path")"
|
||||
|
||||
if [[ -z "$ACCOUNT_ID" ]]; then
|
||||
ACCOUNT_ID="$(
|
||||
python3 - "$ARTIFACT_DIR/01-provider-accounts.json" "$ALLOW_EMPTY_ACCOUNTS" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.load(open(sys.argv[1], "r", encoding="utf-8"))
|
||||
allow_empty = sys.argv[2] == "1"
|
||||
items = payload.get("provider_accounts") or []
|
||||
if not items:
|
||||
if allow_empty:
|
||||
raise SystemExit(3)
|
||||
raise SystemExit(2)
|
||||
first = items[0]
|
||||
print(first.get("id") or "")
|
||||
PY
|
||||
)" || ACCOUNT_ID=""
|
||||
fi
|
||||
|
||||
if [[ -n "$ACCOUNT_ID" ]]; then
|
||||
save_json 02-binding-candidates "$(crm_curl_json GET "/api/provider-accounts/$ACCOUNT_ID/binding-candidates")"
|
||||
fi
|
||||
|
||||
python3 - "$ARTIFACT_DIR" "$ACCOUNT_ID" "$ALLOW_EMPTY_ACCOUNTS" >"$ARTIFACT_DIR/99-summary.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
art_dir = Path(sys.argv[1])
|
||||
account_id = sys.argv[2]
|
||||
allow_empty = sys.argv[3] == "1"
|
||||
|
||||
page = (art_dir / "00-accounts-admin.html").read_text(encoding="utf-8")
|
||||
accounts = json.loads((art_dir / "01-provider-accounts.json").read_text(encoding="utf-8")).get("provider_accounts") or []
|
||||
|
||||
assert "Provider Accounts Admin" in page
|
||||
if not accounts and not allow_empty:
|
||||
raise AssertionError("provider_accounts list is empty")
|
||||
|
||||
summary = {
|
||||
"page_title_seen": "Provider Accounts Admin" in page,
|
||||
"account_count": len(accounts),
|
||||
"selected_account_id": account_id or "",
|
||||
}
|
||||
|
||||
if accounts:
|
||||
first = accounts[0]
|
||||
summary["first_account_provider_id"] = first.get("provider_id")
|
||||
summary["first_account_status"] = first.get("status") or first.get("account_status")
|
||||
summary["first_account_binding_state"] = first.get("binding_state")
|
||||
|
||||
if account_id:
|
||||
candidates = json.loads((art_dir / "02-binding-candidates.json").read_text(encoding="utf-8")).get("binding_candidates") or []
|
||||
summary["binding_candidate_count"] = len(candidates)
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
|
||||
cat "$ARTIFACT_DIR/99-summary.json"
|
||||
112
scripts/acceptance/verify_frontend_acceptance_matrix.sh
Normal file
112
scripts/acceptance/verify_frontend_acceptance_matrix.sh
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
MATRIX_ROOT="${FRONTEND_MATRIX_ROOT:-$ROOT_DIR/artifacts/frontend-acceptance-matrix}"
|
||||
TS="${TS:-$(date +%s)}"
|
||||
MATRIX_DIR="${MATRIX_DIR:-$MATRIX_ROOT/${TS}_frontend_matrix}"
|
||||
|
||||
BROWSER_SMOKE_SCRIPT="${BROWSER_SMOKE_SCRIPT:-$ROOT_DIR/scripts/test/verify_frontend_smoke.sh}"
|
||||
PORTAL_ACCEPTANCE_SCRIPT="${PORTAL_ACCEPTANCE_SCRIPT:-$ROOT_DIR/scripts/acceptance/verify_portal_catalog_ui.sh}"
|
||||
PUBLIC_PORTAL_BROWSER_SCRIPT="${PUBLIC_PORTAL_BROWSER_SCRIPT:-$ROOT_DIR/scripts/acceptance/verify_public_portal_browser.sh}"
|
||||
ACCOUNTS_ACCEPTANCE_SCRIPT="${ACCOUNTS_ACCEPTANCE_SCRIPT:-$ROOT_DIR/scripts/acceptance/verify_accounts_admin_ui.sh}"
|
||||
ROUTE_MATRIX_SCRIPT="${ROUTE_MATRIX_SCRIPT:-$ROOT_DIR/scripts/acceptance/verify_route_acceptance_matrix.sh}"
|
||||
PROVIDER_ADMIN_SCRIPT="${PROVIDER_ADMIN_SCRIPT:-$ROOT_DIR/scripts/acceptance/verify_provider_admin_actions.sh}"
|
||||
RUN_PUBLIC_PORTAL_BROWSER="${RUN_PUBLIC_PORTAL_BROWSER:-0}"
|
||||
|
||||
mkdir -p "$MATRIX_DIR"
|
||||
|
||||
run_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
echo "==> $name"
|
||||
ARTIFACT_DIR="$MATRIX_DIR/$name" "$@" >"$MATRIX_DIR/$name.stdout.txt" 2>"$MATRIX_DIR/$name.stderr.txt"
|
||||
}
|
||||
|
||||
mark_skip() {
|
||||
local name="$1"
|
||||
local reason="$2"
|
||||
printf '%s\n' "$reason" >"$MATRIX_DIR/$name.skip.txt"
|
||||
}
|
||||
|
||||
has_crm_auth() {
|
||||
[[ -n "${CRM_ADMIN_TOKEN:-}" ]] || { [[ -n "${CRM_ADMIN_USERNAME:-}" ]] && [[ -n "${CRM_ADMIN_PASSWORD:-}" ]]; }
|
||||
}
|
||||
|
||||
run_step browser_smoke bash "$BROWSER_SMOKE_SCRIPT"
|
||||
run_step portal_catalog bash "$PORTAL_ACCEPTANCE_SCRIPT"
|
||||
|
||||
if [[ "$RUN_PUBLIC_PORTAL_BROWSER" == "1" ]]; then
|
||||
run_step portal_public_browser bash "$PUBLIC_PORTAL_BROWSER_SCRIPT"
|
||||
else
|
||||
mark_skip portal_public_browser "set RUN_PUBLIC_PORTAL_BROWSER=1 to execute public portal browser verification"
|
||||
fi
|
||||
|
||||
if [[ -n "${CRM_BASE:-}" ]] && has_crm_auth; then
|
||||
run_step accounts_admin bash "$ACCOUNTS_ACCEPTANCE_SCRIPT"
|
||||
else
|
||||
mark_skip accounts_admin "missing CRM_BASE or CRM auth; set CRM_BASE with CRM_ADMIN_TOKEN or CRM_ADMIN_USERNAME/CRM_ADMIN_PASSWORD"
|
||||
fi
|
||||
|
||||
if [[ -n "${CRM_BASE:-}" ]] && has_crm_auth && [[ -n "${SHADOW_HOST_ID:-}" ]] && [[ -n "${SHADOW_GROUP_ID:-}" ]] && { [[ -n "${SUBSCRIPTION_USER_ID:-}" ]] || [[ -n "${GATEWAY_API_KEY:-}" ]]; }; then
|
||||
run_step route_matrix bash "$ROUTE_MATRIX_SCRIPT"
|
||||
else
|
||||
mark_skip route_matrix "missing CRM auth or route data-plane env; require CRM_BASE, auth, SHADOW_HOST_ID, SHADOW_GROUP_ID, and SUBSCRIPTION_USER_ID or GATEWAY_API_KEY"
|
||||
fi
|
||||
|
||||
if [[ -n "${CRM_BASE:-}" ]] && has_crm_auth && [[ -n "${ACCESS_API_KEY:-}" ]] && [[ -n "${PROVIDER_KEYS:-}" ]]; then
|
||||
run_step provider_admin bash "$PROVIDER_ADMIN_SCRIPT"
|
||||
else
|
||||
mark_skip provider_admin "missing provider admin env; require CRM_BASE, auth, ACCESS_API_KEY, and PROVIDER_KEYS"
|
||||
fi
|
||||
|
||||
python3 - "$MATRIX_DIR" >"$MATRIX_DIR/summary.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
matrix_dir = Path(sys.argv[1])
|
||||
|
||||
def load_json(path):
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
def step_result(name, summary_file):
|
||||
step_dir = matrix_dir / name
|
||||
if step_dir.exists():
|
||||
return {"status": "ok", "artifact_dir": str(step_dir), "summary": load_json(step_dir / summary_file)}
|
||||
skip_file = matrix_dir / f"{name}.skip.txt"
|
||||
if skip_file.exists():
|
||||
return {"status": "skipped", "reason": skip_file.read_text(encoding="utf-8").strip()}
|
||||
return {"status": "missing"}
|
||||
|
||||
browser = step_result("browser_smoke", "99-summary.json")
|
||||
portal = step_result("portal_catalog", "99-summary.json")
|
||||
portal_public_browser = step_result("portal_public_browser", "99-summary.json")
|
||||
accounts = step_result("accounts_admin", "99-summary.json")
|
||||
route = step_result("route_matrix", "summary.json")
|
||||
provider = step_result("provider_admin", "99-summary.json")
|
||||
|
||||
summary = {
|
||||
"matrix_dir": str(matrix_dir),
|
||||
"steps": {
|
||||
"browser_smoke": browser,
|
||||
"portal_catalog": portal,
|
||||
"portal_public_browser": portal_public_browser,
|
||||
"accounts_admin": accounts,
|
||||
"route_matrix": route,
|
||||
"provider_admin": provider,
|
||||
},
|
||||
"page_mapping": {
|
||||
"portal": ["browser_smoke", "portal_catalog", "portal_public_browser"],
|
||||
"admin_index": ["browser_smoke"],
|
||||
"logical_groups": ["browser_smoke", "route_matrix"],
|
||||
"route_health": ["browser_smoke", "route_matrix"],
|
||||
"accounts": ["browser_smoke", "accounts_admin"],
|
||||
"providers": ["browser_smoke", "provider_admin"],
|
||||
"batch_import": ["browser_smoke"],
|
||||
},
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
|
||||
cat "$MATRIX_DIR/summary.json"
|
||||
94
scripts/acceptance/verify_portal_catalog_ui.sh
Normal file
94
scripts/acceptance/verify_portal_catalog_ui.sh
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT_DIR/scripts/acceptance/route_acceptance_lib.sh"
|
||||
|
||||
PORTAL_ACCEPTANCE_ROOT="${PORTAL_ACCEPTANCE_ROOT:-$ROOT_DIR/artifacts/frontend-acceptance-matrix}"
|
||||
TS="${TS:-$(timestamp_token)}"
|
||||
ARTIFACT_DIR="${ARTIFACT_DIR:-$PORTAL_ACCEPTANCE_ROOT/${TS}_portal_catalog_ui}"
|
||||
|
||||
PORTAL_PAGE_URL="${PORTAL_PAGE_URL:-https://sub.tksea.top/portal/}"
|
||||
PORTAL_CATALOG_BASE="${PORTAL_CATALOG_BASE:-https://sub.tksea.top/portal-admin-api/api/portal}"
|
||||
PORTAL_PROXY_BASE="${PORTAL_PROXY_BASE:-https://sub.tksea.top/portal-proxy/api/v1}"
|
||||
PORTAL_ACCESS_TOKEN="${PORTAL_ACCESS_TOKEN:-}"
|
||||
|
||||
ensure_artifact_dir
|
||||
curl_status_to_file "$PORTAL_PAGE_URL" "$ARTIFACT_DIR/00-portal.html"
|
||||
|
||||
curl -fsS "${PORTAL_CATALOG_BASE%/}/logical-groups" >"$ARTIFACT_DIR/01-logical-groups.json"
|
||||
|
||||
first_group_id="$(
|
||||
python3 - "$ARTIFACT_DIR/01-logical-groups.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.load(open(sys.argv[1], "r", encoding="utf-8"))
|
||||
items = payload.get("logical_groups") or []
|
||||
if not items:
|
||||
raise SystemExit(2)
|
||||
first = items[0]
|
||||
print(first.get("logical_group_id") or "")
|
||||
PY
|
||||
)" || first_group_id=""
|
||||
|
||||
if [[ -n "$first_group_id" ]]; then
|
||||
curl -fsS "${PORTAL_CATALOG_BASE%/}/logical-groups/${first_group_id}/models" >"$ARTIFACT_DIR/02-group-models.json"
|
||||
fi
|
||||
|
||||
if [[ -n "$PORTAL_ACCESS_TOKEN" ]]; then
|
||||
curl -fsS -H "Authorization: Bearer $PORTAL_ACCESS_TOKEN" "${PORTAL_PROXY_BASE%/}/auth/me" >"$ARTIFACT_DIR/03-auth-me.json"
|
||||
curl -fsS -H "Authorization: Bearer $PORTAL_ACCESS_TOKEN" "${PORTAL_PROXY_BASE%/}/groups/available" >"$ARTIFACT_DIR/04-groups-available.json"
|
||||
curl -fsS -H "Authorization: Bearer $PORTAL_ACCESS_TOKEN" "${PORTAL_PROXY_BASE%/}/subscriptions" >"$ARTIFACT_DIR/05-subscriptions.json"
|
||||
curl -fsS -H "Authorization: Bearer $PORTAL_ACCESS_TOKEN" "${PORTAL_PROXY_BASE%/}/keys?page=1&page_size=20" >"$ARTIFACT_DIR/06-keys.json"
|
||||
fi
|
||||
|
||||
python3 - "$ARTIFACT_DIR" "$PORTAL_ACCESS_TOKEN" >"$ARTIFACT_DIR/99-summary.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
art_dir = Path(sys.argv[1])
|
||||
access_token = sys.argv[2]
|
||||
|
||||
page = (art_dir / "00-portal.html").read_text(encoding="utf-8")
|
||||
catalog = json.loads((art_dir / "01-logical-groups.json").read_text(encoding="utf-8"))
|
||||
groups = catalog.get("logical_groups") or []
|
||||
|
||||
assert "Sub2API 多模型接入中心" in page
|
||||
assert "逻辑分组目录" in page
|
||||
assert groups, groups
|
||||
|
||||
summary = {
|
||||
"page_url": "portal",
|
||||
"page_title_seen": "Sub2API 多模型接入中心" in page,
|
||||
"logical_group_count": len(groups),
|
||||
"first_logical_group_id": groups[0].get("logical_group_id"),
|
||||
"first_logical_group_display_name": groups[0].get("display_name"),
|
||||
"user_projection_checked": bool(access_token),
|
||||
}
|
||||
|
||||
models_file = art_dir / "02-group-models.json"
|
||||
if models_file.exists():
|
||||
models_payload = json.loads(models_file.read_text(encoding="utf-8"))
|
||||
public_models = models_payload.get("public_models") or []
|
||||
summary["first_group_models_count"] = len(public_models)
|
||||
if public_models:
|
||||
summary["first_group_first_model"] = public_models[0].get("public_model")
|
||||
|
||||
if access_token:
|
||||
auth_me = json.loads((art_dir / "03-auth-me.json").read_text(encoding="utf-8"))
|
||||
groups_available = json.loads((art_dir / "04-groups-available.json").read_text(encoding="utf-8"))
|
||||
subscriptions = json.loads((art_dir / "05-subscriptions.json").read_text(encoding="utf-8"))
|
||||
keys_page = json.loads((art_dir / "06-keys.json").read_text(encoding="utf-8"))
|
||||
summary["auth_me_present"] = bool(auth_me.get("data") or auth_me)
|
||||
summary["available_group_count"] = len((groups_available.get("data") if isinstance(groups_available, dict) else groups_available) or [])
|
||||
summary["subscription_count"] = len((subscriptions.get("data") if isinstance(subscriptions, dict) else subscriptions) or [])
|
||||
key_data = keys_page.get("data") if isinstance(keys_page, dict) else keys_page
|
||||
summary["key_count"] = len((key_data or {}).get("items") or [])
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
|
||||
cat "$ARTIFACT_DIR/99-summary.json"
|
||||
120
scripts/acceptance/verify_public_portal_browser.sh
Normal file
120
scripts/acceptance/verify_public_portal_browser.sh
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT_DIR/scripts/acceptance/route_acceptance_lib.sh"
|
||||
|
||||
PORTAL_ACCEPTANCE_ROOT="${PORTAL_ACCEPTANCE_ROOT:-$ROOT_DIR/artifacts/frontend-acceptance-matrix}"
|
||||
TS="${TS:-$(timestamp_token)}"
|
||||
ARTIFACT_DIR="${ARTIFACT_DIR:-$PORTAL_ACCEPTANCE_ROOT/${TS}_portal_public_browser}"
|
||||
|
||||
PUBLIC_PORTAL_PAGE_URL="${PUBLIC_PORTAL_PAGE_URL:-https://sub.tksea.top/portal/}"
|
||||
PUBLIC_PORTAL_CATALOG_BASE="${PUBLIC_PORTAL_CATALOG_BASE:-https://sub.tksea.top/portal-admin-api/api/portal}"
|
||||
PUBLIC_PORTAL_PROXY_BASE="${PUBLIC_PORTAL_PROXY_BASE:-https://sub.tksea.top/portal-proxy/api/v1}"
|
||||
PORTAL_ACCESS_TOKEN="${PORTAL_ACCESS_TOKEN:-}"
|
||||
CHROMIUM_BIN="${CHROMIUM_BIN:-}"
|
||||
VIRTUAL_TIME_BUDGET="${VIRTUAL_TIME_BUDGET:-5000}"
|
||||
USER_DATA_DIR="$ARTIFACT_DIR/chromium-profile"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
dump_dom() {
|
||||
local label="$1"
|
||||
local url="$2"
|
||||
local output="$ARTIFACT_DIR/${label}.dom.html"
|
||||
"$CHROMIUM_BIN" \
|
||||
--headless \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--no-proxy-server \
|
||||
--user-data-dir="$USER_DATA_DIR/$label" \
|
||||
--virtual-time-budget="$VIRTUAL_TIME_BUDGET" \
|
||||
--dump-dom \
|
||||
"$url" >"$output" 2>"$ARTIFACT_DIR/${label}.stderr.txt"
|
||||
printf '%s\n' "$output"
|
||||
}
|
||||
|
||||
CHROMIUM_BIN="$(find_chromium)" || fail "missing chromium-compatible browser; set CHROMIUM_BIN explicitly"
|
||||
[[ -x "$CHROMIUM_BIN" ]] || fail "chromium binary is not executable: $CHROMIUM_BIN"
|
||||
ensure_artifact_dir
|
||||
mkdir -p "$USER_DATA_DIR"
|
||||
|
||||
portal_dom="$(dump_dom "00-portal" "$PUBLIC_PORTAL_PAGE_URL")"
|
||||
assert_contains_file "$portal_dom" "Sub2API 多模型接入中心"
|
||||
assert_contains_file "$portal_dom" "逻辑分组目录"
|
||||
assert_contains_file "$portal_dom" "申请 Key 依赖状态"
|
||||
assert_contains_file "$portal_dom" "可直接申请"
|
||||
assert_contains_file "$portal_dom" "可申请,调用前需确认状态"
|
||||
assert_contains_file "$portal_dom" "待补开通"
|
||||
assert_contains_file "$portal_dom" "待人工整理"
|
||||
assert_contains_file "$portal_dom" "仅目录可见"
|
||||
|
||||
PORTAL_PAGE_URL="$PUBLIC_PORTAL_PAGE_URL" \
|
||||
PORTAL_CATALOG_BASE="$PUBLIC_PORTAL_CATALOG_BASE" \
|
||||
PORTAL_PROXY_BASE="$PUBLIC_PORTAL_PROXY_BASE" \
|
||||
PORTAL_ACCESS_TOKEN="$PORTAL_ACCESS_TOKEN" \
|
||||
ARTIFACT_DIR="$ARTIFACT_DIR/catalog_api" \
|
||||
bash "$ROOT_DIR/scripts/acceptance/verify_portal_catalog_ui.sh" >"$ARTIFACT_DIR/portal_catalog.stdout.txt"
|
||||
|
||||
python3 - "$ARTIFACT_DIR" "$PUBLIC_PORTAL_PAGE_URL" "$PORTAL_ACCESS_TOKEN" >"$ARTIFACT_DIR/99-summary.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
art_dir = Path(sys.argv[1])
|
||||
page_url = sys.argv[2]
|
||||
access_token = sys.argv[3]
|
||||
|
||||
page = (art_dir / "00-portal.dom.html").read_text(encoding="utf-8")
|
||||
catalog_summary = json.loads((art_dir / "catalog_api" / "99-summary.json").read_text(encoding="utf-8"))
|
||||
|
||||
summary = {
|
||||
"page_url": page_url,
|
||||
"page_title_seen": "Sub2API 多模型接入中心" in page,
|
||||
"logical_group_catalog_seen": "逻辑分组目录" in page,
|
||||
"dependency_panel_seen": "申请 Key 依赖状态" in page,
|
||||
"dependency_state_copy_seen": {
|
||||
"ready": "可直接申请" in page,
|
||||
"granted": "可申请,调用前需确认状态" in page,
|
||||
"pending": "待补开通" in page,
|
||||
"ambiguous": "待人工整理" in page,
|
||||
"catalog_only": "仅目录可见" in page,
|
||||
},
|
||||
"user_projection_checked": bool(access_token),
|
||||
"catalog_api_summary": catalog_summary,
|
||||
"result": "pass",
|
||||
}
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
|
||||
cat "$ARTIFACT_DIR/99-summary.json"
|
||||
0
scripts/deploy/deploy_crm_only.sh
Normal file → Executable file
0
scripts/deploy/deploy_crm_only.sh
Normal file → Executable file
@@ -6,8 +6,8 @@ KEY="${KEY:-/home/long/下载/zjsea.pem}"
|
||||
REMOTE="${REMOTE:-ubuntu@43.155.133.187}"
|
||||
REMOTE_PORTAL_DIR="${REMOTE_PORTAL_DIR:-/var/www/sub2api-portal}"
|
||||
REMOTE_NGINX_SITE="${REMOTE_NGINX_SITE:-/etc/nginx/sites-available/tksea}"
|
||||
REMOTE_HOST_PORT="${REMOTE_HOST_PORT:-18169}"
|
||||
REMOTE_CRM_PORT="${REMOTE_CRM_PORT:-18173}"
|
||||
REMOTE_HOST_PORT="${REMOTE_HOST_PORT:-8080}"
|
||||
REMOTE_CRM_PORT="${REMOTE_CRM_PORT:-18190}"
|
||||
LOCAL_PORTAL_DIR="${LOCAL_PORTAL_DIR:-$ROOT_DIR/deploy/tksea-portal}"
|
||||
REMOTE_STAGE_DIR="${REMOTE_STAGE_DIR:-/tmp/sub2api-portal-deploy}"
|
||||
DRY_RUN="${DRY_RUN:-0}"
|
||||
|
||||
116
scripts/test/check_coverage.sh
Executable file
116
scripts/test/check_coverage.sh
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# check_coverage.sh — Check Go project coverage against thresholds
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/test/check_coverage.sh [min-percentage]
|
||||
#
|
||||
# Default threshold: 85% (matches Hermes config agent.min_test_coverage)
|
||||
#
|
||||
# Reads thresholds from:
|
||||
# 1. CLI argument (highest priority)
|
||||
# 2. tests/quality/coverage_thresholds.tsv (per-package thresholds)
|
||||
# 3. Default 85%
|
||||
#
|
||||
# Output:
|
||||
# - Coverage report (stdout)
|
||||
# - Exit code 1 if any package below threshold
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
DEFAULT_THRESHOLD="${1:-85}"
|
||||
THRESHOLD_FILE="${PROJECT_DIR}/tests/quality/coverage_thresholds.tsv"
|
||||
|
||||
echo "==> coverage threshold: ${DEFAULT_THRESHOLD}%"
|
||||
|
||||
# Collect coverage
|
||||
cd "$PROJECT_DIR"
|
||||
COVERAGE_OUT=$(mktemp)
|
||||
go test -count=1 -cover ./internal/... 2>&1 | tee "$COVERAGE_OUT"
|
||||
|
||||
# Check per-package thresholds from file if exists
|
||||
HAS_FILE_THRESHOLDS=false
|
||||
declare -A PACKAGE_THRESHOLDS
|
||||
if [[ -f "$THRESHOLD_FILE" ]]; then
|
||||
HAS_FILE_THRESHOLDS=true
|
||||
while IFS=$'\t' read -r pkg threshold; do
|
||||
[[ -z "$pkg" || "$pkg" == \#* ]] && continue
|
||||
PACKAGE_THRESHOLDS["$pkg"]="$threshold"
|
||||
done < "$THRESHOLD_FILE"
|
||||
fi
|
||||
|
||||
# Parse and validate
|
||||
EXIT_CODE=0
|
||||
declare -a FAILURES=()
|
||||
CURRENT_PKG=""
|
||||
TOTAL_PKG=0
|
||||
PASS_PKG=0
|
||||
|
||||
parse_coverage_line() {
|
||||
local line="$1"
|
||||
# Match: ok github.com/xxx/sub2api-cn-relay-manager/internal/provision 0.012s coverage: 82.8% of statements
|
||||
# Match: ? github.com/xxx/sub2api-cn-relay-manager/internal/provision [no test files]
|
||||
if [[ "$line" =~ ^ok[[:space:]]+.*/[^[:space:]]+[[:space:]]+[0-9.]+s[[:space:]]+coverage:[[:space:]]+([0-9.]+)% ]]; then
|
||||
local pct="${BASH_REMATCH[1]}"
|
||||
local pkg_name
|
||||
pkg_name=$(echo "$line" | awk '{print $2}' | awk -F'/' '{print $NF}')
|
||||
local threshold="$DEFAULT_THRESHOLD"
|
||||
if $HAS_FILE_THRESHOLDS && [[ -n "${PACKAGE_THRESHOLDS[$pkg_name]:-}" ]]; then
|
||||
threshold="${PACKAGE_THRESHOLDS[$pkg_name]}"
|
||||
fi
|
||||
TOTAL_PKG=$((TOTAL_PKG + 1))
|
||||
if (( $(echo "$pct < $threshold" | bc -l 2>/dev/null || echo 1) )); then
|
||||
FAILURES+=("${pkg_name}: ${pct}% < ${threshold}%")
|
||||
EXIT_CODE=1
|
||||
else
|
||||
PASS_PKG=$((PASS_PKG + 1))
|
||||
echo " ✅ ${pkg_name}: ${pct}% (threshold: ${threshold}%)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS= read -r line; do
|
||||
parse_coverage_line "$line"
|
||||
done < "$COVERAGE_OUT"
|
||||
|
||||
echo ""
|
||||
echo "==> summary: ${PASS_PKG}/${TOTAL_PKG} packages pass coverage threshold"
|
||||
if [[ ${#FAILURES[@]} -gt 0 ]]; then
|
||||
echo "FAILURES:"
|
||||
for f in "${FAILURES[@]}"; do
|
||||
echo " ❌ $f"
|
||||
done
|
||||
fi
|
||||
|
||||
# Optionally generate markdown report
|
||||
REPORT_FILE="${COVERAGE_REPORT:-}"
|
||||
if [[ -n "$REPORT_FILE" ]]; then
|
||||
{
|
||||
echo "# Coverage Report ($(date +%Y-%m-%d))"
|
||||
echo ""
|
||||
echo "| Package | Coverage | Threshold | Status |"
|
||||
echo "|---------|----------|-----------|--------|"
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^ok[[:space:]]+.*/[^[:space:]]+[[:space:]]+[0-9.]+s[[:space:]]+coverage:[[:space:]]+([0-9.]+)% ]]; then
|
||||
pkg=$(echo "$line" | awk '{print $2}' | awk -F'/' '{print $NF}')
|
||||
pct="${BASH_REMATCH[1]}"
|
||||
threshold="$DEFAULT_THRESHOLD"
|
||||
if $HAS_FILE_THRESHOLDS && [[ -n "${PACKAGE_THRESHOLDS[$pkg]:-}" ]]; then
|
||||
threshold="${PACKAGE_THRESHOLDS[$pkg]}"
|
||||
fi
|
||||
status="✅"
|
||||
if (( $(echo "$pct < $threshold" | bc -l 2>/dev/null || echo 1) )); then
|
||||
status="❌"
|
||||
fi
|
||||
echo "| ${pkg} | ${pct}% | ${threshold}% | ${status} |"
|
||||
fi
|
||||
done < "$COVERAGE_OUT"
|
||||
echo ""
|
||||
echo "**Overall: ${PASS_PKG}/${TOTAL_PKG} passing, exit $([ $EXIT_CODE -eq 0 ] && echo 0 || echo 1)**"
|
||||
} > "$REPORT_FILE"
|
||||
echo "Report: $REPORT_FILE"
|
||||
fi
|
||||
|
||||
rm -f "$COVERAGE_OUT"
|
||||
exit $EXIT_CODE
|
||||
104
scripts/test/init_test_plan.sh
Executable file
104
scripts/test/init_test_plan.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# init_test_plan.sh — Generate task-level test plan + test case template
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/test/init_test_plan.sh <task-name> [target-dir]
|
||||
#
|
||||
# Example:
|
||||
# bash scripts/test/init_test_plan.sh "preflight-host-readiness" internal/provision
|
||||
#
|
||||
# Output:
|
||||
# docs/test-plans/TEST_PLAN_YYYY-MM-DD_<task-name>.md
|
||||
# docs/test-cases/TEST_CASES_YYYY-MM-DD_<task-name>.md
|
||||
#
|
||||
# These are TEMPLATES. Fill in the actual test cases before implementation,
|
||||
# and mark PASS/FAIL after verification.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
TASK_NAME="${1:?Usage: $0 <task-name> [target-dir]}"
|
||||
TARGET_DIR="${2:-.}"
|
||||
DATE_TAG="$(date +%Y-%m-%d)"
|
||||
|
||||
PLAN_DIR="${SCRIPT_DIR}/docs/test-plans"
|
||||
CASE_DIR="${SCRIPT_DIR}/docs/test-cases"
|
||||
mkdir -p "$PLAN_DIR" "$CASE_DIR"
|
||||
|
||||
# ── Test Plan Template ──────────────────────────────────────────────
|
||||
PLAN_FILE="${PLAN_DIR}/TEST_PLAN_${DATE_TAG}_${TASK_NAME}.md"
|
||||
cat > "$PLAN_FILE" << EOFTPL
|
||||
# Test Plan: ${TASK_NAME}
|
||||
|
||||
日期: ${DATE_TAG}
|
||||
目标文件: ${TARGET_DIR}
|
||||
|
||||
## 测试目标
|
||||
|
||||
<!-- 一句话说明本任务的测试目标 -->
|
||||
|
||||
## 范围
|
||||
|
||||
- 影响文件: <!-- 列出被修改的文件 -->
|
||||
- 风险点: <!-- 可能出问题的地方 -->
|
||||
- 不变区域: <!-- 明确不需要测试的范围 -->
|
||||
|
||||
## 验证层级
|
||||
|
||||
| 层级 | 验证内容 | 命令 | 通过标准 |
|
||||
|------|----------|------|----------|
|
||||
| L1 单测 | | | |
|
||||
| L2 静态分析 | | | |
|
||||
| L3 集成测试 | | | |
|
||||
| L4 构建验证 | | | |
|
||||
| L5 前端验证 | | | |
|
||||
| L6 真实环境 | | | |
|
||||
|
||||
## 测试用例
|
||||
|
||||
见 docs/test-cases/TEST_CASES_${DATE_TAG}_${TASK_NAME}.md
|
||||
|
||||
## 回归检查
|
||||
|
||||
- [ ] 已有测试不受影响
|
||||
- [ ] 覆盖率不低于当前基线
|
||||
EOFTPL
|
||||
|
||||
# ── Test Cases Template ──────────────────────────────────────────────
|
||||
CASE_FILE="${CASE_DIR}/TEST_CASES_${DATE_TAG}_${TASK_NAME}.md"
|
||||
cat > "$CASE_FILE" << EOFCASE
|
||||
# Test Cases: ${TASK_NAME}
|
||||
|
||||
日期: ${DATE_TAG}
|
||||
|
||||
## 用例列表
|
||||
|
||||
| ID | 描述 | 输入 | 预期结果 | 实际结果 | 状态 |
|
||||
|----|------|------|----------|----------|------|
|
||||
| TC1 | | | | | PENDING |
|
||||
| TC2 | | | | | PENDING |
|
||||
| TC3 | | | | | PENDING |
|
||||
|
||||
## 边界用例
|
||||
|
||||
| ID | 描述 | 输入 | 预期结果 | 实际结果 | 状态 |
|
||||
|----|------|------|----------|----------|------|
|
||||
| B1 | | | | | PENDING |
|
||||
| B2 | | | | | PENDING |
|
||||
|
||||
## 异常用例
|
||||
|
||||
| ID | 描述 | 输入 | 预期结果 | 实际结果 | 状态 |
|
||||
|----|------|------|----------|----------|------|
|
||||
| E1 | | | | | PENDING |
|
||||
| E2 | | | | | PENDING |
|
||||
EOFCASE
|
||||
|
||||
echo "✅ Test plan: ${PLAN_FILE}"
|
||||
echo "✅ Test cases: ${CASE_FILE}"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo " 1. 编辑测试计划和用例"
|
||||
echo " 2. 实现代码"
|
||||
echo " 3. 按用例逐条验证"
|
||||
echo " 4. 更新状态为 PASS/FAIL"
|
||||
@@ -271,6 +271,8 @@ run_test_verify_quality_gates_script() {
|
||||
[[ -f "$threshold_file" ]] || fail "missing $threshold_file"
|
||||
|
||||
script_contents="$(cat "$script")"
|
||||
assert_contains "$script_contents" "test_tksea_portal_assets.sh"
|
||||
assert_contains "$script_contents" "verify_frontend_smoke.sh"
|
||||
assert_contains "$script_contents" "gofmt -l ."
|
||||
assert_contains "$script_contents" "go vet ./..."
|
||||
assert_contains "$script_contents" "go test -cover ./internal/..."
|
||||
@@ -520,6 +522,8 @@ EOF
|
||||
CRM_HOST_BASE="http://127.0.0.1:18093" \
|
||||
REMOTE_HOST_BASE="http://127.0.0.1:18093" \
|
||||
HOST_NAME="human-friendly-host-name" \
|
||||
REMOTE_PG_CONTAINER="sub2api-fresh-deepseek-20260519_115244-postgres-1" \
|
||||
REMOTE_REDIS_CONTAINER="sub2api-fresh-deepseek-20260519_115244-redis-1" \
|
||||
ROOT="$artifact_dir/root" \
|
||||
ART="$artifact_dir/run" \
|
||||
PACK_PATH="$pack_dir" \
|
||||
@@ -580,7 +584,8 @@ EOF
|
||||
assert_contains "$ssh_contents" "http://127.0.0.1:18093/v1/chat/completions"
|
||||
assert_not_contains "$ssh_contents" "http://127.0.0.1:18087/v1/models"
|
||||
assert_not_contains "$ssh_contents" "http://127.0.0.1:18087/v1/chat/completions"
|
||||
assert_not_contains "$ssh_contents" "user-key"
|
||||
assert_contains "$ssh_contents" "Authorization: Bearer user-key"
|
||||
assert_not_contains "$ssh_contents" "Authorization: Bearer sk-rel"
|
||||
|
||||
local provider_status
|
||||
provider_status="$(cat "$artifact_dir/run/13-provider-status.json")"
|
||||
@@ -1055,6 +1060,314 @@ EOF
|
||||
assert_contains "$summary" '"fallback_recent_failover_count": 1'
|
||||
}
|
||||
|
||||
run_test_verify_portal_catalog_ui_script() {
|
||||
local tmpdir fakebin artifact_dir stdout_file
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' RETURN
|
||||
fakebin="$tmpdir/bin"
|
||||
artifact_dir="$tmpdir/artifacts"
|
||||
stdout_file="$tmpdir/verify_portal_catalog_ui.stdout.txt"
|
||||
mkdir -p "$fakebin" "$artifact_dir"
|
||||
|
||||
cat > "$fakebin/curl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
url=""
|
||||
output_file=""
|
||||
prev=""
|
||||
for arg in "$@"; do
|
||||
case "$prev" in
|
||||
-o) output_file="$arg"; prev=""; continue ;;
|
||||
esac
|
||||
case "$arg" in
|
||||
-o) prev="$arg"; continue ;;
|
||||
http://*|https://*) url="$arg" ;;
|
||||
esac
|
||||
done
|
||||
write_body() {
|
||||
local body="$1"
|
||||
if [[ -n "$output_file" ]]; then
|
||||
printf '%s\n' "$body" > "$output_file"
|
||||
else
|
||||
printf '%s\n' "$body"
|
||||
fi
|
||||
}
|
||||
case "$url" in
|
||||
http://portal.example.com/)
|
||||
write_body '<html><title>Sub2API 多模型接入中心</title><body>逻辑分组目录</body></html>'
|
||||
;;
|
||||
http://crm.example.com/api/portal/logical-groups)
|
||||
write_body '{"logical_groups":[{"logical_group_id":"portal-group-001","display_name":"Portal Group 001"}]}'
|
||||
;;
|
||||
http://crm.example.com/api/portal/logical-groups/portal-group-001/models)
|
||||
write_body '{"public_models":[{"public_model":"gpt-5.4"}]}'
|
||||
;;
|
||||
http://proxy.example.com/auth/me)
|
||||
write_body '{"code":0,"data":{"id":42,"email":"portal@example.com"}}'
|
||||
;;
|
||||
http://proxy.example.com/groups/available)
|
||||
write_body '{"code":0,"data":[{"id":101,"name":"Portal Group"}]}'
|
||||
;;
|
||||
http://proxy.example.com/subscriptions)
|
||||
write_body '{"code":0,"data":[{"id":1,"group_id":101,"status":"active"}]}'
|
||||
;;
|
||||
"http://proxy.example.com/keys?page=1&page_size=20")
|
||||
write_body '{"code":0,"data":{"items":[{"id":1,"group_id":101,"key":"sk-visible"}]}}'
|
||||
;;
|
||||
*)
|
||||
echo "unexpected curl url: $url" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$fakebin/curl"
|
||||
|
||||
PATH="$fakebin:$PATH" \
|
||||
PORTAL_PAGE_URL="http://portal.example.com/" \
|
||||
PORTAL_CATALOG_BASE="http://crm.example.com/api/portal" \
|
||||
PORTAL_PROXY_BASE="http://proxy.example.com" \
|
||||
PORTAL_ACCESS_TOKEN="portal-token" \
|
||||
ARTIFACT_DIR="$artifact_dir" \
|
||||
bash "$ROOT_DIR/scripts/acceptance/verify_portal_catalog_ui.sh" >"$stdout_file"
|
||||
|
||||
local summary
|
||||
summary="$(cat "$artifact_dir/99-summary.json")"
|
||||
assert_contains "$summary" '"page_title_seen": true'
|
||||
assert_contains "$summary" '"logical_group_count": 1'
|
||||
assert_contains "$summary" '"first_logical_group_id": "portal-group-001"'
|
||||
assert_contains "$summary" '"user_projection_checked": true'
|
||||
assert_contains "$summary" '"key_count": 1'
|
||||
}
|
||||
|
||||
run_test_verify_public_portal_browser_script() {
|
||||
local tmpdir fakebin artifact_dir stdout_file
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' RETURN
|
||||
fakebin="$tmpdir/bin"
|
||||
artifact_dir="$tmpdir/artifacts"
|
||||
stdout_file="$tmpdir/verify_public_portal_browser.stdout.txt"
|
||||
mkdir -p "$fakebin" "$artifact_dir"
|
||||
|
||||
cat > "$fakebin/curl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
url=""
|
||||
output_file=""
|
||||
prev=""
|
||||
for arg in "$@"; do
|
||||
case "$prev" in
|
||||
-o) output_file="$arg"; prev=""; continue ;;
|
||||
esac
|
||||
case "$arg" in
|
||||
-o) prev="$arg"; continue ;;
|
||||
http://*|https://*) url="$arg" ;;
|
||||
esac
|
||||
done
|
||||
write_body() {
|
||||
local body="$1"
|
||||
if [[ -n "$output_file" ]]; then
|
||||
printf '%s\n' "$body" > "$output_file"
|
||||
else
|
||||
printf '%s\n' "$body"
|
||||
fi
|
||||
}
|
||||
case "$url" in
|
||||
http://portal.example.com/portal/)
|
||||
write_body '<html><title>Sub2API 多模型接入中心</title><body>逻辑分组目录 申请 Key 依赖状态</body></html>'
|
||||
;;
|
||||
http://crm.example.com/api/portal/logical-groups)
|
||||
write_body '{"logical_groups":[{"logical_group_id":"portal-group-001","display_name":"Portal Group 001"}]}'
|
||||
;;
|
||||
http://crm.example.com/api/portal/logical-groups/portal-group-001/models)
|
||||
write_body '{"public_models":[{"public_model":"gpt-5.4"}]}'
|
||||
;;
|
||||
http://proxy.example.com/auth/me)
|
||||
write_body '{"code":0,"data":{"id":42,"email":"portal@example.com"}}'
|
||||
;;
|
||||
http://proxy.example.com/groups/available)
|
||||
write_body '{"code":0,"data":[{"id":101,"name":"Portal Group"}]}'
|
||||
;;
|
||||
http://proxy.example.com/subscriptions)
|
||||
write_body '{"code":0,"data":[{"id":1,"group_id":101,"status":"active"}]}'
|
||||
;;
|
||||
"http://proxy.example.com/keys?page=1&page_size=20")
|
||||
write_body '{"code":0,"data":{"items":[{"id":1,"group_id":101,"key":"sk-visible"}]}}'
|
||||
;;
|
||||
*)
|
||||
echo "unexpected curl url: $url" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
cat > "$fakebin/chromium" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' '<html><title>Sub2API 多模型接入中心</title><body>逻辑分组目录 申请 Key 依赖状态 可直接申请 可申请,调用前需确认状态 待补开通 待人工整理 仅目录可见</body></html>'
|
||||
EOF
|
||||
chmod +x "$fakebin/curl" "$fakebin/chromium"
|
||||
|
||||
PATH="$fakebin:$PATH" \
|
||||
CHROMIUM_BIN="$fakebin/chromium" \
|
||||
PUBLIC_PORTAL_PAGE_URL="http://portal.example.com/portal/" \
|
||||
PUBLIC_PORTAL_CATALOG_BASE="http://crm.example.com/api/portal" \
|
||||
PUBLIC_PORTAL_PROXY_BASE="http://proxy.example.com" \
|
||||
PORTAL_ACCESS_TOKEN="portal-token" \
|
||||
ARTIFACT_DIR="$artifact_dir" \
|
||||
bash "$ROOT_DIR/scripts/acceptance/verify_public_portal_browser.sh" >"$stdout_file"
|
||||
|
||||
local summary
|
||||
summary="$(cat "$artifact_dir/99-summary.json")"
|
||||
assert_contains "$summary" '"dependency_panel_seen": true'
|
||||
assert_contains "$summary" '"page_title_seen": true'
|
||||
assert_contains "$summary" '"logical_group_count": 1'
|
||||
assert_contains "$summary" '"user_projection_checked": true'
|
||||
assert_contains "$summary" '"result": "pass"'
|
||||
}
|
||||
|
||||
run_test_verify_accounts_admin_ui_script() {
|
||||
local tmpdir fakebin artifact_dir stdout_file
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' RETURN
|
||||
fakebin="$tmpdir/bin"
|
||||
artifact_dir="$tmpdir/artifacts"
|
||||
stdout_file="$tmpdir/verify_accounts_admin_ui.stdout.txt"
|
||||
mkdir -p "$fakebin" "$artifact_dir"
|
||||
|
||||
cat > "$fakebin/curl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
method="GET"
|
||||
url=""
|
||||
output_file=""
|
||||
prev=""
|
||||
for arg in "$@"; do
|
||||
case "$prev" in
|
||||
-X) method="$arg"; prev=""; continue ;;
|
||||
-o) output_file="$arg"; prev=""; continue ;;
|
||||
esac
|
||||
case "$arg" in
|
||||
-X|-o) prev="$arg"; continue ;;
|
||||
http://*|https://*) url="$arg" ;;
|
||||
esac
|
||||
done
|
||||
write_body() {
|
||||
local body="$1"
|
||||
if [[ -n "$output_file" ]]; then
|
||||
printf '%s\n' "$body" > "$output_file"
|
||||
else
|
||||
printf '%s\n' "$body"
|
||||
fi
|
||||
}
|
||||
case "$method $url" in
|
||||
"GET http://portal.example.com/accounts.html")
|
||||
write_body '<html><title>Provider Accounts Admin</title><body>Provider Accounts Admin</body></html>'
|
||||
;;
|
||||
"GET http://crm.example.com/api/provider-accounts?limit=50")
|
||||
write_body '{"provider_accounts":[{"id":1,"provider_id":"gpt-asxs-shadow-lab","status":"active","binding_state":"conflict"}]}'
|
||||
;;
|
||||
"GET http://crm.example.com/api/provider-accounts/1/binding-candidates")
|
||||
write_body '{"binding_candidates":[{"route_id":"primary-1"},{"route_id":"fallback-1"}]}'
|
||||
;;
|
||||
*)
|
||||
echo "unexpected curl request: $method $url" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$fakebin/curl"
|
||||
|
||||
PATH="$fakebin:$PATH" \
|
||||
CRM_BASE="http://crm.example.com" \
|
||||
CRM_ADMIN_TOKEN="token" \
|
||||
ACCOUNTS_PAGE_URL="http://portal.example.com/accounts.html" \
|
||||
ARTIFACT_DIR="$artifact_dir" \
|
||||
bash "$ROOT_DIR/scripts/acceptance/verify_accounts_admin_ui.sh" >"$stdout_file"
|
||||
|
||||
local summary
|
||||
summary="$(cat "$artifact_dir/99-summary.json")"
|
||||
assert_contains "$summary" '"page_title_seen": true'
|
||||
assert_contains "$summary" '"account_count": 1'
|
||||
assert_contains "$summary" '"selected_account_id": "1"'
|
||||
assert_contains "$summary" '"binding_candidate_count": 2'
|
||||
}
|
||||
|
||||
run_test_verify_frontend_acceptance_matrix_script() {
|
||||
local tmpdir matrix_dir browser_script portal_script public_portal_browser_script accounts_script route_script provider_script stdout_file
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' RETURN
|
||||
matrix_dir="$tmpdir/matrix"
|
||||
stdout_file="$tmpdir/verify_frontend_acceptance_matrix.stdout.txt"
|
||||
browser_script="$tmpdir/browser.sh"
|
||||
portal_script="$tmpdir/portal.sh"
|
||||
public_portal_browser_script="$tmpdir/public-portal-browser.sh"
|
||||
accounts_script="$tmpdir/accounts.sh"
|
||||
route_script="$tmpdir/route.sh"
|
||||
provider_script="$tmpdir/provider.sh"
|
||||
|
||||
cat > "$browser_script" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
printf '%s\n' '{"result":"pass","page_title_seen":true}' > "$ARTIFACT_DIR/99-summary.json"
|
||||
EOF
|
||||
cat > "$portal_script" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
printf '%s\n' '{"logical_group_count":1,"page_title_seen":true}' > "$ARTIFACT_DIR/99-summary.json"
|
||||
EOF
|
||||
cat > "$public_portal_browser_script" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
printf '%s\n' '{"page_title_seen":true,"dependency_panel_seen":true,"result":"pass"}' > "$ARTIFACT_DIR/99-summary.json"
|
||||
EOF
|
||||
cat > "$accounts_script" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
printf '%s\n' '{"account_count":1,"page_title_seen":true}' > "$ARTIFACT_DIR/99-summary.json"
|
||||
EOF
|
||||
cat > "$route_script" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
printf '%s\n' '{"control_plane_group_id":"lg-1","health_ui_group_id":"lg-2","data_plane_group_id":"lg-3"}' > "$ARTIFACT_DIR/summary.json"
|
||||
EOF
|
||||
cat > "$provider_script" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
printf '%s\n' '{"page_title_seen":true,"import_batch_id":321}' > "$ARTIFACT_DIR/99-summary.json"
|
||||
EOF
|
||||
chmod +x "$browser_script" "$portal_script" "$public_portal_browser_script" "$accounts_script" "$route_script" "$provider_script"
|
||||
|
||||
CRM_BASE="http://crm.example.com" \
|
||||
CRM_ADMIN_TOKEN="token" \
|
||||
SHADOW_HOST_ID="shadow-host-1" \
|
||||
SHADOW_GROUP_ID="shadow-group-1" \
|
||||
SUBSCRIPTION_USER_ID="42" \
|
||||
ACCESS_API_KEY="sk-access" \
|
||||
PROVIDER_KEYS="sk-provider-1" \
|
||||
RUN_PUBLIC_PORTAL_BROWSER="1" \
|
||||
MATRIX_DIR="$matrix_dir" \
|
||||
BROWSER_SMOKE_SCRIPT="$browser_script" \
|
||||
PORTAL_ACCEPTANCE_SCRIPT="$portal_script" \
|
||||
PUBLIC_PORTAL_BROWSER_SCRIPT="$public_portal_browser_script" \
|
||||
ACCOUNTS_ACCEPTANCE_SCRIPT="$accounts_script" \
|
||||
ROUTE_MATRIX_SCRIPT="$route_script" \
|
||||
PROVIDER_ADMIN_SCRIPT="$provider_script" \
|
||||
bash "$ROOT_DIR/scripts/acceptance/verify_frontend_acceptance_matrix.sh" >"$stdout_file"
|
||||
|
||||
local summary
|
||||
summary="$(cat "$matrix_dir/summary.json")"
|
||||
assert_contains "$summary" '"browser_smoke"'
|
||||
assert_contains "$summary" '"status": "ok"'
|
||||
assert_contains "$summary" '"portal_public_browser"'
|
||||
assert_contains "$summary" '"portal": ['
|
||||
assert_contains "$summary" '"providers": ['
|
||||
}
|
||||
|
||||
run_test_remote43_patched_stack_renderers() {
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT_DIR/scripts/deploy/remote43_patched_stack_lib.sh"
|
||||
@@ -1183,6 +1496,10 @@ run_test_verify_route_control_plane_script
|
||||
run_test_verify_route_data_plane_script
|
||||
run_test_verify_provider_admin_actions_script
|
||||
run_test_verify_route_health_ui_script
|
||||
run_test_verify_portal_catalog_ui_script
|
||||
run_test_verify_public_portal_browser_script
|
||||
run_test_verify_accounts_admin_ui_script
|
||||
run_test_verify_frontend_acceptance_matrix_script
|
||||
run_test_remote43_patched_stack_renderers
|
||||
run_test_setup_remote43_patched_stack_dry_run
|
||||
run_test_verify_quality_gates_script
|
||||
|
||||
@@ -4,6 +4,8 @@ set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
HTML_FILE="$ROOT_DIR/deploy/tksea-portal/index.html"
|
||||
ADMIN_HTML_FILE="$ROOT_DIR/deploy/tksea-portal/admin-batch-import.html"
|
||||
ADMIN_COMMON_CSS_FILE="$ROOT_DIR/deploy/tksea-portal/admin-common.css"
|
||||
ADMIN_COMMON_JS_FILE="$ROOT_DIR/deploy/tksea-portal/admin-common.js"
|
||||
ADMIN_HOME_FILE="$ROOT_DIR/deploy/tksea-portal/admin/index.html"
|
||||
ADMIN_LOGICAL_GROUPS_FILE="$ROOT_DIR/deploy/tksea-portal/admin/logical-groups.html"
|
||||
ADMIN_ROUTE_HEALTH_FILE="$ROOT_DIR/deploy/tksea-portal/admin/route-health.html"
|
||||
@@ -28,6 +30,8 @@ assert_contains_file() {
|
||||
|
||||
[[ -f "$HTML_FILE" ]] || fail "missing $HTML_FILE"
|
||||
[[ -f "$ADMIN_HTML_FILE" ]] || fail "missing $ADMIN_HTML_FILE"
|
||||
[[ -f "$ADMIN_COMMON_CSS_FILE" ]] || fail "missing $ADMIN_COMMON_CSS_FILE"
|
||||
[[ -f "$ADMIN_COMMON_JS_FILE" ]] || fail "missing $ADMIN_COMMON_JS_FILE"
|
||||
[[ -f "$ADMIN_HOME_FILE" ]] || fail "missing $ADMIN_HOME_FILE"
|
||||
[[ -f "$ADMIN_LOGICAL_GROUPS_FILE" ]] || fail "missing $ADMIN_LOGICAL_GROUPS_FILE"
|
||||
[[ -f "$ADMIN_ROUTE_HEALTH_FILE" ]] || fail "missing $ADMIN_ROUTE_HEALTH_FILE"
|
||||
@@ -52,12 +56,12 @@ assert_contains_file "$HTML_FILE" "showToast"
|
||||
assert_contains_file "$HTML_FILE" "逻辑分组目录"
|
||||
assert_contains_file "$HTML_FILE" "已激活产品权限"
|
||||
assert_contains_file "$HTML_FILE" "权限与订阅视图"
|
||||
assert_contains_file "$HTML_FILE" "可立即申请兼容 Key"
|
||||
assert_contains_file "$HTML_FILE" "需开通兼容线路"
|
||||
assert_contains_file "$HTML_FILE" "可立即申请测试 Key"
|
||||
assert_contains_file "$HTML_FILE" "待补开通"
|
||||
assert_contains_file "$HTML_FILE" "目录已上线"
|
||||
assert_contains_file "$HTML_FILE" "选择逻辑分组"
|
||||
assert_contains_file "$HTML_FILE" "当前逻辑分组说明"
|
||||
assert_contains_file "$HTML_FILE" "兼容宿主线路"
|
||||
assert_contains_file "$HTML_FILE" "申请 Key 依赖状态"
|
||||
assert_contains_file "$HTML_FILE" "portalLogicalGroups"
|
||||
assert_contains_file "$HTML_FILE" "LEGACY_GROUP_CATALOG"
|
||||
assert_contains_file "$HTML_FILE" "逻辑分组权限"
|
||||
@@ -82,34 +86,48 @@ assert_contains_file "$HTML_FILE" "cta-link"
|
||||
assert_contains_file "$HTML_FILE" "已开通订阅"
|
||||
assert_contains_file "$HTML_FILE" "已授予权限"
|
||||
assert_contains_file "$HTML_FILE" "归属待整理"
|
||||
assert_contains_file "$HTML_FILE" "依赖链路"
|
||||
assert_contains_file "$HTML_FILE" "申请资格"
|
||||
assert_contains_file "$HTML_FILE" "route_policy ="
|
||||
assert_contains_file "$HTML_FILE" "gpt-5.4"
|
||||
assert_contains_file "$HTML_FILE" "MiniMax-M2.7-highspeed"
|
||||
assert_contains_file "$HTML_FILE" "deepseek-chat"
|
||||
|
||||
assert_contains_file "$ADMIN_COMMON_CSS_FILE" ".topnav"
|
||||
assert_contains_file "$ADMIN_COMMON_CSS_FILE" ".statusbar"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "Sub2ApiAdminCommon"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "createAdminPageRuntime"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "renderAdminNav"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "Authorization"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" 'credentials: "include"'
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/api/admin/session/login"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/api/admin/session/logout"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/api/admin/session"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/portal/admin/"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/portal/admin/logical-groups.html"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/portal/admin/route-health.html"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/portal/admin/accounts.html"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/portal/admin/providers.html"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/portal/admin/batch-import.html"
|
||||
assert_contains_file "$ADMIN_COMMON_JS_FILE" "/portal/"
|
||||
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "Batch Import Admin"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin/"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin/logical-groups.html"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin/providers.html"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin/batch-import.html"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin/accounts.html"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin-common.css"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin-common.js"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal-admin-api"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "matched_account_state"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "account_resolution"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/api/batch-import/runs"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/api/batch-import/runs/"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" '/items${query ?'
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "Authorization"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "base_url|api_key|requested_model_1,requested_model_2"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "reused"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "reactivated"
|
||||
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "Admin Portal"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/portal/admin/logical-groups.html"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/portal/admin/route-health.html"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/portal/admin/accounts.html"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/portal/admin/providers.html"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/portal/admin/batch-import.html"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/portal/admin-common.css"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/portal/admin-common.js"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "data-admin-nav"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/portal-admin-api"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "浏览器提交到 CRM"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "逻辑分组 / 路由"
|
||||
@@ -118,15 +136,9 @@ assert_contains_file "$ADMIN_HOME_FILE" "帐号资产"
|
||||
assert_contains_file "$ADMIN_HOME_FILE" "/accounts"
|
||||
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "Logical Group Admin"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal/admin/"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal/admin/logical-groups.html"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal/admin/route-health.html"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal/admin/accounts.html"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal/admin/providers.html"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal/admin/batch-import.html"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/api/admin/session/login"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/api/admin/session/logout"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/api/admin/session"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal/admin-common.css"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal/admin-common.js"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "data-admin-nav"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/api/logical-groups"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "logical_group"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "shadow_group_id"
|
||||
@@ -139,37 +151,23 @@ assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "package_tier"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "purchase_cta_label"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "purchase_cta_url"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "首版页面只覆盖新增与查看"
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" 'credentials: "include"'
|
||||
assert_contains_file "$ADMIN_LOGICAL_GROUPS_FILE" "/portal-admin-api"
|
||||
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "Route Health Admin"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal/admin/"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal/admin/logical-groups.html"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal/admin/route-health.html"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal/admin/accounts.html"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal/admin/providers.html"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal/admin/batch-import.html"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/api/admin/session/login"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/api/admin/session/logout"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/api/admin/session"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal/admin-common.css"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal/admin-common.js"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "data-admin-nav"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/api/routing/routes/health"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "healthy"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "cooldown"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "failing"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "disabled"
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" 'credentials: "include"'
|
||||
assert_contains_file "$ADMIN_ROUTE_HEALTH_FILE" "/portal-admin-api"
|
||||
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "Provider Accounts Admin"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal/admin/"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal/admin/logical-groups.html"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal/admin/route-health.html"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal/admin/accounts.html"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal/admin/providers.html"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal/admin/batch-import.html"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/api/admin/session/login"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/api/admin/session/logout"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/api/admin/session"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal/admin-common.css"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal/admin-common.js"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "data-admin-nav"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/api/provider-accounts"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/binding-candidates"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/binding"
|
||||
@@ -185,17 +183,13 @@ assert_contains_file "$ADMIN_ACCOUNTS_FILE" "shadow_host_id"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "provider_accounts"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "显式整理归属"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "conflict"
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" 'credentials: "include"'
|
||||
assert_contains_file "$ADMIN_ACCOUNTS_FILE" "/portal-admin-api"
|
||||
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "Provider Admin"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/portal/admin-common.css"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/portal/admin-common.js"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "data-admin-nav"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "管理员登录"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/portal/admin/logical-groups.html"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/portal/admin/route-health.html"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/portal/admin/accounts.html"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/api/admin/session/login"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/api/admin/session/logout"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/api/admin/session"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/api/packs"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/api/hosts"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/api/providers/"
|
||||
@@ -211,7 +205,6 @@ assert_contains_file "$ADMIN_PROVIDERS_FILE" "Provider Manifest 草稿"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/portal-admin-api"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "/publish"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "发布 Commit Message"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "credentials: \"include\""
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "最近成功模板"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "根据 display name / base url / models 自动生成"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "同模型已存在"
|
||||
@@ -219,13 +212,8 @@ assert_contains_file "$ADMIN_PROVIDERS_FILE" "providerIdPreview"
|
||||
assert_contains_file "$ADMIN_PROVIDERS_FILE" "modelConflicts"
|
||||
|
||||
assert_contains_file "$ADMIN_BATCH_FILE" "/portal/admin-batch-import.html"
|
||||
assert_contains_file "$ADMIN_BATCH_FILE" "Batch Import Admin Redirect"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "管理员登录"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin/route-health.html"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/portal/admin/accounts.html"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/api/admin/session/login"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/api/admin/session/logout"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "/api/admin/session"
|
||||
assert_contains_file "$ADMIN_HTML_FILE" "credentials: \"include\""
|
||||
|
||||
assert_contains_file "$NGINX_FILE" "location = /portal"
|
||||
assert_contains_file "$NGINX_FILE" "location = /portal/admin"
|
||||
|
||||
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"
|
||||
@@ -22,9 +22,31 @@ GOVET_LOG="$OUTPUT_DIR/govet.txt"
|
||||
INTEGRATION_LOG="$OUTPUT_DIR/integration.txt"
|
||||
COVERAGE_LOG="$OUTPUT_DIR/coverage.txt"
|
||||
COVERAGE_REPORT="$OUTPUT_DIR/coverage-report.md"
|
||||
PORTAL_ASSETS_LOG="$OUTPUT_DIR/portal-assets.txt"
|
||||
FRONTEND_SMOKE_LOG="$OUTPUT_DIR/frontend-smoke.txt"
|
||||
|
||||
log "quality gate output dir: $OUTPUT_DIR"
|
||||
|
||||
log "running portal asset regression"
|
||||
bash "$ROOT_DIR/scripts/test/test_tksea_portal_assets.sh" 2>&1 | tee "$PORTAL_ASSETS_LOG"
|
||||
|
||||
log "running frontend browser smoke"
|
||||
set +e
|
||||
bash "$ROOT_DIR/scripts/test/verify_frontend_smoke.sh" 2>&1 | tee "$FRONTEND_SMOKE_LOG"
|
||||
frontend_smoke_status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [[ $frontend_smoke_status -ne 0 ]]; then
|
||||
if grep -Eq 'PermissionError: \[Errno 1\] Operation not permitted|frontend smoke server did not start' "$FRONTEND_SMOKE_LOG"; then
|
||||
if [[ "${ALLOW_BLOCKED_FRONTEND_SMOKE:-0}" == "1" ]]; then
|
||||
log "frontend smoke blocked by socket-restricted environment; continuing because ALLOW_BLOCKED_FRONTEND_SMOKE=1"
|
||||
else
|
||||
fail "frontend smoke blocked by current environment socket restrictions; rerun in an unrestricted environment or set ALLOW_BLOCKED_FRONTEND_SMOKE=1 for local triage"
|
||||
fi
|
||||
else
|
||||
fail "frontend browser smoke failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "running gofmt check"
|
||||
gofmt -l . | tee "$GOFMT_LOG"
|
||||
if [[ -s "$GOFMT_LOG" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user