first commit
This commit is contained in:
Executable
+383
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=scripts/local-debug-env.sh
|
||||
source "$ROOT_DIR/scripts/local-debug-env.sh"
|
||||
|
||||
PLATFORM_URL="$(local_debug_platform_url)"
|
||||
API_URL="$PLATFORM_URL/api/v1"
|
||||
WORK_DIR="$LOCAL_DEBUG_ROOT/smoke"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
SELF_STARTED_PIDS=()
|
||||
|
||||
cleanup_self_started() {
|
||||
local pid
|
||||
for pid in "${SELF_STARTED_PIDS[@]:-}"; do
|
||||
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_url() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local attempts="${3:-30}"
|
||||
printf 'waiting for %s at %s\n' "$name" "$url"
|
||||
for _ in $(seq 1 "$attempts"); do
|
||||
if curl -fsS "$url" >/dev/null 2>&1; then
|
||||
printf '%s is ready\n' "$name"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
printf '%s did not become ready at %s\n' "$name" "$url" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
start_self_hosted_stack() {
|
||||
mkdir -p "$LOCAL_DEBUG_LOG_DIR" "$LOCAL_DEBUG_PID_DIR" "$PLATFORM_DATA_DIR" "$PLATFORM_LOG_DIR" "$RUN_WORKSPACE_ROOT" "$RUN_SPOOL_ROOT" "$GOCACHE"
|
||||
trap cleanup_self_started EXIT
|
||||
|
||||
printf 'self-starting platform for local debug smoke\n'
|
||||
(
|
||||
cd "$ROOT_DIR/platform"
|
||||
exec env \
|
||||
GOCACHE="$GOCACHE" \
|
||||
PLATFORM_ADDR="$PLATFORM_ADDR" \
|
||||
PLATFORM_STORAGE_BACKEND="$PLATFORM_STORAGE_BACKEND" \
|
||||
PLATFORM_DATA_DIR="$PLATFORM_DATA_DIR" \
|
||||
PLATFORM_METADATA_PATH="$PLATFORM_METADATA_PATH" \
|
||||
PLATFORM_LOG_BODY_BACKEND="$PLATFORM_LOG_BODY_BACKEND" \
|
||||
PLATFORM_LOG_DIR="$PLATFORM_LOG_DIR" \
|
||||
go run ./cmd/platform
|
||||
) >"$LOCAL_DEBUG_LOG_DIR/platform.log" 2>&1 &
|
||||
SELF_STARTED_PIDS+=("$!")
|
||||
printf '%s' "$!" >"$LOCAL_DEBUG_PID_DIR/platform.pid"
|
||||
wait_for_url platform "$PLATFORM_URL/healthz" 45
|
||||
|
||||
printf 'self-starting run worker for local debug smoke\n'
|
||||
(
|
||||
cd "$ROOT_DIR/run"
|
||||
exec env \
|
||||
GOCACHE="$GOCACHE" \
|
||||
RUN_MODE="$RUN_MODE" \
|
||||
RUN_PLATFORM_URL="$RUN_PLATFORM_URL" \
|
||||
RUN_ENDPOINT_ID="$RUN_ENDPOINT_ID" \
|
||||
RUN_DISPLAY_NAME="$RUN_DISPLAY_NAME" \
|
||||
RUN_VERSION="$RUN_VERSION" \
|
||||
RUN_REGISTRATION_TOKEN="$RUN_REGISTRATION_TOKEN" \
|
||||
RUN_WORKSPACE_ROOT="$RUN_WORKSPACE_ROOT" \
|
||||
RUN_SPOOL_ROOT="$RUN_SPOOL_ROOT" \
|
||||
RUN_MAX_JOBS="$RUN_MAX_JOBS" \
|
||||
RUN_HEARTBEAT_INTERVAL_MS="$RUN_HEARTBEAT_INTERVAL_MS" \
|
||||
RUN_POLL_INTERVAL_MS="$RUN_POLL_INTERVAL_MS" \
|
||||
RUN_RETRY_BACKOFF_MS="$RUN_RETRY_BACKOFF_MS" \
|
||||
go run ./cmd/run
|
||||
) >"$LOCAL_DEBUG_LOG_DIR/run.log" 2>&1 &
|
||||
SELF_STARTED_PIDS+=("$!")
|
||||
printf '%s' "$!" >"$LOCAL_DEBUG_PID_DIR/run.pid"
|
||||
|
||||
printf 'self-starting platform_web for local debug smoke\n'
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
exec env \
|
||||
PLATFORM_API_PROXY="$PLATFORM_API_PROXY" \
|
||||
VITE_PLATFORM_API_BASE_URL="$VITE_PLATFORM_API_BASE_URL" \
|
||||
VITE_ENABLE_LOCAL_AUTH_FALLBACK="$VITE_ENABLE_LOCAL_AUTH_FALLBACK" \
|
||||
npm --prefix platform_web run dev -- --port "$LOCAL_DEBUG_WEB_PORT"
|
||||
) >"$LOCAL_DEBUG_LOG_DIR/platform_web.log" 2>&1 &
|
||||
SELF_STARTED_PIDS+=("$!")
|
||||
printf '%s' "$!" >"$LOCAL_DEBUG_PID_DIR/platform_web.pid"
|
||||
wait_for_url platform_web "$(local_debug_web_url)" 45
|
||||
}
|
||||
|
||||
if [[ "${LOCAL_DEBUG_SELF_START:-false}" == "true" ]]; then
|
||||
start_self_hosted_stack
|
||||
fi
|
||||
|
||||
json_post() {
|
||||
local url="$1"
|
||||
local body_file="$2"
|
||||
local output_file="$3"
|
||||
local status
|
||||
status="$(curl -sS -H 'Content-Type: application/json' --data-binary "@$body_file" "$url" -o "$output_file" -w '%{http_code}')"
|
||||
case "$status" in
|
||||
2*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
printf 'POST %s failed with HTTP %s\n' "$url" "$status" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
json_get() {
|
||||
local url="$1"
|
||||
local output_file="$2"
|
||||
shift 2
|
||||
curl -fsS "$@" "$url" >"$output_file"
|
||||
}
|
||||
|
||||
require_file_contains() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
if ! grep -Eq "$pattern" "$file"; then
|
||||
printf 'expected %s to contain pattern: %s\n' "$file" "$pattern" >&2
|
||||
sed -n '1,120p' "$file" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
reject_forbidden_fragments() {
|
||||
local file="$1"
|
||||
local pattern
|
||||
pattern="$(local_debug_forbidden_pattern)"
|
||||
if grep -Eq "$pattern" "$file"; then
|
||||
printf 'forbidden local-debug fragment found in %s\n' "$file" >&2
|
||||
grep -En "$pattern" "$file" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
session_token_from_login() {
|
||||
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (!data.sessionId) process.exit(2); process.stdout.write(data.sessionId);' "$1"
|
||||
}
|
||||
|
||||
json_id() {
|
||||
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const id=data.instance?.id || data.serverInstance?.id || data.id || ((data.items||[])[0]||{}).id; if (!id) process.exit(2); process.stdout.write(id);' "$1"
|
||||
}
|
||||
|
||||
printf 'checking platform health at %s\n' "$PLATFORM_URL"
|
||||
json_get "$PLATFORM_URL/healthz" "$WORK_DIR/health.json"
|
||||
require_file_contains "$WORK_DIR/health.json" '"status"[[:space:]]*:[[:space:]]*"ok"'
|
||||
|
||||
cat >"$WORK_DIR/login.request.json" <<'JSON'
|
||||
{"account":"operator.local@example.test","password":"operator-local"}
|
||||
JSON
|
||||
json_post "$API_URL/auth/login" "$WORK_DIR/login.request.json" "$WORK_DIR/login.response.json"
|
||||
SESSION_ID="$(session_token_from_login "$WORK_DIR/login.response.json")"
|
||||
AUTH_HEADER=(-H "Authorization: Bearer $SESSION_ID")
|
||||
require_file_contains "$WORK_DIR/login.response.json" '"status"[[:space:]]*:[[:space:]]*"active"'
|
||||
|
||||
printf 'validating plugin manifests\n'
|
||||
(cd "$ROOT_DIR/plugins" && npm run validate:manifest)
|
||||
|
||||
node - "$ROOT_DIR/plugins/examples/dev-game-plugin/manifest.json" "$WORK_DIR/register-plugin.request.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const manifestPath = process.argv[2];
|
||||
const outputPath = process.argv[3];
|
||||
const source = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const manifest = {
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
description: "Development plugin",
|
||||
version: source.version,
|
||||
kind: "game-plugin",
|
||||
tags: ["example", "development"],
|
||||
server: {
|
||||
type: source.server.type,
|
||||
displayName: source.server.displayName,
|
||||
supportedOs: ["linux", "darwin"],
|
||||
createFormSchema: source.server.createFormSchema
|
||||
},
|
||||
capabilities: ["process.install", "process.start", "process.stop"],
|
||||
permissions: ["server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
|
||||
actions: {
|
||||
install: source.actions.install,
|
||||
start: source.actions.start,
|
||||
stop: source.actions.stop,
|
||||
restart: source.actions.restart
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
key: "logs",
|
||||
title: "Logs",
|
||||
path: "/logs",
|
||||
permissions: ["server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
|
||||
bridgeActions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"]
|
||||
}
|
||||
],
|
||||
bridge: { actions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"] },
|
||||
ai: { purposes: [] }
|
||||
};
|
||||
fs.writeFileSync(outputPath, JSON.stringify({
|
||||
manifestRef: "artifact://manifests/game.example/0.1.0",
|
||||
manifest
|
||||
}, null, 2));
|
||||
NODE
|
||||
|
||||
printf 'registering dev plugin through platform API\n'
|
||||
if ! json_post "$API_URL/game-plugins/register-manifest" "$WORK_DIR/register-plugin.request.json" "$WORK_DIR/register-plugin.response.json"; then
|
||||
json_get "$API_URL/game-plugins/game.example" "$WORK_DIR/register-plugin.response.json"
|
||||
fi
|
||||
reject_forbidden_fragments "$WORK_DIR/register-plugin.response.json"
|
||||
|
||||
node - "$ROOT_DIR/plugins/examples/scum-server-plugin/manifest.json" "$WORK_DIR/register-scum-plugin.request.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const manifestPath = process.argv[2];
|
||||
const outputPath = process.argv[3];
|
||||
const source = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const manifest = {
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
description: "SCUM local proof plugin",
|
||||
version: source.version,
|
||||
kind: "game-plugin",
|
||||
tags: source.tags,
|
||||
server: {
|
||||
type: source.server.type,
|
||||
displayName: source.server.displayName,
|
||||
supportedOs: ["linux"],
|
||||
createFormSchema: source.server.createFormSchema
|
||||
},
|
||||
capabilities: ["process.install", "process.start", "process.stop"],
|
||||
permissions: ["server.create", "server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
|
||||
actions: {
|
||||
install: source.actions.install,
|
||||
start: source.actions.start,
|
||||
stop: source.actions.stop,
|
||||
restart: source.actions.restart
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
key: "overview",
|
||||
title: "SCUM Overview",
|
||||
path: "/overview",
|
||||
permissions: ["server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
|
||||
bridgeActions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"]
|
||||
},
|
||||
{
|
||||
key: "logs",
|
||||
title: "SCUM Logs",
|
||||
path: "/logs",
|
||||
permissions: ["server.read", "server.lifecycle", "server.logs.read", "server.artifacts.read"],
|
||||
bridgeActions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"]
|
||||
}
|
||||
],
|
||||
bridge: { actions: ["server.instances.read", "jobs.dispatch", "logs.query", "artifacts.open"] },
|
||||
ai: { purposes: [] }
|
||||
};
|
||||
fs.writeFileSync(outputPath, JSON.stringify({
|
||||
manifestRef: "artifact://manifests/game.scum/0.1.0",
|
||||
manifest
|
||||
}, null, 2));
|
||||
NODE
|
||||
|
||||
printf 'registering SCUM plugin through platform API\n'
|
||||
if ! json_post "$API_URL/game-plugins/register-manifest" "$WORK_DIR/register-scum-plugin.request.json" "$WORK_DIR/register-scum-plugin.response.json"; then
|
||||
json_get "$API_URL/game-plugins/game.scum" "$WORK_DIR/register-scum-plugin.response.json"
|
||||
fi
|
||||
reject_forbidden_fragments "$WORK_DIR/register-scum-plugin.response.json"
|
||||
|
||||
printf 'waiting for run endpoint heartbeat\n'
|
||||
for _ in $(seq 1 20); do
|
||||
if json_get "$API_URL/run/endpoints?status=online" "$WORK_DIR/run-endpoints.response.json" && grep -q "$RUN_ENDPOINT_ID" "$WORK_DIR/run-endpoints.response.json"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
require_file_contains "$WORK_DIR/run-endpoints.response.json" "$RUN_ENDPOINT_ID"
|
||||
reject_forbidden_fragments "$WORK_DIR/run-endpoints.response.json"
|
||||
|
||||
cat >"$WORK_DIR/create-server.request.json" <<JSON
|
||||
{
|
||||
"id": "server-local-debug",
|
||||
"pluginId": "game.example",
|
||||
"runEndpointId": "$RUN_ENDPOINT_ID",
|
||||
"name": "Local Debug Example Server",
|
||||
"idempotencyKey": "local-debug-create"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat >"$WORK_DIR/create-scum-alpha.request.json" <<JSON
|
||||
{
|
||||
"id": "scum-alpha",
|
||||
"pluginId": "game.scum",
|
||||
"runEndpointId": "$RUN_ENDPOINT_ID",
|
||||
"name": "SCUM Alpha",
|
||||
"idempotencyKey": "local-debug-scum-alpha-create"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat >"$WORK_DIR/create-scum-beta.request.json" <<JSON
|
||||
{
|
||||
"id": "scum-beta",
|
||||
"pluginId": "game.scum",
|
||||
"runEndpointId": "$RUN_ENDPOINT_ID",
|
||||
"name": "SCUM Beta",
|
||||
"idempotencyKey": "local-debug-scum-beta-create"
|
||||
}
|
||||
JSON
|
||||
|
||||
printf 'creating server lifecycle workflow through platform API\n'
|
||||
if ! curl -fsS -H 'Content-Type: application/json' "${AUTH_HEADER[@]}" --data-binary "@$WORK_DIR/create-server.request.json" "$API_URL/server-instances/workflows/create" >"$WORK_DIR/create-server.response.json"; then
|
||||
json_get "$API_URL/server-instances/server-local-debug" "$WORK_DIR/create-server.response.json" "${AUTH_HEADER[@]}"
|
||||
fi
|
||||
reject_forbidden_fragments "$WORK_DIR/create-server.response.json"
|
||||
|
||||
printf 'creating SCUM server lifecycle workflows through platform API\n'
|
||||
if ! curl -fsS -H 'Content-Type: application/json' "${AUTH_HEADER[@]}" --data-binary "@$WORK_DIR/create-scum-alpha.request.json" "$API_URL/server-instances/workflows/create" >"$WORK_DIR/create-scum-alpha.response.json"; then
|
||||
json_get "$API_URL/server-instances/scum-alpha" "$WORK_DIR/create-scum-alpha.response.json" "${AUTH_HEADER[@]}"
|
||||
fi
|
||||
if ! curl -fsS -H 'Content-Type: application/json' "${AUTH_HEADER[@]}" --data-binary "@$WORK_DIR/create-scum-beta.request.json" "$API_URL/server-instances/workflows/create" >"$WORK_DIR/create-scum-beta.response.json"; then
|
||||
json_get "$API_URL/server-instances/scum-beta" "$WORK_DIR/create-scum-beta.response.json" "${AUTH_HEADER[@]}"
|
||||
fi
|
||||
reject_forbidden_fragments "$WORK_DIR/create-scum-alpha.response.json"
|
||||
reject_forbidden_fragments "$WORK_DIR/create-scum-beta.response.json"
|
||||
require_file_contains "$WORK_DIR/create-scum-alpha.response.json" '"pluginId"[[:space:]]*:[[:space:]]*"game.scum"'
|
||||
require_file_contains "$WORK_DIR/create-scum-beta.response.json" '"pluginId"[[:space:]]*:[[:space:]]*"game.scum"'
|
||||
|
||||
SERVER_ID="$(json_id "$WORK_DIR/create-server.response.json")"
|
||||
SCUM_ALPHA_ID="$(json_id "$WORK_DIR/create-scum-alpha.response.json")"
|
||||
SCUM_BETA_ID="$(json_id "$WORK_DIR/create-scum-beta.response.json")"
|
||||
|
||||
printf 'checking jobs, logs, artifacts, and marketplace refs\n'
|
||||
json_get "$API_URL/server-instances" "$WORK_DIR/server-instances.response.json" "${AUTH_HEADER[@]}"
|
||||
json_get "$API_URL/jobs?serverInstanceId=$SERVER_ID" "$WORK_DIR/jobs.response.json" "${AUTH_HEADER[@]}"
|
||||
json_get "$API_URL/jobs?serverInstanceId=$SCUM_ALPHA_ID" "$WORK_DIR/scum-alpha-jobs.response.json" "${AUTH_HEADER[@]}"
|
||||
json_get "$API_URL/jobs?serverInstanceId=$SCUM_BETA_ID" "$WORK_DIR/scum-beta-jobs.response.json" "${AUTH_HEADER[@]}"
|
||||
json_get "$API_URL/log-streams" "$WORK_DIR/log-streams.response.json" "${AUTH_HEADER[@]}"
|
||||
json_get "$API_URL/artifacts" "$WORK_DIR/artifacts.response.json" "${AUTH_HEADER[@]}"
|
||||
json_get "$API_URL/plugin-marketplace/plugins" "$WORK_DIR/marketplace.response.json" "${AUTH_HEADER[@]}"
|
||||
json_get "$API_URL/plugin-marketplace/plugins?serverType=scum&keyword=scum" "$WORK_DIR/scum-marketplace.response.json" "${AUTH_HEADER[@]}"
|
||||
require_file_contains "$WORK_DIR/scum-marketplace.response.json" '"id"[[:space:]]*:[[:space:]]*"game.scum"'
|
||||
require_file_contains "$WORK_DIR/scum-alpha-jobs.response.json" '"serverInstanceId"[[:space:]]*:[[:space:]]*"scum-alpha"'
|
||||
require_file_contains "$WORK_DIR/scum-beta-jobs.response.json" '"serverInstanceId"[[:space:]]*:[[:space:]]*"scum-beta"'
|
||||
|
||||
for file in "$WORK_DIR"/server-instances.response.json "$WORK_DIR"/jobs.response.json "$WORK_DIR"/scum-alpha-jobs.response.json "$WORK_DIR"/scum-beta-jobs.response.json "$WORK_DIR"/log-streams.response.json "$WORK_DIR"/artifacts.response.json "$WORK_DIR"/marketplace.response.json "$WORK_DIR"/scum-marketplace.response.json; do
|
||||
reject_forbidden_fragments "$file"
|
||||
done
|
||||
|
||||
if [[ "$VITE_PLATFORM_API_BASE_URL" != "/api/v1" ]]; then
|
||||
printf 'VITE_PLATFORM_API_BASE_URL must be /api/v1, got %s\n' "$VITE_PLATFORM_API_BASE_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$PLATFORM_API_PROXY" != "$PLATFORM_URL" ]]; then
|
||||
printf 'PLATFORM_API_PROXY must be %s, got %s\n' "$PLATFORM_URL" "$PLATFORM_API_PROXY" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$VITE_ENABLE_LOCAL_AUTH_FALLBACK" != "false" ]]; then
|
||||
printf 'VITE_ENABLE_LOCAL_AUTH_FALLBACK must stay false for local-debug proof, got %s\n' "$VITE_ENABLE_LOCAL_AUTH_FALLBACK" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat >"$WORK_DIR/browser-walkthrough-checklist.md" <<EOF
|
||||
# Local Debug Browser Walkthrough
|
||||
|
||||
- Open $(local_debug_web_url)
|
||||
- Login with operator.local@example.test / operator-local.
|
||||
- Confirm the login path is API-backed and no local fallback banner or fallback workspace appears.
|
||||
- Visit 首页, 服务器管理, 插件市场, 用户管理, AI 提供商管理.
|
||||
- Open server-local-debug detail and inspect lifecycle history, plugin controls, logs, and artifact references.
|
||||
- Confirm 插件市场 can find SCUM Server / game.scum, then open scum-alpha and scum-beta from 服务器管理.
|
||||
- Confirm both SCUM servers are backed by game.scum and have separate lifecycle install jobs and operation history.
|
||||
- Search visible text for forbidden fragments: /Users/, /private/, unix://, tcp://, Bearer , sk-, password=, apiKeyRef, rawApiKey, run session tokens, direct run URLs, plugin-owned transport details.
|
||||
- Acceptance requires platform routes, logical IDs, job refs, log refs, artifact refs, and safe metadata only.
|
||||
EOF
|
||||
|
||||
printf 'local debug smoke passed\n'
|
||||
printf 'evidence directory: %s\n' "$WORK_DIR"
|
||||
Reference in New Issue
Block a user