Skip to content

Commit 39f8109

Browse files
committed
refactor: convert parallels smoke scripts to typescript
1 parent 016f5ae commit 39f8109

23 files changed

Lines changed: 4828 additions & 8037 deletions

scripts/e2e/parallels-linux-smoke.sh

100644100755
Lines changed: 1 addition & 1064 deletions
Large diffs are not rendered by default.

scripts/e2e/parallels-macos-smoke.sh

100644100755
Lines changed: 1 addition & 2125 deletions
Large diffs are not rendered by default.

scripts/e2e/parallels-npm-update-smoke.sh

Lines changed: 1 addition & 2022 deletions
Large diffs are not rendered by default.

scripts/e2e/parallels-windows-smoke.sh

100644100755
Lines changed: 1 addition & 2781 deletions
Large diffs are not rendered by default.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export function posixAgentWorkspaceScript(purpose: string): string {
2+
return `set -eu
3+
workspace="\${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
4+
mkdir -p "$workspace/.openclaw"
5+
cat > "$workspace/IDENTITY.md" <<'IDENTITY_EOF'
6+
# Identity
7+
8+
- Name: OpenClaw
9+
- Purpose: ${purpose}
10+
IDENTITY_EOF
11+
cat > "$workspace/.openclaw/workspace-state.json" <<'STATE_EOF'
12+
{
13+
"version": 1,
14+
"setupCompletedAt": "2026-01-01T00:00:00.000Z"
15+
}
16+
STATE_EOF
17+
rm -f "$workspace/BOOTSTRAP.md"`;
18+
}
19+
20+
export function windowsAgentWorkspaceScript(purpose: string): string {
21+
return `$workspace = $env:OPENCLAW_WORKSPACE_DIR
22+
if (-not $workspace) { $workspace = Join-Path $env:USERPROFILE '.openclaw\\workspace' }
23+
$stateDir = Join-Path $workspace '.openclaw'
24+
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
25+
@'
26+
# Identity
27+
28+
- Name: OpenClaw
29+
- Purpose: ${purpose}
30+
'@ | Set-Content -Path (Join-Path $workspace 'IDENTITY.md') -Encoding UTF8
31+
@'
32+
{
33+
"version": 1,
34+
"setupCompletedAt": "2026-01-01T00:00:00.000Z"
35+
}
36+
'@ | Set-Content -Path (Join-Path $stateDir 'workspace-state.json') -Encoding UTF8
37+
Remove-Item (Join-Path $workspace 'BOOTSTRAP.md') -Force -ErrorAction SilentlyContinue`;
38+
}

scripts/e2e/parallels/common.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export * from "./filesystem.ts";
2+
export * from "./host-command.ts";
3+
export * from "./host-server.ts";
4+
export * from "./package-artifact.ts";
5+
export * from "./provider-auth.ts";
6+
export * from "./snapshots.ts";
7+
export * from "./types.ts";
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { writeFileSync, rmSync } from "node:fs";
2+
import { mkdtempSync } from "node:fs";
3+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4+
import { tmpdir } from "node:os";
5+
import path from "node:path";
6+
7+
export async function exists(filePath: string): Promise<boolean> {
8+
try {
9+
await access(filePath);
10+
return true;
11+
} catch {
12+
return false;
13+
}
14+
}
15+
16+
export async function readJson<T>(filePath: string): Promise<T> {
17+
return JSON.parse(await readFile(filePath, "utf8")) as T;
18+
}
19+
20+
export async function writeJson(filePath: string, value: unknown): Promise<void> {
21+
await mkdir(path.dirname(filePath), { recursive: true });
22+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
23+
}
24+
25+
export async function makeTempDir(prefix: string): Promise<string> {
26+
return mkdtempSync(path.join(tmpdir(), prefix));
27+
}
28+
29+
export async function cleanupPath(filePath: string): Promise<void> {
30+
await rm(filePath, { force: true, recursive: true }).catch(() => undefined);
31+
}
32+
33+
export function cleanupPathSync(filePath: string): void {
34+
rmSync(filePath, { force: true, recursive: true });
35+
}
36+
37+
export function writeExecutable(filePath: string, content: string): void {
38+
writeFileSync(filePath, content, { encoding: "utf8", mode: 0o755 });
39+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { run } from "./host-command.ts";
2+
import type { PhaseRunner } from "./phase-runner.ts";
3+
import { encodePowerShell } from "./powershell.ts";
4+
5+
export interface GuestExecOptions {
6+
check?: boolean;
7+
timeoutMs?: number;
8+
}
9+
10+
export class LinuxGuest {
11+
constructor(
12+
private vmName: string,
13+
private phases: PhaseRunner,
14+
) {}
15+
16+
exec(args: string[], options: GuestExecOptions = {}): string {
17+
const result = run("prlctl", ["exec", this.vmName, "/usr/bin/env", "HOME=/root", ...args], {
18+
check: options.check,
19+
quiet: true,
20+
timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
21+
});
22+
this.phases.append(result.stdout);
23+
this.phases.append(result.stderr);
24+
return result.stdout.trim();
25+
}
26+
27+
bash(script: string): string {
28+
const encoded = Buffer.from(script, "utf8").toString("base64");
29+
return this.exec(["bash", "-lc", `printf '%s' '${encoded}' | base64 -d | bash`]);
30+
}
31+
}
32+
33+
export interface MacosGuestOptions extends GuestExecOptions {
34+
env?: Record<string, string>;
35+
}
36+
37+
export class MacosGuest {
38+
constructor(
39+
private input: {
40+
vmName: string;
41+
getUser: () => string;
42+
getTransport: () => "current-user" | "sudo";
43+
resolveDesktopHome: (user: string) => string;
44+
path: string;
45+
},
46+
private phases: PhaseRunner,
47+
) {}
48+
49+
exec(args: string[], options: MacosGuestOptions = {}): string {
50+
const envArgs = Object.entries({ PATH: this.input.path, ...options.env }).map(
51+
([key, value]) => `${key}=${value}`,
52+
);
53+
const user = this.input.getUser();
54+
const transportArgs =
55+
this.input.getTransport() === "sudo"
56+
? [
57+
"exec",
58+
this.input.vmName,
59+
"/usr/bin/sudo",
60+
"-H",
61+
"-u",
62+
user,
63+
"/usr/bin/env",
64+
`HOME=${this.input.resolveDesktopHome(user)}`,
65+
`USER=${user}`,
66+
`LOGNAME=${user}`,
67+
...envArgs,
68+
...args,
69+
]
70+
: ["exec", this.input.vmName, "--current-user", "/usr/bin/env", ...envArgs, ...args];
71+
const result = run("prlctl", transportArgs, {
72+
check: options.check,
73+
quiet: true,
74+
timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
75+
});
76+
this.phases.append(result.stdout);
77+
this.phases.append(result.stderr);
78+
return result.stdout.trim();
79+
}
80+
81+
sh(script: string, env: Record<string, string> = {}): string {
82+
return this.exec(["/bin/bash", "-lc", script], { env });
83+
}
84+
}
85+
86+
export class WindowsGuest {
87+
constructor(
88+
private vmName: string,
89+
private phases: PhaseRunner,
90+
) {}
91+
92+
exec(args: string[], options: GuestExecOptions = {}): string {
93+
const result = run("prlctl", ["exec", this.vmName, "--current-user", ...args], {
94+
check: options.check,
95+
quiet: true,
96+
timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
97+
});
98+
this.phases.append(result.stdout);
99+
this.phases.append(result.stderr);
100+
return result.stdout.trim();
101+
}
102+
103+
powershell(script: string, options: GuestExecOptions = {}): string {
104+
return this.exec(
105+
[
106+
"powershell.exe",
107+
"-NoProfile",
108+
"-ExecutionPolicy",
109+
"Bypass",
110+
"-EncodedCommand",
111+
encodePowerShell(script),
112+
],
113+
options,
114+
);
115+
}
116+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { spawn, spawnSync, type SpawnOptions } from "node:child_process";
2+
import { writeFile } from "node:fs/promises";
3+
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
import type { CommandResult, RunOptions } from "./types.ts";
6+
7+
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
8+
9+
export function say(message: string): void {
10+
process.stdout.write(`==> ${message}\n`);
11+
}
12+
13+
export function warn(message: string): void {
14+
process.stderr.write(`warn: ${message}\n`);
15+
}
16+
17+
export function die(message: string): never {
18+
process.stderr.write(`error: ${message}\n`);
19+
process.exit(1);
20+
}
21+
22+
export function shellQuote(value: string): string {
23+
return `'${value.replaceAll("'", `'"'"'`)}'`;
24+
}
25+
26+
export function run(command: string, args: string[], options: RunOptions = {}): CommandResult {
27+
const result = spawnSync(command, args, {
28+
cwd: options.cwd ?? repoRoot,
29+
encoding: "utf8",
30+
env: { ...process.env, ...options.env },
31+
input: options.input,
32+
maxBuffer: 50 * 1024 * 1024,
33+
stdio: options.quiet ? ["pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
34+
timeout: options.timeoutMs,
35+
});
36+
37+
if (result.error) {
38+
throw result.error;
39+
}
40+
41+
const status = result.status ?? (result.signal ? 128 : 1);
42+
const commandResult = {
43+
stderr: result.stderr ?? "",
44+
stdout: result.stdout ?? "",
45+
status,
46+
};
47+
if (options.check !== false && status !== 0) {
48+
if (commandResult.stdout) {
49+
process.stdout.write(commandResult.stdout);
50+
}
51+
if (commandResult.stderr) {
52+
process.stderr.write(commandResult.stderr);
53+
}
54+
die(`command failed (${status}): ${[command, ...args].join(" ")}`);
55+
}
56+
return commandResult;
57+
}
58+
59+
export function sh(script: string, options: RunOptions = {}): CommandResult {
60+
return run("bash", ["-lc", script], options);
61+
}
62+
63+
export async function runStreaming(
64+
command: string,
65+
args: string[],
66+
options: RunOptions & { logPath?: string } = {},
67+
): Promise<number> {
68+
return await new Promise((resolve, reject) => {
69+
const child = spawn(command, args, {
70+
cwd: options.cwd ?? repoRoot,
71+
env: { ...process.env, ...options.env },
72+
stdio: ["pipe", "pipe", "pipe"],
73+
} satisfies SpawnOptions);
74+
75+
let log = "";
76+
const append = (chunk: Buffer): void => {
77+
const text = chunk.toString("utf8");
78+
log += text;
79+
if (!options.quiet) {
80+
process.stdout.write(text);
81+
}
82+
};
83+
child.stdout?.on("data", append);
84+
child.stderr?.on("data", (chunk: Buffer) => {
85+
const text = chunk.toString("utf8");
86+
log += text;
87+
if (!options.quiet) {
88+
process.stderr.write(text);
89+
}
90+
});
91+
if (options.input != null) {
92+
child.stdin?.end(options.input);
93+
} else {
94+
child.stdin?.end();
95+
}
96+
97+
let timedOut = false;
98+
const timer =
99+
options.timeoutMs == null
100+
? undefined
101+
: setTimeout(() => {
102+
timedOut = true;
103+
child.kill("SIGTERM");
104+
setTimeout(() => child.kill("SIGKILL"), 2_000).unref();
105+
}, options.timeoutMs);
106+
107+
child.on("error", reject);
108+
child.on("close", async (code, signal) => {
109+
if (timer) {
110+
clearTimeout(timer);
111+
}
112+
if (options.logPath) {
113+
await writeFile(options.logPath, log, "utf8");
114+
}
115+
if (timedOut) {
116+
resolve(124);
117+
} else {
118+
resolve(code ?? (signal ? 128 : 1));
119+
}
120+
});
121+
});
122+
}

0 commit comments

Comments
 (0)