diff --git a/.agents/skills/openclaw-parallels-smoke/SKILL.md b/.agents/skills/openclaw-parallels-smoke/SKILL.md index eb0fbf68a2e..ee137097423 100644 --- a/.agents/skills/openclaw-parallels-smoke/SKILL.md +++ b/.agents/skills/openclaw-parallels-smoke/SKILL.md @@ -16,6 +16,14 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo - Pass `--json` for machine-readable summaries. - Per-phase logs land under `/tmp/openclaw-parallels-*`. - Do not run local and gateway agent turns in parallel on the same fresh workspace or session. +- For `prlctl exec`, pass the VM name before `--current-user` (`prlctl exec "$VM" --current-user ...`), not the other way around. + +## npm install then update + +- Preferred entrypoint: `pnpm test:parallels:npm-update` +- Flow: fresh snapshot -> install npm package baseline -> smoke -> install current main tgz on the same guest -> smoke again. +- Same-guest update verification should set the default model explicitly to `openai/gpt-5.4` before the agent turn and use a fresh explicit `--session-id` so old session model state does not leak into the check. +- Linux same-guest update verification should also export `HOME=/root`, pass `OPENAI_API_KEY` via `prlctl exec ... /usr/bin/env`, and use `openclaw agent --local`; the fresh Linux baseline does not rely on persisted gateway credentials. ## macOS flow diff --git a/package.json b/package.json index 4de3bc47e00..4c239f8d0e0 100644 --- a/package.json +++ b/package.json @@ -724,6 +724,7 @@ "test:macmini": "OPENCLAW_TEST_PROFILE=macmini node scripts/test-parallel.mjs", "test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh", "test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh", + "test:parallels:npm-update": "bash scripts/e2e/parallels-npm-update-smoke.sh", "test:parallels:windows": "bash scripts/e2e/parallels-windows-smoke.sh", "test:perf:budget": "node scripts/test-perf-budget.mjs", "test:perf:find-thread-candidates": "node scripts/test-find-thread-candidates.mjs", diff --git a/scripts/e2e/parallels-npm-update-smoke.sh b/scripts/e2e/parallels-npm-update-smoke.sh new file mode 100755 index 00000000000..cea1c3a3983 --- /dev/null +++ b/scripts/e2e/parallels-npm-update-smoke.sh @@ -0,0 +1,373 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +MACOS_VM="macOS Tahoe" +WINDOWS_VM="Windows 11" +LINUX_VM="Ubuntu 25.10" +OPENAI_API_KEY_ENV="OPENAI_API_KEY" +PACKAGE_SPEC="" +JSON_OUTPUT=0 +RUN_DIR="$(mktemp -d /tmp/openclaw-parallels-npm-update.XXXXXX)" +MAIN_TGZ_DIR="$(mktemp -d)" +MAIN_TGZ_PATH="" +SERVER_PID="" +HOST_IP="" +HOST_PORT="" +LATEST_VERSION="" +CURRENT_HEAD="" +CURRENT_HEAD_SHORT="" +OPENAI_API_KEY_VALUE="" + +MACOS_FRESH_STATUS="skip" +WINDOWS_FRESH_STATUS="skip" +LINUX_FRESH_STATUS="skip" +MACOS_UPDATE_STATUS="skip" +WINDOWS_UPDATE_STATUS="skip" +LINUX_UPDATE_STATUS="skip" +MACOS_UPDATE_VERSION="skip" +WINDOWS_UPDATE_VERSION="skip" +LINUX_UPDATE_VERSION="skip" + +say() { + printf '==> %s\n' "$*" +} + +warn() { + printf 'warn: %s\n' "$*" >&2 +} + +die() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +cleanup() { + if [[ -n "${SERVER_PID:-}" ]]; then + kill "$SERVER_PID" >/dev/null 2>&1 || true + fi + rm -rf "$MAIN_TGZ_DIR" +} + +trap cleanup EXIT + +usage() { + cat <<'EOF' +Usage: bash scripts/e2e/parallels-npm-update-smoke.sh [options] + +Options: + --package-spec Baseline npm package spec. Default: openclaw@latest + --openai-api-key-env Host env var name for OpenAI API key. Default: OPENAI_API_KEY + --json Print machine-readable JSON summary. + -h, --help Show help. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --package-spec) + PACKAGE_SPEC="$2" + shift 2 + ;; + --openai-api-key-env) + OPENAI_API_KEY_ENV="$2" + shift 2 + ;; + --json) + JSON_OUTPUT=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown arg: $1" + ;; + esac +done + +OPENAI_API_KEY_VALUE="${!OPENAI_API_KEY_ENV:-}" +[[ -n "$OPENAI_API_KEY_VALUE" ]] || die "$OPENAI_API_KEY_ENV is required" + +resolve_latest_version() { + npm view openclaw version --userconfig "$(mktemp)" +} + +resolve_host_ip() { + local detected + detected="$(ifconfig | awk '/inet 10\.211\./ { print $2; exit }')" + [[ -n "$detected" ]] || die "failed to detect Parallels host IP" + printf '%s\n' "$detected" +} + +allocate_host_port() { + python3 - <<'PY' +import socket + +sock = socket.socket() +sock.bind(("0.0.0.0", 0)) +print(sock.getsockname()[1]) +sock.close() +PY +} + +ensure_current_build() { + say "Build dist for current head" + pnpm build +} + +pack_main_tgz() { + local pkg + CURRENT_HEAD="$(git rev-parse HEAD)" + CURRENT_HEAD_SHORT="$(git rev-parse --short=7 HEAD)" + ensure_current_build + pkg="$( + npm pack --ignore-scripts --json --pack-destination "$MAIN_TGZ_DIR" \ + | python3 -c 'import json, sys; data = json.load(sys.stdin); print(data[-1]["filename"])' + )" + MAIN_TGZ_PATH="$MAIN_TGZ_DIR/openclaw-main-$CURRENT_HEAD_SHORT.tgz" + cp "$MAIN_TGZ_DIR/$pkg" "$MAIN_TGZ_PATH" +} + +start_server() { + HOST_IP="$(resolve_host_ip)" + HOST_PORT="$(allocate_host_port)" + say "Serve current main tgz on $HOST_IP:$HOST_PORT" + ( + cd "$MAIN_TGZ_DIR" + exec python3 -m http.server "$HOST_PORT" --bind 0.0.0.0 + ) >/tmp/openclaw-parallels-npm-update-http.log 2>&1 & + SERVER_PID=$! + sleep 1 + kill -0 "$SERVER_PID" >/dev/null 2>&1 || die "failed to start host HTTP server" +} + +wait_job() { + local label="$1" + local pid="$2" + if wait "$pid"; then + return 0 + fi + warn "$label failed" + return 1 +} + +extract_last_version() { + local log_path="$1" + python3 - "$log_path" <<'PY' +import pathlib +import re +import sys + +text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace") +matches = re.findall(r"OpenClaw [^\r\n]+", text) +print(matches[-1] if matches else "") +PY +} + +guest_powershell() { + local script="$1" + local encoded + encoded="$( + SCRIPT_CONTENT="$script" python3 - <<'PY' +import base64 +import os + +script = "$ProgressPreference = 'SilentlyContinue'\n" + os.environ["SCRIPT_CONTENT"] +payload = script.encode("utf-16le") +print(base64.b64encode(payload).decode("ascii")) +PY + )" + prlctl exec "$WINDOWS_VM" --current-user powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "$encoded" +} + +run_macos_update() { + local tgz_url="$1" + local head_short="$2" + cat </dev/null +set -euo pipefail +export PATH=/opt/homebrew/bin:/opt/homebrew/opt/node/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin +if [ -z "\${HOME:-}" ]; then export HOME="/Users/\$(id -un)"; fi +cd "\$HOME" +curl -fsSL "$tgz_url" -o /tmp/openclaw-main-update.tgz +/opt/homebrew/bin/npm install -g /tmp/openclaw-main-update.tgz +version="\$(/opt/homebrew/bin/openclaw --version)" +printf '%s\n' "\$version" +case "\$version" in + *"$head_short"*) ;; + *) + echo "version mismatch: expected substring $head_short" >&2 + exit 1 + ;; +esac +/opt/homebrew/bin/openclaw models set openai/gpt-5.4 +/opt/homebrew/bin/node /opt/homebrew/lib/node_modules/openclaw/openclaw.mjs gateway status --deep --require-rpc +/opt/homebrew/bin/node /opt/homebrew/lib/node_modules/openclaw/openclaw.mjs agent --agent main --session-id parallels-npm-update-macos-$head_short --message "Reply with exact ASCII text OK only." --json +EOF + prlctl exec "$MACOS_VM" --current-user /bin/bash /tmp/openclaw-main-update.sh +} + +run_windows_update() { + local tgz_url="$1" + local head_short="$2" + guest_powershell "$(cat </dev/null +set -euo pipefail +export HOME=/root +cd "\$HOME" +curl -fsSL "$tgz_url" -o /tmp/openclaw-main-update.tgz +npm install -g /tmp/openclaw-main-update.tgz --no-fund --no-audit +version="\$(openclaw --version)" +printf '%s\n' "\$version" +case "\$version" in + *"$head_short"*) ;; + *) + echo "version mismatch: expected substring $head_short" >&2 + exit 1 + ;; +esac +openclaw models set openai/gpt-5.4 +openclaw agent --local --agent main --session-id parallels-npm-update-linux-$head_short --message "Reply with exact ASCII text OK only." --json +EOF + prlctl exec "$LINUX_VM" /usr/bin/env "OPENAI_API_KEY=$OPENAI_API_KEY_VALUE" /bin/bash /tmp/openclaw-main-update.sh +} + +write_summary_json() { + local summary_path="$RUN_DIR/summary.json" + python3 - "$summary_path" <<'PY' +import json +import os +import sys + +summary = { + "packageSpec": os.environ["SUMMARY_PACKAGE_SPEC"], + "latestVersion": os.environ["SUMMARY_LATEST_VERSION"], + "currentHead": os.environ["SUMMARY_CURRENT_HEAD"], + "runDir": os.environ["SUMMARY_RUN_DIR"], + "fresh": { + "macos": {"status": os.environ["SUMMARY_MACOS_FRESH_STATUS"]}, + "windows": {"status": os.environ["SUMMARY_WINDOWS_FRESH_STATUS"]}, + "linux": {"status": os.environ["SUMMARY_LINUX_FRESH_STATUS"]}, + }, + "update": { + "macos": { + "status": os.environ["SUMMARY_MACOS_UPDATE_STATUS"], + "version": os.environ["SUMMARY_MACOS_UPDATE_VERSION"], + }, + "windows": { + "status": os.environ["SUMMARY_WINDOWS_UPDATE_STATUS"], + "version": os.environ["SUMMARY_WINDOWS_UPDATE_VERSION"], + }, + "linux": { + "status": os.environ["SUMMARY_LINUX_UPDATE_STATUS"], + "version": os.environ["SUMMARY_LINUX_UPDATE_VERSION"], + "mode": "local-with-openai-env", + }, + }, +} +with open(sys.argv[1], "w", encoding="utf-8") as handle: + json.dump(summary, handle, indent=2, sort_keys=True) +print(sys.argv[1]) +PY +} + +LATEST_VERSION="$(resolve_latest_version)" +if [[ -z "$PACKAGE_SPEC" ]]; then + PACKAGE_SPEC="openclaw@$LATEST_VERSION" +fi + +say "Run fresh npm baseline: $PACKAGE_SPEC" +bash "$ROOT_DIR/scripts/e2e/parallels-macos-smoke.sh" \ + --mode fresh \ + --target-package-spec "$PACKAGE_SPEC" \ + --json >"$RUN_DIR/macos-fresh.log" 2>&1 & +macos_fresh_pid=$! + +bash "$ROOT_DIR/scripts/e2e/parallels-windows-smoke.sh" \ + --mode fresh \ + --target-package-spec "$PACKAGE_SPEC" \ + --json >"$RUN_DIR/windows-fresh.log" 2>&1 & +windows_fresh_pid=$! + +bash "$ROOT_DIR/scripts/e2e/parallels-linux-smoke.sh" \ + --mode fresh \ + --target-package-spec "$PACKAGE_SPEC" \ + --json >"$RUN_DIR/linux-fresh.log" 2>&1 & +linux_fresh_pid=$! + +wait_job "macOS fresh" "$macos_fresh_pid" && MACOS_FRESH_STATUS="pass" || MACOS_FRESH_STATUS="fail" +wait_job "Windows fresh" "$windows_fresh_pid" && WINDOWS_FRESH_STATUS="pass" || WINDOWS_FRESH_STATUS="fail" +wait_job "Linux fresh" "$linux_fresh_pid" && LINUX_FRESH_STATUS="pass" || LINUX_FRESH_STATUS="fail" + +[[ "$MACOS_FRESH_STATUS" == "pass" ]] || die "macOS fresh baseline failed" +[[ "$WINDOWS_FRESH_STATUS" == "pass" ]] || die "Windows fresh baseline failed" +[[ "$LINUX_FRESH_STATUS" == "pass" ]] || die "Linux fresh baseline failed" + +pack_main_tgz +start_server + +tgz_url="http://$HOST_IP:$HOST_PORT/$(basename "$MAIN_TGZ_PATH")" + +say "Run same-guest update to current main" +run_macos_update "$tgz_url" "$CURRENT_HEAD_SHORT" >"$RUN_DIR/macos-update.log" 2>&1 & +macos_update_pid=$! +run_windows_update "$tgz_url" "$CURRENT_HEAD_SHORT" >"$RUN_DIR/windows-update.log" 2>&1 & +windows_update_pid=$! +run_linux_update "$tgz_url" "$CURRENT_HEAD_SHORT" >"$RUN_DIR/linux-update.log" 2>&1 & +linux_update_pid=$! + +wait_job "macOS update" "$macos_update_pid" && MACOS_UPDATE_STATUS="pass" || MACOS_UPDATE_STATUS="fail" +wait_job "Windows update" "$windows_update_pid" && WINDOWS_UPDATE_STATUS="pass" || WINDOWS_UPDATE_STATUS="fail" +wait_job "Linux update" "$linux_update_pid" && LINUX_UPDATE_STATUS="pass" || LINUX_UPDATE_STATUS="fail" + +[[ "$MACOS_UPDATE_STATUS" == "pass" ]] || die "macOS update failed" +[[ "$WINDOWS_UPDATE_STATUS" == "pass" ]] || die "Windows update failed" +[[ "$LINUX_UPDATE_STATUS" == "pass" ]] || die "Linux update failed" + +MACOS_UPDATE_VERSION="$(extract_last_version "$RUN_DIR/macos-update.log")" +WINDOWS_UPDATE_VERSION="$(extract_last_version "$RUN_DIR/windows-update.log")" +LINUX_UPDATE_VERSION="$(extract_last_version "$RUN_DIR/linux-update.log")" + +SUMMARY_PACKAGE_SPEC="$PACKAGE_SPEC" \ +SUMMARY_LATEST_VERSION="$LATEST_VERSION" \ +SUMMARY_CURRENT_HEAD="$CURRENT_HEAD_SHORT" \ +SUMMARY_RUN_DIR="$RUN_DIR" \ +SUMMARY_MACOS_FRESH_STATUS="$MACOS_FRESH_STATUS" \ +SUMMARY_WINDOWS_FRESH_STATUS="$WINDOWS_FRESH_STATUS" \ +SUMMARY_LINUX_FRESH_STATUS="$LINUX_FRESH_STATUS" \ +SUMMARY_MACOS_UPDATE_STATUS="$MACOS_UPDATE_STATUS" \ +SUMMARY_WINDOWS_UPDATE_STATUS="$WINDOWS_UPDATE_STATUS" \ +SUMMARY_LINUX_UPDATE_STATUS="$LINUX_UPDATE_STATUS" \ +SUMMARY_MACOS_UPDATE_VERSION="$MACOS_UPDATE_VERSION" \ +SUMMARY_WINDOWS_UPDATE_VERSION="$WINDOWS_UPDATE_VERSION" \ +SUMMARY_LINUX_UPDATE_VERSION="$LINUX_UPDATE_VERSION" \ +write_summary_json >/dev/null + +if [[ "$JSON_OUTPUT" -eq 1 ]]; then + cat "$RUN_DIR/summary.json" +else + say "Run dir: $RUN_DIR" + cat "$RUN_DIR/summary.json" +fi