fix(test): kill openclaw test process groups

This commit is contained in:
Vincent Koc
2026-06-17 22:57:48 +02:00
parent 5893758957
commit 912946ff94
2 changed files with 49 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
// OpenClaw test instance tests cover spawned test instance lifecycle.
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { createOpenClawTestInstance, testing } from "./openclaw-test-instance.js";
async function expectPathMissing(targetPath: string): Promise<void> {
@@ -44,6 +44,24 @@ describe("openclaw test instance", () => {
).rejects.toThrow("gateway exited before listening");
});
it("signals test instance process groups on POSIX", () => {
const child = {
pid: 1234,
kill: vi.fn(() => true),
};
const killProcess = vi.fn(() => true);
testing.signalOpenClawTestProcess(child, "SIGKILL", killProcess);
if (process.platform === "win32") {
expect(killProcess).not.toHaveBeenCalled();
expect(child.kill).toHaveBeenCalledWith("SIGKILL");
} else {
expect(killProcess).toHaveBeenCalledWith(-1234, "SIGKILL");
expect(child.kill).not.toHaveBeenCalled();
}
});
it("creates isolated config and spawn env without mutating process env", async () => {
const previousHome = process.env.HOME;
const inst = await createOpenClawTestInstance({

View File

@@ -73,6 +73,8 @@ type BoundedStringLog = string[] & {
truncated?: boolean;
};
type OpenClawTestChildProcess = Pick<ChildProcessWithoutNullStreams, "kill" | "pid">;
function createBoundedStringLog(): string[] {
const log = [] as BoundedStringLog;
log.byteLength = 0;
@@ -152,6 +154,7 @@ async function prepareGatewayEntrypoint(cwd: string): Promise<string[]> {
cwd,
env: { ...process.env, VITEST: "1" },
stdio: ["ignore", "pipe", "pipe"],
detached: shouldUseOpenClawTestProcessGroup(),
});
child.stdout?.setEncoding("utf8");
child.stderr?.setEncoding("utf8");
@@ -167,7 +170,7 @@ async function prepareGatewayEntrypoint(cwd: string): Promise<string[]> {
]);
if (completed === null) {
child.kill("SIGKILL");
signalOpenClawTestProcess(child, "SIGKILL");
throw new Error(`timeout preparing gateway entrypoint\n${formatLogs(stdout, stderr)}`);
}
if (completed.code !== 0) {
@@ -401,6 +404,7 @@ export async function createOpenClawTestInstance(
cwd,
env,
stdio: ["ignore", "pipe", "pipe"],
detached: shouldUseOpenClawTestProcessGroup(),
},
);
@@ -428,7 +432,7 @@ export async function createOpenClawTestInstance(
}
if (!hasChildExited(child) && !child.killed) {
try {
child.kill("SIGTERM");
signalOpenClawTestProcess(child, "SIGTERM");
} catch {
// ignore
}
@@ -439,7 +443,7 @@ export async function createOpenClawTestInstance(
);
if (!exited && !hasChildExited(child) && !child.killed) {
try {
child.kill("SIGKILL");
signalOpenClawTestProcess(child, "SIGKILL");
} catch {
// ignore
}
@@ -479,6 +483,7 @@ async function runCommand(params: {
cwd: params.cwd,
env: params.env,
stdio: ["ignore", "pipe", "pipe"],
detached: shouldUseOpenClawTestProcessGroup(),
});
child.stdout?.setEncoding("utf8");
child.stderr?.setEncoding("utf8");
@@ -493,7 +498,7 @@ async function runCommand(params: {
sleep(params.timeoutMs).then(() => null),
]);
if (completed === null) {
child.kill("SIGKILL");
signalOpenClawTestProcess(child, "SIGKILL");
await waitForGatewayExit(child, GATEWAY_STOP_TIMEOUT_MS);
throw new Error(
`command timed out after ${params.timeoutMs}ms: ${params.args.join(" ")}\n${formatLogs(stdout, stderr)}`,
@@ -506,10 +511,31 @@ async function runCommand(params: {
};
}
function shouldUseOpenClawTestProcessGroup(): boolean {
return process.platform !== "win32";
}
function signalOpenClawTestProcess(
child: OpenClawTestChildProcess,
signal: NodeJS.Signals,
killProcess: (pid: number, signal: NodeJS.Signals) => boolean = process.kill,
): void {
if (shouldUseOpenClawTestProcessGroup() && typeof child.pid === "number") {
try {
killProcess(-child.pid, signal);
return;
} catch {
// Fall back to the direct child if the process group already exited.
}
}
child.kill(signal);
}
export const testing = {
appendLogChunk,
createBoundedStringLog,
formatLogs,
hasChildExited,
signalOpenClawTestProcess,
waitForPortOpen,
};