mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:00:42 +00:00
refactor: convert parallels smoke scripts to typescript
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const SCRIPT_PATH = "scripts/e2e/parallels-npm-update-smoke.sh";
|
||||
const SCRIPT_PATH = "scripts/e2e/parallels/npm-update-smoke.ts";
|
||||
|
||||
describe("parallels npm update smoke", () => {
|
||||
it("does not leave guard/server children attached to the wrapper", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
expect(script).toContain('wait "$SERVER_PID" 2>/dev/null || true');
|
||||
expect(script).toContain(") >&2 &");
|
||||
expect(script).toContain('wait "$pid" 2>/dev/null || true');
|
||||
expect(script).toContain("spawnLogged");
|
||||
expect(script).toContain('child.on("close"');
|
||||
expect(script).toContain("await this.server?.stop()");
|
||||
});
|
||||
|
||||
it("scrubs future plugin entries before invoking old same-guest updaters", () => {
|
||||
@@ -17,15 +17,15 @@ describe("parallels npm update smoke", () => {
|
||||
|
||||
expect(script).toContain("Remove-FuturePluginEntries");
|
||||
expect(script).toContain("scrub_future_plugin_entries");
|
||||
expect(script).toContain("delete entries.feishu");
|
||||
expect(script).toContain("delete entries.whatsapp");
|
||||
expect(script).toContain("Remove-FuturePluginEntries\n Stop-OpenClawGatewayProcesses");
|
||||
expect(script).toContain("delete plugins.entries.feishu");
|
||||
expect(script).toContain("delete plugins.entries.whatsapp");
|
||||
expect(script).toContain("Remove-FuturePluginEntries\nStop-OpenClawGatewayProcesses");
|
||||
expect(script).toContain("scrub_future_plugin_entries\nstop_openclaw_gateway_processes");
|
||||
expect(script).toContain("$env:OPENCLAW_DISABLE_BUNDLED_PLUGINS = '1'");
|
||||
expect(script).toContain(
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw update",
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw update --tag",
|
||||
);
|
||||
expect(script).toContain("OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw update");
|
||||
expect(script).toContain("OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw update --tag");
|
||||
expect(script).toContain(
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw gateway stop",
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user