import { chmodSync, copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { delimiter, join, win32 } from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { modelProviderConfigBatchJson, resolveParallelsModelTimeoutSeconds, resolveProviderAuth as resolveProviderAuthDirect, resolveSnapshot, resolveUbuntuVmName, resolveWindowsProviderAuth, run, shellQuote, } from "../../scripts/e2e/parallels/common.ts"; import { resolveHostCommandInvocation } from "../../scripts/e2e/parallels/host-command.ts"; import { spawnNodeEvalSync } from "../../src/test-utils/node-process.js"; const WRAPPERS = { linux: "scripts/e2e/parallels-linux-smoke.sh", macos: "scripts/e2e/parallels-macos-smoke.sh", npmUpdate: "scripts/e2e/parallels-npm-update-smoke.sh", windows: "scripts/e2e/parallels-windows-smoke.sh", }; const TS_PATHS = { agentWorkspace: "scripts/e2e/parallels/agent-workspace.ts", common: "scripts/e2e/parallels/common.ts", guestTransports: "scripts/e2e/parallels/guest-transports.ts", hostCommand: "scripts/e2e/parallels/host-command.ts", hostServer: "scripts/e2e/parallels/host-server.ts", laneRunner: "scripts/e2e/parallels/lane-runner.ts", linux: "scripts/e2e/parallels/linux-smoke.ts", macosDiscord: "scripts/e2e/parallels/macos-discord.ts", macos: "scripts/e2e/parallels/macos-smoke.ts", npmUpdateScripts: "scripts/e2e/parallels/npm-update-scripts.ts", npmUpdate: "scripts/e2e/parallels/npm-update-smoke.ts", packageArtifact: "scripts/e2e/parallels/package-artifact.ts", parallelsVm: "scripts/e2e/parallels/parallels-vm.ts", phaseRunner: "scripts/e2e/parallels/phase-runner.ts", powershell: "scripts/e2e/parallels/powershell.ts", providerAuth: "scripts/e2e/parallels/provider-auth.ts", snapshots: "scripts/e2e/parallels/snapshots.ts", windows: "scripts/e2e/parallels/windows-smoke.ts", windowsGit: "scripts/e2e/parallels/windows-git.ts", }; const OS_TS_PATHS = [TS_PATHS.linux, TS_PATHS.macos, TS_PATHS.windows]; function countNonEmptyLines(value: string): number { let count = 0; for (const line of value.split("\n")) { if (line) { count += 1; } } return count; } function fakePrlctlEnv(tempDir: string): Record { const pathValue = `${tempDir}${delimiter}${process.env.Path ?? process.env.PATH ?? ""}`; const fakeBootstrap = pathToFileURL(join(tempDir, "prlctl-bootstrap.mjs")).href; const nodeOptions = [process.env.NODE_OPTIONS, `--import=${fakeBootstrap}`] .filter(Boolean) .join(" "); return { NODE_OPTIONS: nodeOptions, PATH: pathValue, Path: pathValue }; } function writeFakePrlctl(tempDir: string, posixScript: string, windowsBootstrap: string): void { const prlctlPath = join(tempDir, "prlctl"); writeFileSync(prlctlPath, posixScript); chmodSync(prlctlPath, 0o755); copyFileSync(process.execPath, join(tempDir, "prlctl.exe")); writeFileSync(join(tempDir, "prlctl-bootstrap.mjs"), windowsBootstrap); } function withEnv(env: Record, callback: () => T): T { const previous = new Map(); for (const [key, value] of Object.entries(env)) { previous.set(key, process.env[key]); } for (const [key, value] of Object.entries(env)) { process.env[key] = value; } try { return callback(); } finally { for (const [key, value] of previous) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } } } describe("Parallels smoke model selection", () => { it("keeps the public shell entrypoints as thin TypeScript launchers", () => { for (const [platform, wrapperPath] of Object.entries(WRAPPERS)) { const wrapper = readFileSync(wrapperPath, "utf8"); expect(wrapper, wrapperPath).toContain('exec pnpm --dir "$ROOT_DIR" exec tsx'); if (platform === "npmUpdate") { expect(wrapper, wrapperPath).toContain(TS_PATHS.npmUpdate); } else { expect(wrapper, wrapperPath).toContain(TS_PATHS[platform as "linux" | "macos" | "windows"]); } expect(countNonEmptyLines(wrapper)).toBeLessThanOrEqual(5); } }); it("keeps provider auth and model defaults in the shared TypeScript helper", () => { const providerAuth = readFileSync(TS_PATHS.providerAuth, "utf8"); expect(providerAuth).toContain("OPENCLAW_PARALLELS_OPENAI_MODEL"); expect(providerAuth).toContain("OPENCLAW_PARALLELS_WINDOWS_OPENAI_MODEL"); expect(providerAuth).toContain("openai/gpt-5.5"); expect(providerAuth).toContain('authChoice: "openai-api-key"'); expect(providerAuth).toContain('authChoice: "apiKey"'); expect(providerAuth).toContain('authChoice: "minimax-global-api"'); for (const scriptPath of [...OS_TS_PATHS, TS_PATHS.npmUpdate]) { const script = readFileSync(scriptPath, "utf8"); expect(script, scriptPath).toMatch(/resolve(?:Windows)?ProviderAuth/u); expect(script, scriptPath).toContain("--model "); expect(script, scriptPath).toContain("modelId"); } }); it("writes full model ids as config map keys in provider batches", () => { const batch = JSON.parse(modelProviderConfigBatchJson("openai/gpt-5.5", "windows")) as Array<{ path: string; value: unknown; }>; expect(batch.map((entry) => entry.path)).toContain('agents.defaults.models["openai/gpt-5.5"]'); expect(JSON.stringify(batch)).not.toContain("agentRuntime"); }); it("keeps snapshot, host, package, and quote helpers shared", () => { const common = readFileSync(TS_PATHS.common, "utf8"); const hostCommand = readFileSync(TS_PATHS.hostCommand, "utf8"); const hostServer = readFileSync(TS_PATHS.hostServer, "utf8"); const laneRunner = readFileSync(TS_PATHS.laneRunner, "utf8"); const packageArtifact = readFileSync(TS_PATHS.packageArtifact, "utf8"); const parallelsVm = readFileSync(TS_PATHS.parallelsVm, "utf8"); const snapshots = readFileSync(TS_PATHS.snapshots, "utf8"); expect(common).toContain('export * from "./host-command.ts"'); expect(common).toContain('export * from "./lane-runner.ts"'); expect(common).toContain('export * from "./package-artifact.ts"'); expect(common).toContain('export * from "./parallels-vm.ts"'); expect(common).toContain('export * from "./snapshots.ts"'); expect(hostCommand).toContain("export function shellQuote"); expect(laneRunner).toContain("export async function runSmokeLane"); expect(packageArtifact).toContain("withPackageLock"); expect(packageArtifact).toContain("Wait for Parallels package lock"); expect(packageArtifact).toContain("export async function packageVersionFromTgz"); expect(packageArtifact).toContain("export async function packOpenClaw"); expect(parallelsVm).toContain("export function resolveUbuntuVmName"); expect(parallelsVm).toContain("export function waitForVmStatus"); expect(hostServer).toContain("export async function startHostServer"); expect(hostServer).toContain("http.server"); expect(snapshots).toContain("export function resolveSnapshot"); for (const scriptPath of OS_TS_PATHS) { const script = readFileSync(scriptPath, "utf8"); expect(script, scriptPath).toContain("resolveSnapshot"); expect(script, scriptPath).toContain("runSmokeLane"); expect(script, scriptPath).not.toContain("def aliases(name: str)"); } }); it("quotes shell args and resolves fuzzy snapshot hints through the shared TypeScript helper", () => { const tempDir = mkdtempSync(join(tmpdir(), "openclaw-parallels-helper-")); writeFakePrlctl( tempDir, `#!/usr/bin/env bash set -euo pipefail if [[ "$1" == "snapshot-list" ]]; then cat <<'JSON' { "{older}": {"name": "fresh", "state": "running"}, "{wanted}": {"name": "fresh-poweroff-2026-04-01", "state": "poweroff"}, "{other}": {"name": "unrelated", "state": "poweroff"} } JSON exit 0 fi exit 1 `, `import { basename } from "node:path"; const isPrlctl = [process.argv0, process.execPath].some((value) => basename(value).toLowerCase() === "prlctl.exe", ); if (isPrlctl) { if (process.argv.some((arg) => arg.includes("snapshot-list"))) { console.log(JSON.stringify({ "{older}": { name: "fresh", state: "running" }, "{wanted}": { name: "fresh-poweroff-2026-04-01", state: "poweroff" }, "{other}": { name: "unrelated", state: "poweroff" }, })); process.exit(0); } process.exit(1); } `, ); try { const output = withEnv(fakePrlctlEnv(tempDir), () => { const snapshot = resolveSnapshot("vm", "fresh"); return `${shellQuote("it's ok")}\n${[snapshot.id, snapshot.state, snapshot.name].join("\t")}`; }); expect(output.split("\n")[0]).toBe("'it'\"'\"'s ok'"); expect(output).toContain("{wanted}\tpoweroff\tfresh-poweroff-2026-04-01"); } finally { rmSync(tempDir, { force: true, recursive: true }); } }); it("resolves a latest snapshot hint to the matching version before older LATEST labels", () => { const tempDir = mkdtempSync(join(tmpdir(), "openclaw-parallels-snapshot-latest-")); writeFakePrlctl( tempDir, `#!/usr/bin/env bash set -euo pipefail if [[ "$1" == "snapshot-list" ]]; then cat <<'JSON' { "{old}": {"name": "macOS 26.3.1 LATEST", "state": "poweron"}, "{wanted}": {"name": "macOS 26.5", "state": "poweron"} } JSON exit 0 fi exit 1 `, `import { basename } from "node:path"; const isPrlctl = [process.argv0, process.execPath].some((value) => basename(value).toLowerCase() === "prlctl.exe", ); if (isPrlctl) { if (process.argv.some((arg) => arg.includes("snapshot-list"))) { console.log(JSON.stringify({ "{old}": { name: "macOS 26.3.1 LATEST", state: "poweron" }, "{wanted}": { name: "macOS 26.5", state: "poweron" }, })); process.exit(0); } process.exit(1); } `, ); try { const output = withEnv(fakePrlctlEnv(tempDir), () => { const snapshot = resolveSnapshot("vm", "macOS 26.5 latest"); return [snapshot.id, snapshot.state, snapshot.name].join("\t"); }); expect(output).toBe("{wanted}\tpoweron\tmacOS 26.5"); } finally { rmSync(tempDir, { force: true, recursive: true }); } }); it("uses one Ubuntu VM fallback resolver for Linux lanes", () => { const tempDir = mkdtempSync(join(tmpdir(), "openclaw-parallels-vm-helper-")); writeFakePrlctl( tempDir, `#!/usr/bin/env bash set -euo pipefail if [[ "$1" == "list" ]]; then cat <<'JSON' [ {"name": "Ubuntu 26.04"}, {"name": "Ubuntu 25.10"}, {"name": "Ubuntu 23.10"}, {"name": "Ubuntu 24.04.3 ARM64"} ] JSON exit 0 fi exit 1 `, `import { basename } from "node:path"; const isPrlctl = [process.argv0, process.execPath].some((value) => basename(value).toLowerCase() === "prlctl.exe", ); if (isPrlctl) { if (process.argv.some((arg) => arg.includes("list"))) { console.log(JSON.stringify([ { name: "Ubuntu 26.04" }, { name: "Ubuntu 25.10" }, { name: "Ubuntu 23.10" }, { name: "Ubuntu 24.04.3 ARM64" }, ])); process.exit(0); } process.exit(1); } `, ); try { const output = withEnv(fakePrlctlEnv(tempDir), () => resolveUbuntuVmName("Ubuntu missing")); expect(output).toBe("Ubuntu 26.04"); } finally { rmSync(tempDir, { force: true, recursive: true }); } }); it("waits for apt locks during Linux snapshot bootstrap", () => { const script = readFileSync(TS_PATHS.linux, "utf8"); expect(script).toContain("DPkg::Lock::Timeout=300"); }); it("keeps Linux bad-plugin diagnostics gated for historical update baselines", () => { const script = readFileSync(TS_PATHS.linux, "utf8"); expect(script).toContain('BAD_PLUGIN_DIAGNOSTIC_MIN_VERSION = "2026.5.7"'); expect(script).toContain("parseOpenClawPackageVersion"); expect(script).toContain("maybeInjectBadPluginFixture"); expect(script).toContain("maybeVerifyBadPluginDiagnostic"); expect(script).toContain("Skipping bad plugin diagnostic fixture"); expect(script).toContain("Skipping bad plugin diagnostic assertion"); }); it("resolves provider defaults and explicit model overrides", () => { expect( withEnv({ OPENAI_API_KEY: "sk-openai" }, () => resolveProviderAuthDirect({ provider: "openai" }), ), ).toEqual({ apiKeyEnv: "OPENAI_API_KEY", apiKeyValue: "sk-openai", authChoice: "openai-api-key", authKeyFlag: "openai-api-key", modelId: "openai/gpt-5.5", }); expect( withEnv({ CUSTOM_ANTHROPIC_KEY: "sk-anthropic" }, () => resolveProviderAuthDirect({ apiKeyEnv: "CUSTOM_ANTHROPIC_KEY", modelId: "anthropic/custom", provider: "anthropic", }), ), ).toEqual({ apiKeyEnv: "CUSTOM_ANTHROPIC_KEY", apiKeyValue: "sk-anthropic", authChoice: "apiKey", authKeyFlag: "anthropic-api-key", modelId: "anthropic/custom", }); }); it("uses the shared GPT-5 OpenAI model for Windows smoke unless overridden", () => { expect( withEnv({ OPENAI_API_KEY: "sk-openai" }, () => resolveWindowsProviderAuth({ provider: "openai" }), ), ).toEqual({ apiKeyEnv: "OPENAI_API_KEY", apiKeyValue: "sk-openai", authChoice: "openai-api-key", authKeyFlag: "openai-api-key", modelId: "openai/gpt-5.5", }); expect( withEnv( { OPENAI_API_KEY: "sk-openai", OPENCLAW_PARALLELS_WINDOWS_OPENAI_MODEL: "openai/custom-windows", }, () => resolveWindowsProviderAuth({ provider: "openai" }), ), ).toEqual({ apiKeyEnv: "OPENAI_API_KEY", apiKeyValue: "sk-openai", authChoice: "openai-api-key", authKeyFlag: "openai-api-key", modelId: "openai/custom-windows", }); }); it("rejects invalid providers and missing keys before touching guests", () => { const invalidProvider = spawnNodeEvalSync( `import { parseProvider } from "./${TS_PATHS.common}"; parseProvider("bogus");`, { env: process.env, imports: ["tsx"] }, ); expect(invalidProvider.status).toBe(1); expect(invalidProvider.stderr).toContain("invalid --provider: bogus"); const missingKey = spawnNodeEvalSync( `import { resolveProviderAuth } from "./${TS_PATHS.common}"; resolveProviderAuth({ provider: "openai", apiKeyEnv: "PARALLELS_TEST_MISSING_KEY" });`, { env: { ...process.env, PARALLELS_TEST_MISSING_KEY: "" }, imports: ["tsx"], }, ); expect(missingKey.status).toBe(1); expect(missingKey.stderr).toContain("PARALLELS_TEST_MISSING_KEY is required"); }); it("seeds configured agent workspace files before OS smoke agent turns", () => { const workspace = readFileSync(TS_PATHS.agentWorkspace, "utf8"); expect(workspace).toContain("IDENTITY.md"); expect(workspace).toContain("BOOTSTRAP.md"); for (const scriptPath of OS_TS_PATHS) { const script = readFileSync(scriptPath, "utf8"); expect(script, scriptPath).toContain("AgentWorkspaceScript"); expect(script, scriptPath).toContain("parallels-"); if (scriptPath !== TS_PATHS.windows) { expect(script, scriptPath).toContain("agents.defaults.skipBootstrap"); expect(script, scriptPath).toContain("tools.profile"); } expect(script, scriptPath).toContain("--thinking"); expect(script, scriptPath).toContain("off"); expect(script, scriptPath).toContain("finalAssistant(Raw|Visible)Text"); } expect(readFileSync(TS_PATHS.macos, "utf8")).toContain("modelProviderConfigBatchJson"); expect(readFileSync(TS_PATHS.macos, "utf8")).toContain("config set --batch-file"); expect(readFileSync(TS_PATHS.linux, "utf8")).toContain("modelProviderConfigBatchJson"); expect(readFileSync(TS_PATHS.linux, "utf8")).toContain("config set --batch-file"); expect(readFileSync(TS_PATHS.windows, "utf8")).toContain("windowsAgentTurnConfigPatchScript"); const powershell = readFileSync(TS_PATHS.powershell, "utf8"); expect(powershell).toContain("config set --batch-file"); expect(powershell).toContain("agents.defaults.skipBootstrap"); expect(powershell).toContain("tools.profile"); expect(powershell).toContain("replace(/^\\\\uFEFF/u"); const npmUpdateScripts = readFileSync(TS_PATHS.npmUpdateScripts, "utf8"); expect(npmUpdateScripts).toContain("posixAgentWorkspaceScript"); expect(npmUpdateScripts).toContain("windowsAgentWorkspaceScript"); expect(npmUpdateScripts).toContain("tools.profile"); expect(npmUpdateScripts).toContain("--thinking off"); expect(npmUpdateScripts).toContain("finalAssistant(Raw|Visible)Text"); expect(npmUpdateScripts).toContain("posixAssertAgentOkScript"); expect(npmUpdateScripts).toContain("windowsAgentTurnConfigPatchScript"); expect(npmUpdateScripts).toContain("modelProviderConfigBatchJson"); expect(npmUpdateScripts).toContain("config set --batch-file"); }); 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"); expect(phaseRunner).toContain("clearTimeout(timer)"); expect(phaseRunner).toContain("remainingTimeoutMs"); expect(guestTransports).toContain("this.phases.remainingTimeoutMs"); for (const scriptPath of OS_TS_PATHS) { const script = readFileSync(scriptPath, "utf8"); expect(script, scriptPath).toContain("PhaseRunner"); expect(script, scriptPath).toContain("remainingPhaseTimeoutMs"); expect(script, scriptPath).toContain("timeoutMs:"); } }); it("runs POSIX guest shell scripts with a normal install umask", () => { const guestTransports = readFileSync(TS_PATHS.guestTransports, "utf8"); expect(guestTransports.match(/umask 022/g)).toHaveLength(2); }); it("provisions portable Git before Windows dev update lanes", () => { const script = readFileSync(TS_PATHS.windows, "utf8"); const windowsGit = readFileSync(TS_PATHS.windowsGit, "utf8"); const combined = `${script}\n${windowsGit}`; expect(script).toContain("prepareMinGitZip"); expect(script).toContain("ensureGuestGit"); expect(script).toContain("fresh.ensure-git"); expect(script).toContain("upgrade.ensure-git"); expect(combined).toContain("MinGit-"); expect(combined).toContain("portable-git"); expect(combined).toContain("where.exe git.exe"); expect(windowsGit.indexOf('"MinGit-2.53.0.2-64-bit.zip"')).toBeLessThan( windowsGit.indexOf('"MinGit-2.53.0.2-arm64.zip"'), ); expect(windowsGit).toContain('if "-64-bit." in name:'); expect(windowsGit).toContain('elif "-arm64." in name:'); }); it("preseeds dev update channel before stable-to-dev update lanes", () => { const macos = readFileSync(TS_PATHS.macos, "utf8"); const windows = readFileSync(TS_PATHS.windows, "utf8"); expect(macos).toContain('channel: "dev"'); expect(windows).toContain("Name channel -Value 'dev'"); expect(macos).toContain("OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS=1"); expect(windows).toContain("OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS"); }); it("passes aggregate model overrides into each OS fresh lane", () => { const script = readFileSync(TS_PATHS.npmUpdate, "utf8"); expect(script).toContain("scripts/e2e/parallels/${platform}-smoke.ts"); expect(script).toContain('"--model"'); expect(script).toContain("auth.modelId"); expect(script).toContain("authForPlatform"); expect(script).toContain("OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR"); }); it("keeps the Windows update config scrub compatible with PowerShell 5.1", () => { const script = readFileSync(TS_PATHS.npmUpdateScripts, "utf8"); expect(script).not.toContain("ConvertFrom-Json -AsHashtable"); expect(script).toContain("function Get-OpenClawJsonProperty"); expect(script).toContain("function Remove-OpenClawJsonProperty"); expect(script).toContain("Remove-OpenClawJsonProperty $entries $pluginId"); }); it("keeps aggregate update guest scripts isolated from the npm-update orchestrator", () => { const orchestrator = readFileSync(TS_PATHS.npmUpdate, "utf8"); const updateScripts = readFileSync(TS_PATHS.npmUpdateScripts, "utf8"); expect(orchestrator).toContain("macosUpdateScript"); expect(orchestrator).toContain("windowsUpdateScript"); expect(orchestrator).toContain("linuxUpdateScript"); expect(orchestrator).not.toContain("Remove-FuturePluginEntries"); expect(updateScripts).toContain("Remove-FuturePluginEntries"); expect(updateScripts).toContain("scrub_future_plugin_entries"); expect(updateScripts).toContain("Invoke-OpenClaw update"); expect(updateScripts).toContain("Parallels npm update smoke test assistant."); }); it("keeps macOS Discord roundtrip isolated from the lane orchestrator", () => { const macos = readFileSync(TS_PATHS.macos, "utf8"); const discord = readFileSync(TS_PATHS.macosDiscord, "utf8"); expect(macos).toContain("MacosDiscordSmoke"); expect(macos).not.toContain("Authorization: Bot"); expect(discord).toContain("Authorization: Bot"); expect(discord).toContain('"--silent"'); expect(discord).toContain("doctor --fix --yes --non-interactive"); expect(discord).toContain("channels status --probe --json"); expect(discord).toContain("Stop ${this.input.vmName} after successful Discord smoke"); }); it("resolves macOS smoke commands from the guest PATH", () => { const macos = readFileSync(TS_PATHS.macos, "utf8"); expect(macos).toContain("/usr/local/bin:/usr/local/sbin"); expect(macos).toContain('const guestOpenClaw = "openclaw"'); expect(macos).toContain('const guestNode = "node"'); expect(macos).toContain('const guestNpm = "npm"'); expect(macos).toContain("$(npm root -g)/openclaw/openclaw.mjs"); expect(macos).toContain("guestOpenClawEntryExec"); expect(macos).not.toContain('const guestOpenClaw = "/opt/homebrew/bin/openclaw"'); expect(macos).not.toContain('const guestNode = "/opt/homebrew/bin/node"'); expect(macos).not.toContain('const guestNpm = "/opt/homebrew/bin/npm"'); expect(macos).not.toContain("/opt/homebrew/lib/node_modules/openclaw/openclaw.mjs"); }); it("keeps Windows gateway reachability on a real deadline with start recovery", () => { const script = readFileSync(TS_PATHS.windows, "utf8"); expect(script).toContain("OPENCLAW_PARALLELS_WINDOWS_GATEWAY_RECOVERY_AFTER_S"); expect(script).toContain("Date.now() < deadline"); expect(script).toContain("gateway start"); expect(script).toContain("gateway-reachable recovery"); }); it("runs Windows ref onboarding through a detached done-file runner", () => { const script = readFileSync(TS_PATHS.windows, "utf8"); const transports = readFileSync(TS_PATHS.guestTransports, "utf8"); expect(script).toContain("guestPowerShellBackground"); expect(script).toContain("runWindowsBackgroundPowerShell"); expect(transports).toContain("Join-Path $env:TEMP"); expect(transports).toContain("__OPENCLAW_BACKGROUND_DONE__"); expect(transports).toContain("__OPENCLAW_BACKGROUND_EXIT__"); expect(transports).toContain("__OPENCLAW_LOG_OFFSET__"); expect(transports).toContain("poll.status !== 0 && poll.status !== 124"); expect(transports).toContain("Start-Process -FilePath powershell.exe"); expect(transports).toContain('launch.stdout.includes("started")'); expect(transports).toContain("waitForWindowsBackgroundMaterialized"); }); it("returns timed-out host command status when check is disabled", () => { const result = run( process.execPath, ["-e", "process.stdout.write('partial'); setTimeout(() => {}, 1000);"], { check: false, quiet: true, timeoutMs: 50, }, ); expect(result.status).toBe(124); expect(result.stdout).toBeTypeOf("string"); }); it("does not wait for host commands that trap SIGTERM after a timeout", () => { const startedAt = Date.now(); const result = run( process.execPath, [ "-e", [ "process.on('SIGTERM', () => {});", "setTimeout(() => process.exit(77), 700);", "setInterval(() => {}, 1000);", ].join(""), ], { check: false, quiet: true, timeoutMs: 50, }, ); expect(result.status).toBe(124); expect(Date.now() - startedAt).toBeLessThan(500); }); it("routes Windows host pnpm and npm shims through safe runners", () => { const comSpec = "C:\\Windows\\System32\\cmd.exe"; expect( resolveHostCommandInvocation("pnpm", ["build"], { env: { ComSpec: comSpec, npm_execpath: "C:\\Tools\\pnpm.cmd", }, platform: "win32", }), ).toEqual({ args: ["/d", "/s", "/c", "C:\\Tools\\pnpm.cmd build"], command: comSpec, shell: false, windowsVerbatimArguments: true, }); const execPath = "C:\\nodejs\\node.exe"; const npmCmdPath = win32.resolve(win32.dirname(execPath), "npm.cmd"); expect( resolveHostCommandInvocation("npm", ["view", "openclaw", "version"], { env: { ComSpec: comSpec }, execPath, existsSync: (candidate) => candidate === npmCmdPath, platform: "win32", }), ).toEqual({ args: ["/d", "/s", "/c", `${npmCmdPath} view openclaw version`], command: comSpec, shell: false, windowsVerbatimArguments: true, }); }); it("wraps explicit Windows batch host commands without shell mode", () => { expect( resolveHostCommandInvocation("C:\\Tools\\helper.cmd", ["@scope/pkg@^1.0.0"], { comSpec: "cmd.exe", platform: "win32", }), ).toEqual({ args: ["/d", "/s", "/c", "C:\\Tools\\helper.cmd @scope/pkg@^^1.0.0"], command: "cmd.exe", shell: false, windowsVerbatimArguments: true, }); }); it("runs the Windows agent turn through the detached done-file runner", () => { const script = readFileSync(TS_PATHS.windows, "utf8"); expect(script).toContain('guestPowerShellBackground(\n "agent-turn"'); expect(script).toContain("OPENCLAW_PARALLELS_WINDOWS_AGENT_TIMEOUT_S"); expect(script).toContain("OPENCLAW_PARALLELS_WINDOWS_AGENT_TIMEOUT_S || 2700"); expect(script).toContain("windowsAgentTurnConfigPatchScript(this.auth.modelId)"); expect(script).toContain("--model"); expect(script).toContain('resolveParallelsModelTimeoutSeconds("windows")'); expect(script).toContain("finalAssistant(Raw|Visible)Text"); expect(script).toContain("parallels-windows-smoke-retry-$attempt"); expect(script).toContain("agent turn attempt $attempt failed or finished without OK response"); expect(script).not.toContain("$config.models.providers"); expect(script).not.toContain("timeoutSeconds = 300"); expect(script).not.toContain("$sessionId.jsonl"); }); it("gives GPT-5.5 enough Parallels model time on slower desktop guests", () => { expect({ linux: resolveParallelsModelTimeoutSeconds("linux"), macos: resolveParallelsModelTimeoutSeconds("macos"), windows: resolveParallelsModelTimeoutSeconds("windows"), }).toEqual({ linux: 900, macos: 1800, windows: 1800, }); expect(readFileSync(TS_PATHS.macos, "utf8")).toContain( "OPENCLAW_PARALLELS_MACOS_AGENT_TIMEOUT_S || 2700", ); expect(readFileSync(TS_PATHS.macos, "utf8")).toContain( '--timeout ${resolveParallelsModelTimeoutSeconds("macos")}', ); expect(readFileSync(TS_PATHS.linux, "utf8")).toContain( '--timeout ${resolveParallelsModelTimeoutSeconds("linux")}', ); }); it("waits through transient Windows restoring state before VM operations", () => { const script = readFileSync(TS_PATHS.windows, "utf8"); const transports = readFileSync(TS_PATHS.guestTransports, "utf8"); expect(script).toContain("waitForVmNotRestoring"); expect(script).toContain("snapshot-switch retry"); expect(transports).toContain("launch retry"); }); it("keeps Windows update-only env flags scoped before verification", () => { const windows = readFileSync(TS_PATHS.windows, "utf8"); const powershell = readFileSync(TS_PATHS.powershell, "utf8"); expect(powershell).toContain("windowsScopedEnvFunction"); expect(windows).toContain( "Invoke-WithScopedEnv @{ OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS", ); expect(windows).toContain("$script:OpenClawUpdateExit = $LASTEXITCODE"); expect(windows).not.toContain("$env:OPENCLAW_DISABLE_BUNDLED_PLUGINS = '1'"); }); it("writes Parallels phase timing artifacts", () => { const phaseRunner = readFileSync(TS_PATHS.phaseRunner, "utf8"); const npmUpdate = readFileSync(TS_PATHS.npmUpdate, "utf8"); expect(phaseRunner).toContain("phase-timings.json"); expect(phaseRunner).toContain("slowest"); expect(npmUpdate).toContain("timings: this.timings"); expect(npmUpdate).toContain("recordTiming"); }); it("resolves Windows OpenClaw commands without assuming the npm shim path", () => { const powershell = readFileSync(TS_PATHS.powershell, "utf8"); const windows = readFileSync(TS_PATHS.windows, "utf8"); expect(powershell).toContain("windowsOpenClawResolver"); expect(powershell).toContain("providerTimeoutConfigJson"); expect(powershell).toContain("models.providers.${providerId}"); expect(powershell).toContain("agents.defaults.models${configPathMapKey(modelId)}"); expect(powershell).toContain("OPENCLAW_PARALLELS_AGENT_RUNTIME_POLICY_SUPPORTED"); expect(powershell).toContain('selectedModelEntry.agentRuntime = { id: "openclaw" }'); expect(powershell).toContain("delete selectedModelEntry.agentRuntime"); expect(powershell).toContain("delete providerEntry.agentRuntime"); expect(powershell).toContain("configPathMapKey"); expect(powershell).toContain('transport: "sse"'); expect(powershell).toContain("Resolve-OpenClawCommand"); expect(powershell).toContain("npm\\node_modules\\openclaw\\openclaw.mjs"); expect(powershell).toContain("$ErrorActionPreference = 'Continue'"); expect(powershell).toContain("$PSNativeCommandUseErrorActionPreference = $false"); expect(windows).toContain("windowsOpenClawResolver"); expect(windows).toContain("Invoke-OpenClaw gateway"); expect(windows).not.toContain("Join-Path $env:APPDATA 'npm\\\\openclaw.cmd'"); }); });