fix(qa-matrix): taskkill scenario cli trees on windows

This commit is contained in:
Vincent Koc
2026-06-21 08:16:56 +02:00
parent b574da57cf
commit b06e2f9149
2 changed files with 64 additions and 3 deletions

View File

@@ -3,13 +3,14 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
formatMatrixQaCliCommand,
redactMatrixQaCliOutput,
resolveMatrixQaOpenClawCliEntryPath,
runMatrixQaOpenClawCli,
startMatrixQaOpenClawCli,
testing,
} from "./scenario-runtime-cli.js";
function isProcessRunning(pid: number): boolean {
@@ -60,6 +61,38 @@ describe("Matrix QA CLI runtime", () => {
).toBe("GET /_matrix/client/v3/sync?access_token=abcdef…ghij");
});
it("force-kills Windows CLI process trees when graceful taskkill fails", () => {
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
try {
const killMock = vi.fn();
const child = {
pid: 12345,
kill: killMock,
} as unknown as Parameters<typeof testing.killMatrixQaCliChild>[0];
const runTaskkill = vi
.fn()
.mockReturnValueOnce({ status: 1 })
.mockReturnValueOnce({ status: 0 });
testing.killMatrixQaCliChild(child, "SIGTERM", runTaskkill);
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
stdio: "ignore",
windowsHide: true,
});
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
stdio: "ignore",
windowsHide: true,
});
expect(killMock).not.toHaveBeenCalled();
} finally {
if (platformDescriptor) {
Object.defineProperty(process, "platform", platformDescriptor);
}
}
});
it("prefers the ESM OpenClaw CLI entrypoint when present", async () => {
const root = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "matrix-qa-cli-entry-"));
try {

View File

@@ -1,5 +1,5 @@
// Qa Matrix plugin module implements scenario runtime cli behavior.
import { spawn as startOpenClawCliProcess } from "node:child_process";
import { spawn as startOpenClawCliProcess, spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { chmod, mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
@@ -107,8 +107,32 @@ function formatMatrixQaCliTimeoutError(result: MatrixQaCliRunResult, timeoutMs:
function killMatrixQaCliChild(
child: ReturnType<typeof startOpenClawCliProcess>,
signal: NodeJS.Signals,
runTaskkill: typeof spawnSync = spawnSync,
): void {
if (process.platform !== "win32" && child.pid) {
if (process.platform === "win32") {
if (child.pid) {
const args = ["/PID", String(child.pid), "/T"];
if (signal === "SIGKILL") {
args.push("/F");
}
const result = runTaskkill("taskkill", args, { stdio: "ignore", windowsHide: true });
if (!result.error && result.status === 0) {
return;
}
if (signal !== "SIGKILL") {
const forceResult = runTaskkill("taskkill", [...args, "/F"], {
stdio: "ignore",
windowsHide: true,
});
if (!forceResult.error && forceResult.status === 0) {
return;
}
}
}
child.kill(signal);
return;
}
if (child.pid) {
try {
process.kill(-child.pid, signal);
return;
@@ -459,3 +483,7 @@ export async function createMatrixQaOpenClawCliRuntime(params: {
stateDir,
};
}
export const testing = {
killMatrixQaCliChild,
};