#!/usr/bin/env bash set -euo pipefail FNOS_HOST="${FNOS_HOST:-192.168.31.188}" WEB_PORT="${STORYFORGE_WEB_V4_DEV_PORT:-19192}" COLLECTOR_PORT="${STORYFORGE_COLLECTOR_DEV_PORT:-19193}" CUTVIDEO_FORWARD_PORT="${CUTVIDEO_FORWARD_PORT:-19186}" STORYFORGE_COMPAT_FORWARD_PORT="${STORYFORGE_COMPAT_FORWARD_PORT:-19181}" WEB_URL="${STORYFORGE_FNOS_WEB_URL:-http://$FNOS_HOST:$WEB_PORT}" BACKEND_URL="${STORYFORGE_FNOS_COLLECTOR_URL:-http://$FNOS_HOST:$COLLECTOR_PORT}" CUTVIDEO_URL="${CUTVIDEO_BASE_URL:-http://$FNOS_HOST:$CUTVIDEO_FORWARD_PORT}" COMPAT_URL="${STORYFORGE_COMPAT_BASE_URL:-http://$FNOS_HOST:$STORYFORGE_COMPAT_FORWARD_PORT}" LIVE_RECORDER_URL="${LIVE_RECORDER_BASE_URL:-http://$FNOS_HOST:19106}" CURL_MAX_TIME="${STORYFORGE_FNOS_CURL_MAX_TIME:-60}" need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "missing required command: $1" >&2 exit 1 fi } need_cmd curl need_cmd python3 need_cmd rg curl_fetch() { curl -fsS --max-time "$CURL_MAX_TIME" "$@" } tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT index_file="$tmp_dir/index.html" runtime_file="$tmp_dir/runtime.js" health_file="$tmp_dir/health.json" session_file="$tmp_dir/session.json" projects_file="$tmp_dir/projects.json" action_registry_file="$tmp_dir/action-registry.json" integrations_file="$tmp_dir/integrations.json" bootstrap_file="$tmp_dir/bootstrap.json" compat_file="$tmp_dir/compat.html" live_recorder_health_file="$tmp_dir/live-recorder-health.json" token_file="$tmp_dir/token.txt" project_id_file="$tmp_dir/project-id.txt" echo "[1/7] check fnOS web" curl_fetch "$WEB_URL/" >"$index_file" rg -Fq "StoryForge" "$index_file" echo "web ok" echo "[2/7] check runtime config" curl_fetch "$WEB_URL/assets/storyforge-runtime-config.js" >"$runtime_file" rg -Fq "$BACKEND_URL" "$runtime_file" echo "runtime config ok" echo "[3/7] check collector healthz" curl_fetch "$BACKEND_URL/healthz" >"$health_file" python3 -c ' import json, pathlib, sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text()) expected_cutvideo = sys.argv[2] if str(payload.get("status") or "").lower() != "ok": raise SystemExit(f"unexpected health status: {payload.get('status')!r}") lan_routing = payload.get("lanRouting") or {} if not isinstance(lan_routing, dict): raise SystemExit("lanRouting missing") if lan_routing.get("cutvideoBaseUrl") != expected_cutvideo: raise SystemExit(f"unexpected cutvideoBaseUrl: {lan_routing.get('cutvideoBaseUrl')!r}") if lan_routing.get("cutvideoRouteMode") != "fnos_tunnel": raise SystemExit(f"unexpected cutvideoRouteMode: {lan_routing.get('cutvideoRouteMode')!r}") print("healthz ok") ' "$health_file" "$CUTVIDEO_URL" echo "[4/7] check auto-session" curl_fetch -X POST "$BACKEND_URL/v2/auth/auto-session" \ -H 'content-type: application/json' \ -d '{}' >"$session_file" python3 -c ' import json, pathlib, sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text()) token = str(payload.get("token") or "") mode = str(payload.get("mode") or "") username = str((payload.get("account") or {}).get("username") or "") if not token: raise SystemExit("auto-session did not return token") if mode != "auto": raise SystemExit(f"unexpected mode: {mode!r}") if not username: raise SystemExit("auto-session returned empty username") pathlib.Path(sys.argv[2]).write_text(token, encoding="utf-8") print(f"auto-session ok: {username}") ' "$session_file" "$token_file" token="$(cat "$token_file")" echo "[5/7] check live project context and action registry" curl_fetch "$BACKEND_URL/v2/projects" \ -H "Authorization: Bearer $token" >"$projects_file" python3 -c ' import json, pathlib, sys projects = json.loads(pathlib.Path(sys.argv[1]).read_text()) if not isinstance(projects, list) or not projects: raise SystemExit("projects endpoint returned no project") project_id = str(projects[0].get("id") or "") if not project_id: raise SystemExit("project id missing") pathlib.Path(sys.argv[2]).write_text(project_id, encoding="utf-8") print(f"project ok: {project_id}") ' "$projects_file" "$project_id_file" project_id="$(cat "$project_id_file")" curl_fetch "$BACKEND_URL/v2/oneliner/action-registry?project_id=$project_id" \ -H "Authorization: Bearer $token" >"$action_registry_file" python3 -c ' import json, pathlib, sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text()) items = payload.get("items") or [] keys = {str(item.get("action_key") or "") for item in items} required = {"import-homepage", "search-similar-accounts", "save-benchmark-link", "refresh-tracking", "mark-tracking-read"} required = required | {"track-account", "import-video-link", "import-text", "generate-copy", "create-assistant"} missing = sorted(required - keys) if missing: raise SystemExit(f"action registry missing: {missing}") print("action registry ok") ' "$action_registry_file" echo "[6/7] check integrations health" curl_fetch "$BACKEND_URL/v2/integrations/health" \ -H "Authorization: Bearer $token" >"$integrations_file" python3 -c ' import json, pathlib, sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text()) expected_cutvideo = sys.argv[2] cutvideo = payload.get("cutvideo") or {} if cutvideo.get("base_url") != expected_cutvideo: raise SystemExit(f"unexpected cutvideo base_url: {cutvideo.get('base_url')!r}") if not cutvideo.get("reachable"): raise SystemExit("cutvideo is not reachable") if not cutvideo.get("supports_uploads"): raise SystemExit("cutvideo uploads are not available") print("integrations ok") ' "$integrations_file" "$CUTVIDEO_URL" echo "[7/8] check fnOS tunnel endpoints" curl_fetch "$CUTVIDEO_URL/api/bootstrap" >"$bootstrap_file" curl_fetch "$COMPAT_URL/" >"$compat_file" python3 -c ' import json, pathlib, sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text()) if not payload: raise SystemExit("empty cutvideo bootstrap payload") print("cutvideo bootstrap ok") ' "$bootstrap_file" if ! rg -Fq "数字人网页业务台" "$compat_file" && ! rg -Fq "BUSINESS CONSOLE" "$compat_file"; then echo "compat page does not look like the expected business console" >&2 exit 1 fi echo "compat ok" echo "[8/8] check live recorder health" curl_fetch "$LIVE_RECORDER_URL/api/healthz" >"$live_recorder_health_file" python3 -c ' import json, pathlib, sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text()) if payload.get("ok") is not True: raise SystemExit(f"unexpected live recorder health payload: {payload!r}") print("live recorder ok") ' "$live_recorder_health_file" echo "fnOS lan smoke passed:" echo " web: $WEB_URL/" echo " collector: $BACKEND_URL" echo " cutvideo: $CUTVIDEO_URL" echo " compat: $COMPAT_URL" echo " live_recorder: $LIVE_RECORDER_URL"