refactor: convert parallels smoke scripts to typescript

This commit is contained in:
Peter Steinberger
2026-04-29 12:52:42 +01:00
parent 016f5ae862
commit 39f810911c
23 changed files with 4829 additions and 8038 deletions

View File

@@ -1,61 +1,288 @@
import { readFileSync } from "node:fs";
import { execFileSync, spawnSync } from "node:child_process";
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const OS_SCRIPT_PATHS = [
"scripts/e2e/parallels-linux-smoke.sh",
"scripts/e2e/parallels-macos-smoke.sh",
"scripts/e2e/parallels-windows-smoke.sh",
];
const NPM_UPDATE_SCRIPT_PATH = "scripts/e2e/parallels-npm-update-smoke.sh";
const PARALLELS_PACKAGE_COMMON_PATH = "scripts/e2e/lib/parallels-package-common.sh";
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",
linux: "scripts/e2e/parallels/linux-smoke.ts",
macos: "scripts/e2e/parallels/macos-smoke.ts",
npmUpdate: "scripts/e2e/parallels/npm-update-smoke.ts",
packageArtifact: "scripts/e2e/parallels/package-artifact.ts",
phaseRunner: "scripts/e2e/parallels/phase-runner.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 runTsEval(source: string, env: Record<string, string> = {}) {
return execFileSync("node", ["--import", "tsx", "--input-type=module", "--eval", source], {
encoding: "utf8",
env: { ...process.env, ...env },
});
}
function resolveProviderAuth(
provider: string,
options: {
apiKeyEnv?: string;
env?: Record<string, string>;
modelId?: string;
} = {},
) {
const source = `
import { resolveProviderAuth } from "./${TS_PATHS.common}";
const result = resolveProviderAuth({
provider: ${JSON.stringify(provider)},
apiKeyEnv: ${JSON.stringify(options.apiKeyEnv)},
modelId: ${JSON.stringify(options.modelId)},
});
console.log(JSON.stringify(result));
`;
return JSON.parse(runTsEval(source, options.env)) as {
apiKeyEnv: string;
apiKeyValue: string;
authChoice: string;
authKeyFlag: string;
modelId: string;
};
}
describe("Parallels smoke model selection", () => {
it("keeps the OpenAI smoke lane on the stable direct API model by default", () => {
for (const scriptPath of [...OS_SCRIPT_PATHS, NPM_UPDATE_SCRIPT_PATH]) {
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(wrapper.split("\n").filter(Boolean).length).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("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).toContain(
'MODEL_ID="${OPENCLAW_PARALLELS_OPENAI_MODEL:-openai/gpt-5.5}"',
);
expect(script, scriptPath).toContain("resolveProviderAuth");
expect(script, scriptPath).toContain("--model <provider/model>");
expect(script, scriptPath).toContain("MODEL_ID_EXPLICIT=1");
expect(script, scriptPath).toContain("modelId");
}
});
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 packageArtifact = readFileSync(TS_PATHS.packageArtifact, "utf8");
const snapshots = readFileSync(TS_PATHS.snapshots, "utf8");
expect(common).toContain('export * from "./host-command.ts"');
expect(common).toContain('export * from "./package-artifact.ts"');
expect(common).toContain('export * from "./snapshots.ts"');
expect(hostCommand).toContain("export function shellQuote");
expect(packageArtifact).toContain("export async function packageVersionFromTgz");
expect(packageArtifact).toContain("export async function packOpenClaw");
expect(hostServer).toContain("export async function startHostServer");
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).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-"));
const prlctlPath = join(tempDir, "prlctl");
writeFileSync(
prlctlPath,
`#!/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
`,
);
chmodSync(prlctlPath, 0o755);
try {
const output = runTsEval(
`
import { resolveSnapshot, shellQuote } from "./${TS_PATHS.common}";
console.log(shellQuote("it's ok"));
const snapshot = resolveSnapshot("vm", "fresh");
console.log([snapshot.id, snapshot.state, snapshot.name].join("\\t"));
`,
{ PATH: `${tempDir}:${process.env.PATH ?? ""}` },
);
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 provider defaults and explicit model overrides", () => {
expect(resolveProviderAuth("openai", { env: { OPENAI_API_KEY: "sk-openai" } })).toEqual({
apiKeyEnv: "OPENAI_API_KEY",
apiKeyValue: "sk-openai",
authChoice: "openai-api-key",
authKeyFlag: "openai-api-key",
modelId: "openai/gpt-5.5",
});
expect(
resolveProviderAuth("anthropic", {
apiKeyEnv: "CUSTOM_ANTHROPIC_KEY",
env: { CUSTOM_ANTHROPIC_KEY: "sk-anthropic" },
modelId: "anthropic/custom",
}),
).toEqual({
apiKeyEnv: "CUSTOM_ANTHROPIC_KEY",
apiKeyValue: "sk-anthropic",
authChoice: "apiKey",
authKeyFlag: "anthropic-api-key",
modelId: "anthropic/custom",
});
});
it("rejects invalid providers and missing keys before touching guests", () => {
const invalidProvider = spawnSync(
"node",
[
"--import",
"tsx",
"--input-type=module",
"--eval",
`import { parseProvider } from "./${TS_PATHS.common}"; parseProvider("bogus");`,
],
{ encoding: "utf8", env: process.env },
);
expect(invalidProvider.status).toBe(1);
expect(invalidProvider.stderr).toContain("invalid --provider: bogus");
const missingKey = spawnSync(
"node",
[
"--import",
"tsx",
"--input-type=module",
"--eval",
`import { resolveProviderAuth } from "./${TS_PATHS.common}"; resolveProviderAuth({ provider: "openai", apiKeyEnv: "PARALLELS_TEST_MISSING_KEY" });`,
],
{
encoding: "utf8",
env: { ...process.env, PARALLELS_TEST_MISSING_KEY: "" },
},
);
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 seedHelper = readFileSync(PARALLELS_PACKAGE_COMMON_PATH, "utf8");
expect(seedHelper, PARALLELS_PACKAGE_COMMON_PATH).toContain("workspace-state.json");
expect(seedHelper, PARALLELS_PACKAGE_COMMON_PATH).toContain("IDENTITY.md");
expect(seedHelper, PARALLELS_PACKAGE_COMMON_PATH).toContain("BOOTSTRAP.md");
const workspace = readFileSync(TS_PATHS.agentWorkspace, "utf8");
for (const scriptPath of OS_SCRIPT_PATHS) {
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");
const expectedSeedHelper = scriptPath.includes("windows")
? "parallels_powershell_seed_workspace_snippet"
: "parallels_bash_seed_workspace_snippet";
expect(script, scriptPath).toContain(expectedSeedHelper);
expect(script, scriptPath).toMatch(/--session-id\s+['"]?parallels-/);
expect(script, scriptPath).toContain("agents.defaults.skipBootstrap true --strict-json");
expect(script, scriptPath).toContain("AgentWorkspaceScript");
expect(script, scriptPath).toContain("parallels-");
expect(script, scriptPath).toContain("agents.defaults.skipBootstrap");
}
const npmUpdate = readFileSync(TS_PATHS.npmUpdate, "utf8");
expect(npmUpdate).toContain("posixAgentWorkspaceScript");
expect(npmUpdate).toContain("windowsAgentWorkspaceScript");
});
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("passes aggregate model overrides into each OS fresh lane", () => {
const script = readFileSync(NPM_UPDATE_SCRIPT_PATH, "utf8");
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).toMatch(/parallels-macos-smoke\.sh"[\s\S]*?--model "\$MODEL_ID"/);
expect(script).toMatch(/parallels-windows-smoke\.sh"[\s\S]*?--model "\$MODEL_ID"/);
expect(script).toMatch(/parallels-linux-smoke\.sh"[\s\S]*?--model "\$MODEL_ID"/);
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");
});
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("this.auth.modelId");
expect(script).toContain("OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR");
});
it("keeps Windows gateway reachability on a real deadline with start recovery", () => {
const script = readFileSync("scripts/e2e/parallels-windows-smoke.sh", "utf8");
const script = readFileSync(TS_PATHS.windows, "utf8");
expect(script).toContain(
'GATEWAY_RECOVERY_AFTER_S="${OPENCLAW_PARALLELS_WINDOWS_GATEWAY_RECOVERY_AFTER_S:-180}"',
);
expect(script).toContain("deadline=$((SECONDS + TIMEOUT_GATEWAY_S))");
expect(script).toContain("while (( SECONDS < deadline )); do");
expect(script).toContain("run_gateway_daemon_action start");
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");
});
});