Skip to content

Commit dfeb5b8

Browse files
committed
fix(e2e): harden Parallels helper cleanup
1 parent d9f6e03 commit dfeb5b8

3 files changed

Lines changed: 150 additions & 24 deletions

File tree

scripts/e2e/parallels/host-server.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,54 @@ export async function startHostServer(input: {
8181
hostIp: input.hostIp,
8282
port: actualPort,
8383
stop: async () => {
84-
child.kill("SIGTERM");
85-
await new Promise<void>((resolve) => {
86-
child.once("exit", () => resolve());
87-
setTimeout(() => {
88-
child.kill("SIGKILL");
89-
resolve();
90-
}, 2_000).unref();
91-
});
84+
await stopHostServerChild(child);
9285
},
9386
urlFor: (filePath) =>
9487
`http://${input.hostIp}:${actualPort}/${encodeURIComponent(path.basename(filePath))}`,
9588
};
9689
}
9790

91+
async function stopHostServerChild(
92+
child: ChildProcessWithoutNullStreams,
93+
terminateTimeoutMs = 2_000,
94+
killTimeoutMs = 1_500,
95+
): Promise<boolean> {
96+
if (child.exitCode != null) {
97+
return true;
98+
}
99+
child.kill("SIGTERM");
100+
if (await waitForChildExit(child, terminateTimeoutMs)) {
101+
return true;
102+
}
103+
child.kill("SIGKILL");
104+
return await waitForChildExit(child, killTimeoutMs);
105+
}
106+
107+
async function waitForChildExit(
108+
child: ChildProcessWithoutNullStreams,
109+
timeoutMs: number,
110+
): Promise<boolean> {
111+
if (child.exitCode != null) {
112+
return true;
113+
}
114+
return await new Promise<boolean>((resolve) => {
115+
let settled = false;
116+
const onExit = () => settle(true);
117+
const timeout = setTimeout(() => settle(child.exitCode != null), timeoutMs);
118+
timeout.unref();
119+
function settle(exited: boolean): void {
120+
if (settled) {
121+
return;
122+
}
123+
settled = true;
124+
clearTimeout(timeout);
125+
child.off("exit", onExit);
126+
resolve(exited);
127+
}
128+
child.once("exit", onExit);
129+
});
130+
}
131+
98132
async function waitForHostServer(
99133
child: ChildProcessWithoutNullStreams,
100134
port: number,
@@ -160,4 +194,5 @@ async function delay(ms: number): Promise<void> {
160194

161195
export const testing = {
162196
appendBoundedOutput,
197+
stopHostServerChild,
163198
};

scripts/e2e/parallels/provider-auth.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import { mkdtempSync } from "node:fs";
1+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import path from "node:path";
44
import { parsePositiveInt, readPositiveIntEnv } from "./env-limits.ts";
55
import { die, run } from "./host-command.ts";
66
import type { Mode, Platform, Provider, ProviderAuth } from "./types.ts";
77

8+
type ResolveLatestVersionDeps = {
9+
createTempDir?: typeof mkdtempSync;
10+
removeDir?: typeof rmSync;
11+
runCommand?: typeof run;
12+
tempDir?: typeof tmpdir;
13+
writeFile?: typeof writeFileSync;
14+
};
15+
816
export function parseBoolEnv(value: string | undefined): boolean {
917
return /^(1|true|yes|on)$/i.test(value ?? "");
1018
}
@@ -192,21 +200,26 @@ export function parsePlatformList(value: string): Set<Platform> {
192200
return result;
193201
}
194202

195-
export function resolveLatestVersion(versionOverride = ""): string {
203+
export function resolveLatestVersion(
204+
versionOverride = "",
205+
deps: ResolveLatestVersionDeps = {},
206+
): string {
196207
if (versionOverride) {
197208
return versionOverride;
198209
}
199-
return run(
200-
"npm",
201-
[
202-
"view",
203-
"openclaw",
204-
"version",
205-
"--userconfig",
206-
mkdtempSync(path.join(tmpdir(), "openclaw-npm-")),
207-
],
208-
{
210+
const createTempDir = deps.createTempDir ?? mkdtempSync;
211+
const removeDir = deps.removeDir ?? rmSync;
212+
const runCommand = deps.runCommand ?? run;
213+
const resolveTempDir = deps.tempDir ?? tmpdir;
214+
const writeFile = deps.writeFile ?? writeFileSync;
215+
const userConfigDir = createTempDir(path.join(resolveTempDir(), "openclaw-npm-"));
216+
const userConfigPath = path.join(userConfigDir, "npmrc");
217+
try {
218+
writeFile(userConfigPath, "", "utf8");
219+
return runCommand("npm", ["view", "openclaw", "version", "--userconfig", userConfigPath], {
209220
quiet: true,
210-
},
211-
).stdout.trim();
221+
}).stdout.trim();
222+
} finally {
223+
removeDir(userConfigDir, { force: true, recursive: true });
224+
}
212225
}

test/scripts/parallels-smoke-model.test.ts

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
1-
import { chmodSync, copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1+
import { EventEmitter } from "node:events";
2+
import {
3+
chmodSync,
4+
copyFileSync,
5+
existsSync,
6+
mkdtempSync,
7+
readFileSync,
8+
rmSync,
9+
statSync,
10+
writeFileSync,
11+
} from "node:fs";
212
import { createServer } from "node:net";
313
import { tmpdir } from "node:os";
4-
import { delimiter, join, win32 } from "node:path";
14+
import { basename, delimiter, join, win32 } from "node:path";
515
import { setTimeout as delay } from "node:timers/promises";
616
import { pathToFileURL } from "node:url";
717
import { beforeAll, describe, expect, it, vi } from "vitest";
818
import {
919
modelProviderConfigBatchJson,
1020
readPositiveIntEnv,
21+
resolveLatestVersion,
1122
resolveParallelsModelTimeoutSeconds,
1223
resolveProviderAuth as resolveProviderAuthDirect,
1324
resolveSnapshot,
@@ -84,6 +95,21 @@ function writeFakePrlctl(tempDir: string, posixScript: string, windowsBootstrap:
8495
writeFileSync(join(tempDir, "prlctl-bootstrap.mjs"), windowsBootstrap);
8596
}
8697

98+
class FakeHostServerChild extends EventEmitter {
99+
exitCode: number | null = null;
100+
readonly signals: string[] = [];
101+
102+
kill(signal?: NodeJS.Signals | number): boolean {
103+
this.signals.push(String(signal));
104+
return true;
105+
}
106+
107+
exit(): void {
108+
this.exitCode = 0;
109+
this.emit("exit", 0, null);
110+
}
111+
}
112+
87113
function withEnv<T>(env: Record<string, string>, callback: () => T): T {
88114
const previous = new Map<string, string | undefined>();
89115
for (const [key, _value] of Object.entries(env)) {
@@ -286,6 +312,58 @@ describe("Parallels smoke model selection", () => {
286312
expect(retained).toBe(`${"a".repeat(2)}${"b".repeat(10)}`);
287313
});
288314

315+
it("waits for host artifact server exit after SIGKILL before stop resolves", async () => {
316+
vi.useFakeTimers();
317+
try {
318+
const child = new FakeHostServerChild();
319+
const stop = hostServerTesting.stopHostServerChild(child as never, 100, 100);
320+
expect(child.signals).toEqual(["SIGTERM"]);
321+
322+
await vi.advanceTimersByTimeAsync(100);
323+
expect(child.signals).toEqual(["SIGTERM", "SIGKILL"]);
324+
325+
let resolved = false;
326+
void stop.then(() => {
327+
resolved = true;
328+
});
329+
await Promise.resolve();
330+
expect(resolved).toBe(false);
331+
332+
child.exit();
333+
await expect(stop).resolves.toBe(true);
334+
expect(resolved).toBe(true);
335+
} finally {
336+
vi.useRealTimers();
337+
}
338+
});
339+
340+
it("uses a temporary npmrc file and cleans it after resolving the latest package version", () => {
341+
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-parallels-version-"));
342+
let userConfigPath = "";
343+
try {
344+
const version = resolveLatestVersion("", {
345+
createTempDir: (prefix) => {
346+
expect(prefix).toBe(join(tmpdir(), "openclaw-npm-"));
347+
return mkdtempSync(join(tempRoot, "npm-"));
348+
},
349+
runCommand: (command, args, options) => {
350+
userConfigPath = args.at(-1) ?? "";
351+
expect(command).toBe("npm");
352+
expect(args).toEqual(["view", "openclaw", "version", "--userconfig", userConfigPath]);
353+
expect(options).toEqual({ quiet: true });
354+
expect(statSync(userConfigPath).isFile()).toBe(true);
355+
return { status: 0, stderr: "", stdout: "2026.6.1\n" };
356+
},
357+
});
358+
359+
expect(version).toBe("2026.6.1");
360+
expect(basename(userConfigPath)).toBe("npmrc");
361+
expect(existsSync(userConfigPath)).toBe(false);
362+
} finally {
363+
rmSync(tempRoot, { force: true, recursive: true });
364+
}
365+
});
366+
289367
it.runIf(process.platform !== "win32")(
290368
"reports only the bounded host artifact server stderr tail",
291369
async () => {

0 commit comments

Comments
 (0)