























11// Run Additional Boundary Checks tests cover run additional boundary checks script behavior.
2-import { spawnSync } from "node:child_process";
2+import { spawn, spawnSync } from "node:child_process";
33import fs from "node:fs";
44import os from "node:os";
55import path from "node:path";
@@ -47,6 +47,13 @@ function isProcessAlive(pid: number): boolean {
4747}
4848}
494950+function isProcessZombie(pid: number): boolean {
51+const result = spawnSync("ps", ["-o", "stat=", "-p", String(pid)], {
52+encoding: "utf8",
53+});
54+return result.status === 0 && result.stdout.trim().startsWith("Z");
55+}
56+5057async function sleep(ms: number): Promise<void> {
5158await new Promise((resolve) => {
5259setTimeout(resolve, ms);
@@ -75,6 +82,32 @@ async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
7582throw new Error(`process still alive: ${pid}`);
7683}
778485+async function waitForNotRunning(pid: number, timeoutMs: number): Promise<void> {
86+const deadlineAt = Date.now() + timeoutMs;
87+while (Date.now() < deadlineAt) {
88+if (!isProcessAlive(pid) || isProcessZombie(pid)) {
89+return;
90+}
91+await sleep(25);
92+}
93+throw new Error(`process still running: ${pid}`);
94+}
95+96+async function waitForChildClose(
97+child: ReturnType<typeof spawn>,
98+timeoutMs: number,
99+): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
100+return await new Promise((resolve, reject) => {
101+const timeout = setTimeout(() => {
102+reject(new Error("child did not close before timeout"));
103+}, timeoutMs);
104+child.once("close", (code, signal) => {
105+clearTimeout(timeout);
106+resolve({ code, signal });
107+});
108+});
109+}
110+78111describe("run-additional-boundary-checks", () => {
79112it("runs prompt snapshot drift checks in CI", () => {
80113expect(BOUNDARY_CHECKS[0]).toEqual({
@@ -284,4 +317,84 @@ describe("run-additional-boundary-checks", () => {
284317}
285318},
286319);
320+321+it.skipIf(process.platform === "win32")(
322+"cleans active check descendants on parent signal",
323+async () => {
324+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-signal-"));
325+const readyPath = path.join(tempDir, "ready");
326+const childPidPath = path.join(tempDir, "child.pid");
327+let childPid = 0;
328+let runner: ReturnType<typeof spawn> | undefined;
329+try {
330+const childScript = [
331+"process.on('SIGTERM', () => {});",
332+"setInterval(() => {}, 1000);",
333+].join("");
334+const parentScript = [
335+"const { spawn } = require('node:child_process');",
336+"const fs = require('node:fs');",
337+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
338+"fs.writeFileSync(process.env.OPENCLAW_TEST_CHILD_PID, String(child.pid));",
339+"fs.writeFileSync(process.env.OPENCLAW_TEST_READY, 'ready');",
340+"process.on('SIGTERM', () => process.exit(0));",
341+"setInterval(() => {}, 1000);",
342+].join("");
343+const runnerScript = `
344+import { runChecks } from ${JSON.stringify(
345+ new URL("../../scripts/run-additional-boundary-checks.mjs", import.meta.url).href,
346+ )};
347+348+await runChecks(
349+ [{
350+ label: "parent-signal",
351+ command: process.execPath,
352+ args: ["-e", ${JSON.stringify(parentScript)}],
353+ }],
354+ {
355+ checkTimeoutMs: 30000,
356+ concurrency: 1,
357+ cwd: process.cwd(),
358+ env: process.env,
359+ output: { write() { return true; } },
360+ outputMaxBytes: 4096,
361+ },
362+);
363+`;
364+365+runner = spawn(process.execPath, ["--input-type=module", "--eval", runnerScript], {
366+cwd: process.cwd(),
367+env: {
368+ ...process.env,
369+OPENCLAW_TEST_CHILD_PID: childPidPath,
370+OPENCLAW_TEST_READY: readyPath,
371+},
372+stdio: ["ignore", "ignore", "pipe"],
373+});
374+375+await waitForFile(readyPath, 2000);
376+childPid = Number(fs.readFileSync(childPidPath, "utf8"));
377+expect(Number.isInteger(childPid)).toBe(true);
378+expect(isProcessAlive(childPid)).toBe(true);
379+380+runner.kill("SIGTERM");
381+await sleep(50);
382+runner.kill("SIGTERM");
383+384+await expect(waitForChildClose(runner, 10_000)).resolves.toEqual({
385+code: null,
386+signal: "SIGTERM",
387+});
388+await waitForNotRunning(childPid, 2000);
389+} finally {
390+if (childPid && isProcessAlive(childPid)) {
391+process.kill(childPid, "SIGKILL");
392+}
393+if (runner?.pid && isProcessAlive(runner.pid)) {
394+runner.kill("SIGKILL");
395+}
396+fs.rmSync(tempDir, { force: true, recursive: true });
397+}
398+},
399+);
287400});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。