Files
openclaw/test/scripts/parallels-smoke-model.test.ts
Peter Steinberger bb46b79d3c refactor: internalize OpenClaw agent runtime (#85341)
* refactor: extract agent core package

Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts.

* refactor: extract shared llm runtime

Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout.

* refactor: remove pi runtime internals

Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code.

* refactor: tighten agent session runtime

Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts.

* refactor: remove static model and pi auth paths

Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities.

* refactor: remove legacy provider compat paths

* docs: remove agent parity notes

* fix: skip provider wildcard metadata parsing

* refactor: share session extension sdk loading

* refactor: inline acpx proxy error formatter

* refactor: fold edit recovery into edit tool

* fix: accept extension batch separator

* test: align startup provider plugin expectations

* fix: restore provider-scoped release discovery

* test: align static asset packaging expectations

* fix: run static provider catalogs during scoped discovery

* fix: add provider entry catalogs for scoped live discovery

* fix: load lightweight provider catalog entries

* fix: refresh provider-scoped plugin metadata

* fix: keep provider catalog entries on release live path

* fix: keep static manifest models in release live checks

* fix: harden release model discovery

* fix: reduce OpenAI live cache probe reasoning

* fix: disable OpenAI cache probe reasoning

* ci: extend OpenAI gateway live timeout

* fix: extend live gateway model budget

* fix: stabilize release validation regressions

* fix: honor provider aliases in model rows

* fix: stabilize release validation lanes

* fix: stabilize release memory qa

* ci: stabilize release validation lanes

* ci: prefer ipv4 for live docker node calls

* fix: restore shared tool-call stream wrapper

* ci: remove legacy pi test shard alias

* fix: clean up embedded agent test drift

* fix: stabilize runtime alias status

* fix: clean up embedded agent ci drift

* fix: restore release ci invariants

* fix: clean up post-rebase runtime drift

* fix: restore release ci checks

* fix: restore release ci after rebase

* fix: remove stale pi runtime path

* test: align compaction runtime expectations

* test: update plugin prerelease expectations

* fix: handle claude live tool approvals

* fix: stabilize release validation gates

* fix: finish agent runtime import

* test: finish post-rebase agent runtime mocks

* fix: keep codex compaction native

* fix: stabilize codex app-server hook tests

* test: isolate codex diagnostic active run

* test: remove codex diagnostic completion race

# Conflicts:
#	extensions/codex/src/app-server/run-attempt.test.ts

* ci: fix full release manifest performance run id

* refactor: narrow llm plugin sdk boundary

* chore: drop generated google boundary stamps

* fix: repair rebase fallout

* fix: clean up rebased runtime references

* fix: decode codex jwt payloads as base64url

* fix: preserve shipped pi runtime alias

* fix: add scoped sdk virtual modules

* fix: decode llm codex oauth jwt as base64url

* fix: avoid stale vertex adc negative cache

* fix: harden tool arg decoding and codeql path

* fix: keep vertex adc negative checks live

* refactor: consolidate codex jwt and edit helpers

* fix: await codex oauth node runtime imports

* fix: preserve sdk tool and notice contracts

* fix: preserve shipped compat config boundaries

* fix: align codex oauth callback host

* fix: terminate agent-core loop streams on failure

* fix: keep codex oauth callback alive during fallback

* ci: include session tools in critical codeql scans

* fix: keep Cloudflare Anthropic provider auth header

* docs: redirect legacy pi runtime pages

* fix: honor bundled web provider compat discovery

* fix: protect session output spill files

* fix: keep legacy agent dir env blocked

* fix: contain auto-discovered skill symlinks

* fix: harden agent core sdk proxy surfaces

* fix: restore approval reaction sdk compat

* fix: keep live docker runs bounded

* fix: keep codex oauth redirect host aligned

* fix: resolve post-rebase agent runtime drift

* fix: redact anthropic oauth parse failures

* fix: preserve responses strict tool shaping

* fix: repair agent runtime rebase cleanup

* docs: redirect retired parity pages

* fix: bound auto-discovered resources to roots

* fix: repair post-rebase agent test drift

* fix: preserve bundled provider allowlist migration

* fix: preserve manifest-owned provider aliases

* fix: declare photon image dependency

* fix: keep provider headers out of proxy body

* fix: preserve shipped env aliases

* fix: refresh control ui i18n generated state

* fix: quote read fallback paths

* fix: preview edits through configured backend

* test: satisfy core test typecheck

* fix: preserve ZAI usage auth fallback

* test: repair codex diagnostic test

* fix: repair agent runtime rebase drift

* test: finish embedded runner import rename

* fix: repair agent runtime rebase integrations

* test: align compaction oauth fallback expectations

* fix: allow sdk-auth session models

* fix: update doctor tool schema import

* fix: preserve bedrock plugin region

* fix: stream harmony-like prose immediately

* ci: include session runtime in codeql shards

* fix: repair latest rebase integrations

* fix: honor explicit codex websocket transport

* fix: keep openai-compatible credentials provider-scoped

* fix: refresh sdk api baseline after rebase

* fix: route cli runtime aliases through openclaw harness

* test: rename stale harness mock expectation

* test: rename embedded agent overflow calls

* test: clean embedded auth test wording

* test: use openclaw stream types in deepinfra cache test

* fix: refresh sdk api baseline on latest main

* fix: honor bundled discovery compat allowlists

* fix: refresh sdk api baseline after latest rebase

* fix: remove stale rebase imports

* test: rename stale model catalog mock

* test: mock renamed doctor runtime modules

* fix: map canonical kimi env auth

* fix: use internal model registry in bench script

* fix: migrate deepinfra provider catalog entry

* fix: enforce builtin tool suppression

* fix: route compaction auth and proxy payloads safely

* refactor: prune unused llm registry leftovers

* test: update codex hooks session import

* test: fix model picker ci coverage

* test: align model picker auth mock types
2026-05-27 19:24:04 +01:00

775 lines
30 KiB
TypeScript

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<string, string> {
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<T>(env: Record<string, string>, callback: () => T): T {
const previous = new Map<string, string | undefined>();
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 <provider/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 agent workspace state before OS smoke agent turns", () => {
const workspace = readFileSync(TS_PATHS.agentWorkspace, "utf8");
expect(workspace).toContain("workspace-state.json");
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).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'");
});
});