#!/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" <"$WORK_DIR/create-scum-alpha.request.json" <"$WORK_DIR/create-scum-beta.request.json" <"$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" <