fix(e2e): resolve macOS Parallels VM

This commit is contained in:
Vincent Koc
2026-06-15 08:27:48 +08:00
parent a7e0822a1a
commit a231ab8acf
4 changed files with 136 additions and 14 deletions
+5 -1
View File
@@ -45,11 +45,12 @@ import {
import { MacosGuest } from "./guest-transports.ts";
import { runSmokeLane, type SmokeLane, type SmokeLaneStatus } from "./lane-runner.ts";
import { MacosDiscordSmoke } from "./macos-discord.ts";
import { waitForVmStatus } from "./parallels-vm.ts";
import { resolveMacosVmName, waitForVmStatus } from "./parallels-vm.ts";
import { PhaseRunner } from "./phase-runner.ts";
interface MacosOptions {
vmName: string;
vmNameExplicit: boolean;
snapshotHint: string;
mode: Mode;
provider: Provider;
@@ -129,6 +130,7 @@ const defaultOptions = (): MacosOptions => ({
snapshotHint: "macOS 26.5 latest",
targetPackageSpec: "",
vmName: "macOS Tahoe",
vmNameExplicit: false,
});
function usage(): string {
@@ -170,6 +172,7 @@ export function parseArgs(argv: string[]): MacosOptions {
break parseArgv;
case "--vm":
options.vmName = ensureValue(args, i, arg);
options.vmNameExplicit = true;
i++;
break;
case "--snapshot-hint":
@@ -309,6 +312,7 @@ class MacosSmoke {
}
async run(): Promise<void> {
this.options.vmName = resolveMacosVmName(this.options.vmName, this.options.vmNameExplicit);
this.runDir = await makeTempDir("openclaw-parallels-macos.");
this.phases = new PhaseRunner(this.runDir);
this.guest = new MacosGuest(
+36 -13
View File
@@ -37,13 +37,14 @@ import {
} from "./common.ts";
import { runWindowsBackgroundPowerShell } from "./guest-transports.ts";
import { linuxUpdateScript, macosUpdateScript, windowsUpdateScript } from "./npm-update-scripts.ts";
import { ensureVmRunning, resolveUbuntuVmName } from "./parallels-vm.ts";
import { ensureVmRunning, resolveMacosVmName, resolveUbuntuVmName } from "./parallels-vm.ts";
import { runTimedUpdateJob } from "./update-job-timeout.ts";
interface NpmUpdateOptions {
betaValidation?: string;
freshTargetSpec?: string;
hostIp?: string;
macosVm?: string;
packageSpec: string;
targetTarball?: string;
updateTarget: string;
@@ -111,7 +112,7 @@ interface NpmUpdateSummary {
}>;
}
const macosVm = "macOS Tahoe";
const macosVmDefault = "macOS Tahoe";
const windowsVm = "Windows 11";
const linuxVmDefault = "Ubuntu 26.04";
const updateTimeoutSeconds = readPositiveIntEnv("OPENCLAW_PARALLELS_NPM_UPDATE_TIMEOUT_S", 1200);
@@ -298,6 +299,7 @@ Options:
Aliases like beta3 resolve to the latest *-beta.3 version.
--platform <list> Comma-separated platforms to run: all, macos, windows, linux.
Default: all
--macos-vm <name> Explicit Parallels macOS VM name.
--provider <openai|anthropic|minimax>
--model <provider/model> Override the model used for agent-turn smoke checks.
--host-ip <ip> Override Parallels host IP.
@@ -315,6 +317,7 @@ export function parseArgs(argv: string[]): NpmUpdateOptions {
betaValidation: undefined,
freshTargetSpec: undefined,
json: false,
macosVm: undefined,
modelId: undefined,
packageSpec: "",
targetTarball: undefined,
@@ -358,6 +361,10 @@ export function parseArgs(argv: string[]): NpmUpdateOptions {
options.platforms = parsePlatformList(ensureValue(args, i, arg));
i++;
break;
case "--macos-vm":
options.macosVm = ensureValue(args, i, arg);
i++;
break;
case "--provider":
options.provider = parseProvider(ensureValue(args, i, arg));
i++;
@@ -455,6 +462,7 @@ export class NpmUpdateSmoke {
private targetTarballPath = "";
private targetTarballBuildCommit = "";
private targetTarballVersion = "";
private macosVm = macosVmDefault;
private linuxVm = linuxVmDefault;
private freshStatus = platformRecord("skip");
@@ -507,6 +515,12 @@ export class NpmUpdateSmoke {
if (this.options.platforms.has("linux")) {
this.linuxVm = resolveUbuntuVmName(linuxVmDefault);
}
if (this.options.platforms.has("macos")) {
this.macosVm = resolveMacosVmName(
this.options.macosVm ?? macosVmDefault,
Boolean(this.options.macosVm),
);
}
this.preflightRegistryUpdateTarget();
say(`Run fresh npm baseline: ${this.packageSpec}`);
@@ -535,7 +549,7 @@ export class NpmUpdateSmoke {
private async runFreshBaselines(): Promise<void> {
const jobs: Job[] = [];
if (this.options.platforms.has("macos")) {
jobs.push(this.spawnFresh("macOS", "macos", []));
jobs.push(this.spawnFresh("macOS", "macos", ["--vm", this.macosVm]));
}
if (this.options.platforms.has("windows")) {
jobs.push(this.spawnFresh("Windows", "windows", []));
@@ -563,7 +577,16 @@ export class NpmUpdateSmoke {
private async runFreshTargetInstalls(): Promise<void> {
const jobs: Job[] = [];
if (this.options.platforms.has("macos")) {
jobs.push(this.spawnFresh("macOS", "macos", [], {}, this.freshTargetSpec, "fresh-target"));
jobs.push(
this.spawnFresh(
"macOS",
"macos",
["--vm", this.macosVm],
{},
this.freshTargetSpec,
"fresh-target",
),
);
}
if (this.options.platforms.has("windows")) {
jobs.push(
@@ -768,7 +791,7 @@ export class NpmUpdateSmoke {
private async runSameGuestUpdates(): Promise<void> {
const jobs: Job[] = [];
if (this.options.platforms.has("macos")) {
ensureVmRunning(macosVm);
ensureVmRunning(this.macosVm);
jobs.push(this.spawnUpdate("macOS", "macos", (ctx) => this.runMacosUpdate(ctx)));
}
if (this.options.platforms.has("windows")) {
@@ -912,7 +935,7 @@ export class NpmUpdateSmoke {
ctx: UpdateJobContext,
): Promise<void> {
const scriptPath = this.writeGuestScript(
macosVm,
this.macosVm,
script,
"openclaw-parallels-npm-update-macos",
);
@@ -923,14 +946,14 @@ export class NpmUpdateSmoke {
? macosExecArgs[sudoUserArgIndex + 1]
: "";
if (sudoUser) {
run("prlctl", ["exec", macosVm, "/usr/sbin/chown", sudoUser, scriptPath], {
run("prlctl", ["exec", this.macosVm, "/usr/sbin/chown", sudoUser, scriptPath], {
timeoutMs: 30_000,
});
}
try {
const status = await this.runStreamingToJobLog(
"prlctl",
["exec", macosVm, ...macosExecArgs, "/bin/bash", scriptPath],
["exec", this.macosVm, ...macosExecArgs, "/bin/bash", scriptPath],
timeoutMs,
ctx,
);
@@ -938,14 +961,14 @@ export class NpmUpdateSmoke {
throw new Error(`macOS update command failed with exit code ${status}`);
}
} finally {
this.removeGuestScript(macosVm, scriptPath);
this.removeGuestScript(this.macosVm, scriptPath);
}
}
private resolveMacosUpdateExecArgs(ctx: UpdateJobContext): string[] {
const guestPath =
"/opt/homebrew/bin:/opt/homebrew/opt/node/bin:/usr/local/bin:/usr/local/sbin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin";
const currentUser = run("prlctl", ["exec", macosVm, "--current-user", "whoami"], {
const currentUser = run("prlctl", ["exec", this.macosVm, "--current-user", "whoami"], {
check: false,
quiet: true,
timeoutMs: 45_000,
@@ -980,7 +1003,7 @@ export class NpmUpdateSmoke {
private resolveMacosDesktopUser(): string {
const consoleUser =
run("prlctl", ["exec", macosVm, "/usr/bin/stat", "-f", "%Su", "/dev/console"], {
run("prlctl", ["exec", this.macosVm, "/usr/bin/stat", "-f", "%Su", "/dev/console"], {
check: false,
quiet: true,
timeoutMs: 30_000,
@@ -998,7 +1021,7 @@ export class NpmUpdateSmoke {
}
const users = run(
"prlctl",
["exec", macosVm, "/usr/bin/dscl", ".", "-list", "/Users", "NFSHomeDirectory"],
["exec", this.macosVm, "/usr/bin/dscl", ".", "-list", "/Users", "NFSHomeDirectory"],
{ check: false, quiet: true, timeoutMs: 30_000 },
).stdout.replaceAll("\r", "");
for (const line of users.split("\n")) {
@@ -1019,7 +1042,7 @@ export class NpmUpdateSmoke {
private resolveMacosDesktopHome(user: string): string {
const output = run(
"prlctl",
["exec", macosVm, "/usr/bin/dscl", ".", "-read", `/Users/${user}`, "NFSHomeDirectory"],
["exec", this.macosVm, "/usr/bin/dscl", ".", "-read", `/Users/${user}`, "NFSHomeDirectory"],
{ check: false, quiet: true, timeoutMs: 30_000 },
).stdout.replaceAll("\r", "");
const match = /NFSHomeDirectory:\s*(\S+)/.exec(output);
+16
View File
@@ -105,6 +105,22 @@ export function resolveUbuntuVmName(requested: string, explicit = false): string
return fallback;
}
export function resolveMacosVmName(requested: string, explicit = false): string {
const names = listVmNames();
if (names.includes(requested)) {
return requested;
}
if (explicit) {
die(`VM not found: ${requested}`);
}
const fallback = names.find((name) => name === "macOS");
if (!fallback) {
die(`VM not found: ${requested}; select a macOS VM explicitly`);
}
warn(`requested VM ${requested} not found; using ${fallback}`);
return fallback;
}
function listVms(timeoutMs = PRLCTL_STATUS_TIMEOUT_MS): PrlctlVmListItem[] {
return JSON.parse(
run("prlctl", ["list", "--all", "--json"], {