mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 18:32:54 +00:00
fix(e2e): bound Parallels host VM commands
This commit is contained in:
@@ -363,7 +363,8 @@ class LinuxSmoke extends SmokeRunController<LinuxOptions> {
|
||||
private phase = async (name: string, timeoutSeconds: number, fn: () => Promise<void> | void) =>
|
||||
await this.phases.phase(name, timeoutSeconds, fn);
|
||||
|
||||
private remainingPhaseTimeoutMs = (): number | undefined => this.phases.remainingTimeoutMs();
|
||||
private remainingPhaseTimeoutMs = (fallbackMs?: number): number | undefined =>
|
||||
this.phases.remainingTimeoutMs(fallbackMs);
|
||||
|
||||
private logGuestPreflight(): void {
|
||||
this.guestBash(String.raw`set -euo pipefail
|
||||
@@ -406,11 +407,17 @@ printf 'preflight.npmRoot=%s\n' "$(npm root -g 2>/dev/null || true)"`);
|
||||
say(`Restore snapshot ${this.options.snapshotHint} (${this.snapshot.id})`);
|
||||
run("prlctl", ["snapshot-switch", this.options.vmName, "--id", this.snapshot.id], {
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(),
|
||||
});
|
||||
if (this.snapshot.state === "poweroff") {
|
||||
waitForVmStatus(this.options.vmName, "stopped", 180);
|
||||
waitForVmStatus(this.options.vmName, "stopped", 180, {
|
||||
probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000),
|
||||
});
|
||||
say(`Start restored poweroff snapshot ${this.snapshot.name}`);
|
||||
run("prlctl", ["start", this.options.vmName], { quiet: true });
|
||||
run("prlctl", ["start", this.options.vmName], {
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(120_000),
|
||||
});
|
||||
}
|
||||
this.waitForGuestReady();
|
||||
}
|
||||
|
||||
@@ -565,8 +565,8 @@ class MacosSmoke {
|
||||
await this.phases.phase(name, timeoutSeconds, fn);
|
||||
}
|
||||
|
||||
private remainingPhaseTimeoutMs(): number | undefined {
|
||||
return this.phases.remainingTimeoutMs();
|
||||
private remainingPhaseTimeoutMs(fallbackMs?: number): number | undefined {
|
||||
return this.phases.remainingTimeoutMs(fallbackMs);
|
||||
}
|
||||
|
||||
private async phaseReturns(
|
||||
@@ -653,6 +653,7 @@ exec node "$entry" ${argv}`,
|
||||
run("prlctl", ["exec", this.options.vmName, "/usr/bin/stat", "-f", "%Su", "/dev/console"], {
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(30_000),
|
||||
})
|
||||
.stdout.trim()
|
||||
.replaceAll("\r", "")
|
||||
@@ -671,6 +672,7 @@ exec node "$entry" ${argv}`,
|
||||
{
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(30_000),
|
||||
},
|
||||
).stdout.replaceAll("\r", "");
|
||||
for (const line of users.split("\n")) {
|
||||
@@ -700,7 +702,7 @@ exec node "$entry" ${argv}`,
|
||||
`/Users/${user}`,
|
||||
"NFSHomeDirectory",
|
||||
],
|
||||
{ check: false, quiet: true },
|
||||
{ check: false, quiet: true, timeoutMs: this.remainingPhaseTimeoutMs(30_000) },
|
||||
).stdout.replaceAll("\r", "");
|
||||
const match = /^NFSHomeDirectory:\s+(.+)$/m.exec(output);
|
||||
return match?.[1]?.trim() || `/Users/${user}`;
|
||||
@@ -713,7 +715,7 @@ exec node "$entry" ${argv}`,
|
||||
const result = run(
|
||||
"prlctl",
|
||||
["snapshot-switch", this.options.vmName, "--id", this.snapshot.id, "--skip-resume"],
|
||||
{ check: false, quiet: true, timeoutMs: 360_000 },
|
||||
{ check: false, quiet: true, timeoutMs: this.remainingPhaseTimeoutMs(360_000) },
|
||||
);
|
||||
this.log(result.stdout);
|
||||
this.log(result.stderr);
|
||||
@@ -725,10 +727,17 @@ exec node "$entry" ${argv}`,
|
||||
const status = run("prlctl", ["status", this.options.vmName], {
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(60_000),
|
||||
}).stdout;
|
||||
if (status.includes(" running") || status.includes(" suspended")) {
|
||||
run("prlctl", ["stop", this.options.vmName, "--kill"], { check: false, quiet: true });
|
||||
waitForVmStatus(this.options.vmName, "stopped", 360);
|
||||
run("prlctl", ["stop", this.options.vmName, "--kill"], {
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(120_000),
|
||||
});
|
||||
waitForVmStatus(this.options.vmName, "stopped", 360, {
|
||||
probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000),
|
||||
});
|
||||
}
|
||||
run("sleep", ["3"], { quiet: true });
|
||||
}
|
||||
@@ -738,15 +747,23 @@ exec node "$entry" ${argv}`,
|
||||
const status = run("prlctl", ["status", this.options.vmName], {
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: 60_000,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(60_000),
|
||||
}).stdout;
|
||||
if (this.snapshot.state === "poweroff" || status.includes(" stopped")) {
|
||||
waitForVmStatus(this.options.vmName, "stopped", 360);
|
||||
waitForVmStatus(this.options.vmName, "stopped", 360, {
|
||||
probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000),
|
||||
});
|
||||
say(`Start restored poweroff snapshot ${this.snapshot.name}`);
|
||||
run("prlctl", ["start", this.options.vmName], { quiet: true });
|
||||
run("prlctl", ["start", this.options.vmName], {
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(120_000),
|
||||
});
|
||||
} else if (status.includes(" suspended")) {
|
||||
say(`Resume restored snapshot ${this.snapshot.name}`);
|
||||
run("prlctl", ["start", this.options.vmName], { quiet: true });
|
||||
run("prlctl", ["start", this.options.vmName], {
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(120_000),
|
||||
});
|
||||
}
|
||||
this.waitForCurrentUser();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { die, run, say, warn } from "./host-command.ts";
|
||||
|
||||
const PRLCTL_STATUS_TIMEOUT_MS = 30_000;
|
||||
const PRLCTL_TRANSITION_TIMEOUT_MS = 120_000;
|
||||
|
||||
interface PrlctlVmListItem {
|
||||
name?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface WaitForVmStatusOptions {
|
||||
probeTimeoutMs?: () => number | undefined;
|
||||
}
|
||||
|
||||
export function listVmNames(): string[] {
|
||||
return listVms()
|
||||
.map((item) => (item.name ?? "").trim())
|
||||
@@ -15,12 +22,18 @@ export function vmStatus(vmName: string): string {
|
||||
return listVms().find((vm) => vm.name === vmName)?.status || "missing";
|
||||
}
|
||||
|
||||
export function waitForVmStatus(vmName: string, expected: string, timeoutSeconds: number): void {
|
||||
export function waitForVmStatus(
|
||||
vmName: string,
|
||||
expected: string,
|
||||
timeoutSeconds: number,
|
||||
options: WaitForVmStatusOptions = {},
|
||||
): void {
|
||||
const deadline = Date.now() + timeoutSeconds * 1000;
|
||||
while (Date.now() < deadline) {
|
||||
const status = run("prlctl", ["status", vmName], {
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: options.probeTimeoutMs?.() ?? PRLCTL_STATUS_TIMEOUT_MS,
|
||||
}).stdout;
|
||||
if (status.includes(` ${expected}`)) {
|
||||
return;
|
||||
@@ -39,10 +52,16 @@ export function ensureVmRunning(vmName: string, timeoutSeconds = 180): void {
|
||||
}
|
||||
if (status === "stopped") {
|
||||
say(`Start ${vmName} before update phase`);
|
||||
run("prlctl", ["start", vmName], { quiet: true });
|
||||
run("prlctl", ["start", vmName], {
|
||||
quiet: true,
|
||||
timeoutMs: PRLCTL_TRANSITION_TIMEOUT_MS,
|
||||
});
|
||||
} else if (status === "suspended" || status === "paused") {
|
||||
say(`Resume ${vmName} before update phase`);
|
||||
run("prlctl", ["resume", vmName], { quiet: true });
|
||||
run("prlctl", ["resume", vmName], {
|
||||
quiet: true,
|
||||
timeoutMs: PRLCTL_TRANSITION_TIMEOUT_MS,
|
||||
});
|
||||
} else if (status === "missing") {
|
||||
die(`VM not found before update phase: ${vmName}`);
|
||||
}
|
||||
@@ -79,7 +98,10 @@ export function resolveUbuntuVmName(requested: string, explicit = false): string
|
||||
|
||||
function listVms(): PrlctlVmListItem[] {
|
||||
return JSON.parse(
|
||||
run("prlctl", ["list", "--all", "--json"], { quiet: true }).stdout,
|
||||
run("prlctl", ["list", "--all", "--json"], {
|
||||
quiet: true,
|
||||
timeoutMs: PRLCTL_STATUS_TIMEOUT_MS,
|
||||
}).stdout,
|
||||
) as PrlctlVmListItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { die, run } from "./host-command.ts";
|
||||
import type { SnapshotInfo } from "./types.ts";
|
||||
|
||||
const SNAPSHOT_LIST_TIMEOUT_MS = 120_000;
|
||||
|
||||
export function resolveSnapshot(vmName: string, hint: string): SnapshotInfo {
|
||||
const output = run("prlctl", ["snapshot-list", vmName, "--json"], { quiet: true }).stdout;
|
||||
const output = run("prlctl", ["snapshot-list", vmName, "--json"], {
|
||||
quiet: true,
|
||||
timeoutMs: SNAPSHOT_LIST_TIMEOUT_MS,
|
||||
}).stdout;
|
||||
const payload = JSON.parse(output) as Record<string, { name?: string; state?: string }>;
|
||||
let best: SnapshotInfo | null = null;
|
||||
let bestScore = -1;
|
||||
|
||||
@@ -449,6 +449,7 @@ class WindowsSmoke extends SmokeRunController<WindowsOptions> {
|
||||
{
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(),
|
||||
},
|
||||
);
|
||||
this.log(result.stdout);
|
||||
@@ -469,9 +470,14 @@ class WindowsSmoke extends SmokeRunController<WindowsOptions> {
|
||||
}
|
||||
this.waitForVmNotRestoring(240);
|
||||
if (this.snapshot.state === "poweroff") {
|
||||
waitForVmStatus(this.options.vmName, "stopped", 240);
|
||||
waitForVmStatus(this.options.vmName, "stopped", 240, {
|
||||
probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000),
|
||||
});
|
||||
say(`Start restored poweroff snapshot ${this.snapshot.name}`);
|
||||
run("prlctl", ["start", this.options.vmName], { quiet: true });
|
||||
run("prlctl", ["start", this.options.vmName], {
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(120_000),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +487,7 @@ class WindowsSmoke extends SmokeRunController<WindowsOptions> {
|
||||
const status = run("prlctl", ["status", this.options.vmName], {
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(30_000),
|
||||
}).stdout;
|
||||
if (!status.includes(" restoring")) {
|
||||
return;
|
||||
|
||||
@@ -602,10 +602,15 @@ if (isPrlctl) {
|
||||
it("clears phase timers and applies phase deadlines to guest commands", () => {
|
||||
const phaseRunner = readFileSync(TS_PATHS.phaseRunner, "utf8");
|
||||
const guestTransports = readFileSync(TS_PATHS.guestTransports, "utf8");
|
||||
const parallelsVm = readFileSync(TS_PATHS.parallelsVm, "utf8");
|
||||
const snapshots = readFileSync(TS_PATHS.snapshots, "utf8");
|
||||
|
||||
expect(phaseRunner).toContain("clearTimeout(timer)");
|
||||
expect(phaseRunner).toContain("remainingTimeoutMs");
|
||||
expect(guestTransports).toContain("this.phases.remainingTimeoutMs");
|
||||
expect(parallelsVm).toContain("PRLCTL_STATUS_TIMEOUT_MS");
|
||||
expect(parallelsVm).toContain("probeTimeoutMs");
|
||||
expect(snapshots).toContain("SNAPSHOT_LIST_TIMEOUT_MS");
|
||||
|
||||
for (const scriptPath of OS_TS_PATHS) {
|
||||
const script = readFileSync(scriptPath, "utf8");
|
||||
@@ -614,6 +619,14 @@ if (isPrlctl) {
|
||||
expect(script, scriptPath).toContain("remainingPhaseTimeoutMs");
|
||||
expect(script, scriptPath).toContain("timeoutMs:");
|
||||
}
|
||||
|
||||
const linux = readFileSync(TS_PATHS.linux, "utf8");
|
||||
const macos = readFileSync(TS_PATHS.macos, "utf8");
|
||||
const windows = readFileSync(TS_PATHS.windows, "utf8");
|
||||
expect(linux).toContain("probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000)");
|
||||
expect(windows).toContain("probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000)");
|
||||
expect(macos).toContain("probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000)");
|
||||
expect(macos).toContain("timeoutMs: this.remainingPhaseTimeoutMs(360_000)");
|
||||
});
|
||||
|
||||
it("streams full phase logs to disk while bounding the failure tail", async () => {
|
||||
|
||||
Reference in New Issue
Block a user