mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 11:02:26 +00:00
fix(e2e): harden Parallels helper cleanup
This commit is contained in:
@@ -81,20 +81,54 @@ export async function startHostServer(input: {
|
||||
hostIp: input.hostIp,
|
||||
port: actualPort,
|
||||
stop: async () => {
|
||||
child.kill("SIGTERM");
|
||||
await new Promise<void>((resolve) => {
|
||||
child.once("exit", () => resolve());
|
||||
setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
resolve();
|
||||
}, 2_000).unref();
|
||||
});
|
||||
await stopHostServerChild(child);
|
||||
},
|
||||
urlFor: (filePath) =>
|
||||
`http://${input.hostIp}:${actualPort}/${encodeURIComponent(path.basename(filePath))}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function stopHostServerChild(
|
||||
child: ChildProcessWithoutNullStreams,
|
||||
terminateTimeoutMs = 2_000,
|
||||
killTimeoutMs = 1_500,
|
||||
): Promise<boolean> {
|
||||
if (child.exitCode != null) {
|
||||
return true;
|
||||
}
|
||||
child.kill("SIGTERM");
|
||||
if (await waitForChildExit(child, terminateTimeoutMs)) {
|
||||
return true;
|
||||
}
|
||||
child.kill("SIGKILL");
|
||||
return await waitForChildExit(child, killTimeoutMs);
|
||||
}
|
||||
|
||||
async function waitForChildExit(
|
||||
child: ChildProcessWithoutNullStreams,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> {
|
||||
if (child.exitCode != null) {
|
||||
return true;
|
||||
}
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
let settled = false;
|
||||
const onExit = () => settle(true);
|
||||
const timeout = setTimeout(() => settle(child.exitCode != null), timeoutMs);
|
||||
timeout.unref();
|
||||
function settle(exited: boolean): void {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
child.off("exit", onExit);
|
||||
resolve(exited);
|
||||
}
|
||||
child.once("exit", onExit);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForHostServer(
|
||||
child: ChildProcessWithoutNullStreams,
|
||||
port: number,
|
||||
@@ -160,4 +194,5 @@ async function delay(ms: number): Promise<void> {
|
||||
|
||||
export const testing = {
|
||||
appendBoundedOutput,
|
||||
stopHostServerChild,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { parsePositiveInt, readPositiveIntEnv } from "./env-limits.ts";
|
||||
import { die, run } from "./host-command.ts";
|
||||
import type { Mode, Platform, Provider, ProviderAuth } from "./types.ts";
|
||||
|
||||
type ResolveLatestVersionDeps = {
|
||||
createTempDir?: typeof mkdtempSync;
|
||||
removeDir?: typeof rmSync;
|
||||
runCommand?: typeof run;
|
||||
tempDir?: typeof tmpdir;
|
||||
writeFile?: typeof writeFileSync;
|
||||
};
|
||||
|
||||
export function parseBoolEnv(value: string | undefined): boolean {
|
||||
return /^(1|true|yes|on)$/i.test(value ?? "");
|
||||
}
|
||||
@@ -192,21 +200,26 @@ export function parsePlatformList(value: string): Set<Platform> {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveLatestVersion(versionOverride = ""): string {
|
||||
export function resolveLatestVersion(
|
||||
versionOverride = "",
|
||||
deps: ResolveLatestVersionDeps = {},
|
||||
): string {
|
||||
if (versionOverride) {
|
||||
return versionOverride;
|
||||
}
|
||||
return run(
|
||||
"npm",
|
||||
[
|
||||
"view",
|
||||
"openclaw",
|
||||
"version",
|
||||
"--userconfig",
|
||||
mkdtempSync(path.join(tmpdir(), "openclaw-npm-")),
|
||||
],
|
||||
{
|
||||
const createTempDir = deps.createTempDir ?? mkdtempSync;
|
||||
const removeDir = deps.removeDir ?? rmSync;
|
||||
const runCommand = deps.runCommand ?? run;
|
||||
const resolveTempDir = deps.tempDir ?? tmpdir;
|
||||
const writeFile = deps.writeFile ?? writeFileSync;
|
||||
const userConfigDir = createTempDir(path.join(resolveTempDir(), "openclaw-npm-"));
|
||||
const userConfigPath = path.join(userConfigDir, "npmrc");
|
||||
try {
|
||||
writeFile(userConfigPath, "", "utf8");
|
||||
return runCommand("npm", ["view", "openclaw", "version", "--userconfig", userConfigPath], {
|
||||
quiet: true,
|
||||
},
|
||||
).stdout.trim();
|
||||
}).stdout.trim();
|
||||
} finally {
|
||||
removeDir(userConfigDir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { chmodSync, copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { EventEmitter } from "node:events";
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { createServer } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join, win32 } from "node:path";
|
||||
import { basename, delimiter, join, win32 } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
modelProviderConfigBatchJson,
|
||||
readPositiveIntEnv,
|
||||
resolveLatestVersion,
|
||||
resolveParallelsModelTimeoutSeconds,
|
||||
resolveProviderAuth as resolveProviderAuthDirect,
|
||||
resolveSnapshot,
|
||||
@@ -84,6 +95,21 @@ function writeFakePrlctl(tempDir: string, posixScript: string, windowsBootstrap:
|
||||
writeFileSync(join(tempDir, "prlctl-bootstrap.mjs"), windowsBootstrap);
|
||||
}
|
||||
|
||||
class FakeHostServerChild extends EventEmitter {
|
||||
exitCode: number | null = null;
|
||||
readonly signals: string[] = [];
|
||||
|
||||
kill(signal?: NodeJS.Signals | number): boolean {
|
||||
this.signals.push(String(signal));
|
||||
return true;
|
||||
}
|
||||
|
||||
exit(): void {
|
||||
this.exitCode = 0;
|
||||
this.emit("exit", 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
@@ -286,6 +312,58 @@ describe("Parallels smoke model selection", () => {
|
||||
expect(retained).toBe(`${"a".repeat(2)}${"b".repeat(10)}`);
|
||||
});
|
||||
|
||||
it("waits for host artifact server exit after SIGKILL before stop resolves", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const child = new FakeHostServerChild();
|
||||
const stop = hostServerTesting.stopHostServerChild(child as never, 100, 100);
|
||||
expect(child.signals).toEqual(["SIGTERM"]);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
expect(child.signals).toEqual(["SIGTERM", "SIGKILL"]);
|
||||
|
||||
let resolved = false;
|
||||
void stop.then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
child.exit();
|
||||
await expect(stop).resolves.toBe(true);
|
||||
expect(resolved).toBe(true);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a temporary npmrc file and cleans it after resolving the latest package version", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-parallels-version-"));
|
||||
let userConfigPath = "";
|
||||
try {
|
||||
const version = resolveLatestVersion("", {
|
||||
createTempDir: (prefix) => {
|
||||
expect(prefix).toBe(join(tmpdir(), "openclaw-npm-"));
|
||||
return mkdtempSync(join(tempRoot, "npm-"));
|
||||
},
|
||||
runCommand: (command, args, options) => {
|
||||
userConfigPath = args.at(-1) ?? "";
|
||||
expect(command).toBe("npm");
|
||||
expect(args).toEqual(["view", "openclaw", "version", "--userconfig", userConfigPath]);
|
||||
expect(options).toEqual({ quiet: true });
|
||||
expect(statSync(userConfigPath).isFile()).toBe(true);
|
||||
return { status: 0, stderr: "", stdout: "2026.6.1\n" };
|
||||
},
|
||||
});
|
||||
|
||||
expect(version).toBe("2026.6.1");
|
||||
expect(basename(userConfigPath)).toBe("npmrc");
|
||||
expect(existsSync(userConfigPath)).toBe(false);
|
||||
} finally {
|
||||
rmSync(tempRoot, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"reports only the bounded host artifact server stderr tail",
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user