test: harden parallels macos dashboard smoke

This commit is contained in:
Peter Steinberger 2026-03-23 16:13:20 -07:00
parent a921b5bdff
commit ccfeecb688
No known key found for this signature in database
2 changed files with 107 additions and 3 deletions

View File

@ -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.

View File

@ -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 <<EOF
set -eu
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin:\${PATH:-}"
if [ -z "\${HOME:-}" ]; then export HOME="/Users/\$(id -un)"; fi
cd "\$HOME"
dashboard_url=$dashboard_url_q
dashboard_http_url=$dashboard_http_url_q
dashboard_port=\$(printf '%s\n' "\$dashboard_http_url" | sed -E 's#^https?://[^:/]+:([0-9]+).*\$#\1#')
if [ -z "\$dashboard_port" ] || [ "\$dashboard_port" = "\$dashboard_http_url" ]; then
echo "failed to parse dashboard port from \$dashboard_http_url" >&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 '<title>OpenClaw Control</title>' /tmp/openclaw-dashboard-smoke.html >/dev/null; then
if grep -F '<openclaw-app></openclaw-app>' /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 '<title>OpenClaw Control</title>' /tmp/openclaw-dashboard-smoke.html >/dev/null
grep -F '<openclaw-app></openclaw-app>' /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
)"