From ccfeecb6887cd97937e33a71877ad512741e82b2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Mar 2026 16:13:20 -0700 Subject: [PATCH] test: harden parallels macos dashboard smoke --- .../skills/openclaw-parallels-smoke/SKILL.md | 1 + scripts/e2e/parallels-macos-smoke.sh | 109 +++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/.agents/skills/openclaw-parallels-smoke/SKILL.md b/.agents/skills/openclaw-parallels-smoke/SKILL.md index c182f607533..2a597cf012d 100644 --- a/.agents/skills/openclaw-parallels-smoke/SKILL.md +++ b/.agents/skills/openclaw-parallels-smoke/SKILL.md @@ -32,6 +32,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo - Preferred entrypoint: `pnpm test:parallels:macos` - Default to the snapshot closest to `macOS 26.3.1 latest`. - On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.3.1 latest'` before blaming auth or the harness. +- The macOS smoke should include a dashboard load phase after gateway health: resolve the tokenized URL with `openclaw dashboard --no-open`, verify the served HTML contains the Control UI title/root shell, then open Safari and require an established localhost TCP connection from Safari to the gateway port. - `prlctl exec` is fine for deterministic repo commands, but use the guest Terminal or `prlctl enter` when installer parity or shell-sensitive behavior matters. - Multi-word `openclaw agent --message ...` checks should go through a guest shell wrapper (`guest_current_user_sh` / `guest_current_user_cli` or `/bin/sh -lc ...`), not raw `prlctl exec ... node openclaw.mjs ...`, or the message can be split into extra argv tokens and Commander reports `too many arguments for 'agent'`. - On the fresh Tahoe snapshot, `brew` exists but `node` may be missing from PATH in noninteractive exec. Use `/opt/homebrew/bin/node` when needed. diff --git a/scripts/e2e/parallels-macos-smoke.sh b/scripts/e2e/parallels-macos-smoke.sh index c78350faef1..3f2889a6b42 100644 --- a/scripts/e2e/parallels-macos-smoke.sh +++ b/scripts/e2e/parallels-macos-smoke.sh @@ -31,6 +31,7 @@ GUEST_NPM_BIN="/opt/homebrew/bin/npm" MAIN_TGZ_DIR="$(mktemp -d)" MAIN_TGZ_PATH="" +PACKED_MAIN_COMMIT_SHORT="" SERVER_PID="" RUN_DIR="$(mktemp -d /tmp/openclaw-parallels-smoke.XXXXXX)" BUILD_LOCK_DIR="${TMPDIR:-/tmp}/openclaw-parallels-build.lock" @@ -41,6 +42,7 @@ TIMEOUT_ONBOARD_S=180 TIMEOUT_GATEWAY_S=60 TIMEOUT_AGENT_S=120 TIMEOUT_PERMISSION_S=60 +TIMEOUT_DASHBOARD_S=60 TIMEOUT_SNAPSHOT_S=180 TIMEOUT_DISCORD_S=180 @@ -51,6 +53,8 @@ FRESH_GATEWAY_STATUS="skip" UPGRADE_GATEWAY_STATUS="skip" FRESH_AGENT_STATUS="skip" UPGRADE_AGENT_STATUS="skip" +FRESH_DASHBOARD_STATUS="skip" +UPGRADE_DASHBOARD_STATUS="skip" FRESH_DISCORD_STATUS="skip" UPGRADE_DISCORD_STATUS="skip" @@ -562,8 +566,12 @@ extract_package_version_from_tgz() { tar -xOf "$1" package/package.json | python3 -c 'import json, sys; print(json.load(sys.stdin)["version"])' } +extract_package_build_commit_from_tgz() { + tar -xOf "$1" package/dist/build-info.json | python3 -c 'import json, sys; print(json.load(sys.stdin).get("commit", ""))' +} + pack_main_tgz() { - local short_head pkg + local short_head pkg packed_commit if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then say "Pack target package tgz: $TARGET_PACKAGE_SPEC" pkg="$( @@ -578,6 +586,7 @@ pack_main_tgz() { fi say "Pack current main tgz" ensure_current_build + stage_pack_runtime_deps short_head="$(git rev-parse --short HEAD)" pkg="$( npm pack --ignore-scripts --json --pack-destination "$MAIN_TGZ_DIR" \ @@ -585,6 +594,9 @@ pack_main_tgz() { )" MAIN_TGZ_PATH="$MAIN_TGZ_DIR/openclaw-main-$short_head.tgz" cp "$MAIN_TGZ_DIR/$pkg" "$MAIN_TGZ_PATH" + packed_commit="$(extract_package_build_commit_from_tgz "$MAIN_TGZ_PATH")" + [[ -n "$packed_commit" ]] || die "failed to read packed build commit from $MAIN_TGZ_PATH" + PACKED_MAIN_COMMIT_SHORT="${packed_commit:0:7}" say "Packed $MAIN_TGZ_PATH" tar -xOf "$MAIN_TGZ_PATH" package/dist/build-info.json } @@ -594,7 +606,8 @@ verify_target_version() { verify_version_contains "$TARGET_EXPECT_VERSION" return fi - verify_version_contains "$(git rev-parse --short=7 HEAD)" + [[ -n "$PACKED_MAIN_COMMIT_SHORT" ]] || die "packed main commit not captured" + verify_version_contains "$PACKED_MAIN_COMMIT_SHORT" } current_build_commit() { @@ -610,6 +623,10 @@ else: PY } +current_control_ui_ready() { + [[ -f "dist/control-ui/index.html" ]] +} + acquire_build_lock() { local owner_pid="" while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do @@ -637,15 +654,22 @@ ensure_current_build() { acquire_build_lock head="$(git rev-parse HEAD)" build_commit="$(current_build_commit)" - if [[ "$build_commit" == "$head" ]]; then + if [[ "$build_commit" == "$head" ]] && current_control_ui_ready; then release_build_lock return fi say "Build dist for current head" pnpm build + say "Build Control UI for current head" + pnpm ui:build build_commit="$(current_build_commit)" release_build_lock [[ "$build_commit" == "$head" ]] || die "dist/build-info.json still does not match HEAD after build" + current_control_ui_ready || die "dist/control-ui/index.html missing after ui build" +} + +stage_pack_runtime_deps() { + node scripts/stage-bundled-plugin-runtime-deps.mjs } start_server() { @@ -719,6 +743,77 @@ verify_turn() { --json } +resolve_dashboard_url() { + local dashboard_url + dashboard_url="$( + guest_current_user_cli "$GUEST_OPENCLAW_BIN" dashboard --no-open \ + | awk '/^Dashboard URL: / { sub(/^Dashboard URL: /, ""); print; exit }' + )" + dashboard_url="${dashboard_url//$'\r'/}" + dashboard_url="${dashboard_url//$'\n'/}" + [[ -n "$dashboard_url" ]] || { + echo "failed to resolve dashboard URL from openclaw dashboard --no-open" >&2 + return 1 + } + printf '%s\n' "$dashboard_url" +} + +verify_dashboard_load() { + local dashboard_url dashboard_http_url dashboard_url_q dashboard_http_url_q cmd + dashboard_url="$(resolve_dashboard_url)" + dashboard_http_url="${dashboard_url%%#*}" + dashboard_url_q="$(shell_quote "$dashboard_url")" + dashboard_http_url_q="$(shell_quote "$dashboard_http_url")" + cmd="$(cat <&2 + exit 1 +fi +deadline=\$((SECONDS + 30)) +dashboard_ready=0 +while [ \$SECONDS -lt \$deadline ]; do + if curl -fsSL "\$dashboard_http_url" >/tmp/openclaw-dashboard-smoke.html 2>/dev/null; then + if grep -F 'OpenClaw Control' /tmp/openclaw-dashboard-smoke.html >/dev/null; then + if grep -F '' /tmp/openclaw-dashboard-smoke.html >/dev/null; then + dashboard_ready=1 + break + fi + fi + fi + sleep 1 +done +[ "\$dashboard_ready" = "1" ] || { + echo "dashboard HTML did not become ready at \$dashboard_http_url" >&2 + exit 1 +} +grep -F 'OpenClaw Control' /tmp/openclaw-dashboard-smoke.html >/dev/null +grep -F '' /tmp/openclaw-dashboard-smoke.html >/dev/null +pkill -x Safari >/dev/null 2>&1 || true +open -a Safari "\$dashboard_url" +deadline=\$((SECONDS + 20)) +while [ \$SECONDS -lt \$deadline ]; do + if pgrep -x Safari >/dev/null 2>&1; then + if lsof -nPiTCP:"\$dashboard_port" -sTCP:ESTABLISHED 2>/dev/null \ + | awk 'NR > 1 && \$1 != "node" { found = 1 } END { exit found ? 0 : 1 }'; then + exit 0 + fi + fi + sleep 1 +done +echo "Safari did not establish a dashboard client connection on port \$dashboard_port" >&2 +exit 1 +EOF +)" + guest_current_user_exec /bin/sh -lc "$cmd" +} + configure_discord_smoke() { local guilds_json script guilds_json="$( @@ -996,6 +1091,7 @@ summary = { "version": os.environ["SUMMARY_FRESH_MAIN_VERSION"], "gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"], "agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"], + "dashboard": os.environ["SUMMARY_FRESH_DASHBOARD_STATUS"], "discord": os.environ["SUMMARY_FRESH_DISCORD_STATUS"], }, "upgrade": { @@ -1005,6 +1101,7 @@ summary = { "mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"], "gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"], "agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"], + "dashboard": os.environ["SUMMARY_UPGRADE_DASHBOARD_STATUS"], "discord": os.environ["SUMMARY_UPGRADE_DISCORD_STATUS"], }, } @@ -1041,6 +1138,8 @@ run_fresh_main_lane() { phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard phase_run "fresh.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway FRESH_GATEWAY_STATUS="pass" + phase_run "fresh.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load + FRESH_DASHBOARD_STATUS="pass" phase_run "fresh.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn FRESH_AGENT_STATUS="pass" if discord_smoke_enabled; then @@ -1074,6 +1173,8 @@ run_upgrade_lane() { phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard phase_run "upgrade.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway UPGRADE_GATEWAY_STATUS="pass" + phase_run "upgrade.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load + UPGRADE_DASHBOARD_STATUS="pass" phase_run "upgrade.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn UPGRADE_AGENT_STATUS="pass" if discord_smoke_enabled; then @@ -1153,6 +1254,7 @@ SUMMARY_JSON_PATH="$( SUMMARY_FRESH_MAIN_VERSION="$FRESH_MAIN_VERSION" \ SUMMARY_FRESH_GATEWAY_STATUS="$FRESH_GATEWAY_STATUS" \ SUMMARY_FRESH_AGENT_STATUS="$FRESH_AGENT_STATUS" \ + SUMMARY_FRESH_DASHBOARD_STATUS="$FRESH_DASHBOARD_STATUS" \ SUMMARY_FRESH_DISCORD_STATUS="$FRESH_DISCORD_STATUS" \ SUMMARY_UPGRADE_PRECHECK_STATUS="$UPGRADE_PRECHECK_STATUS" \ SUMMARY_UPGRADE_STATUS="$UPGRADE_STATUS" \ @@ -1160,6 +1262,7 @@ SUMMARY_JSON_PATH="$( SUMMARY_UPGRADE_MAIN_VERSION="$UPGRADE_MAIN_VERSION" \ SUMMARY_UPGRADE_GATEWAY_STATUS="$UPGRADE_GATEWAY_STATUS" \ SUMMARY_UPGRADE_AGENT_STATUS="$UPGRADE_AGENT_STATUS" \ + SUMMARY_UPGRADE_DASHBOARD_STATUS="$UPGRADE_DASHBOARD_STATUS" \ SUMMARY_UPGRADE_DISCORD_STATUS="$UPGRADE_DISCORD_STATUS" \ write_summary_json )"