





















@@ -36,6 +36,34 @@ function isProcessAlive(pid: number): boolean {
3636}
3737}
383839+async function sleep(ms: number): Promise<void> {
40+await new Promise((resolve) => {
41+setTimeout(resolve, ms);
42+});
43+}
44+45+async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
46+const deadlineAt = Date.now() + timeoutMs;
47+while (Date.now() < deadlineAt) {
48+if (fs.existsSync(filePath)) {
49+return;
50+}
51+await sleep(25);
52+}
53+throw new Error(`timed out waiting for ${filePath}`);
54+}
55+56+async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
57+const deadlineAt = Date.now() + timeoutMs;
58+while (Date.now() < deadlineAt) {
59+if (!isProcessAlive(pid)) {
60+return;
61+}
62+await sleep(25);
63+}
64+throw new Error(`timed out waiting for pid ${pid} to exit`);
65+}
66+3967function writeGroupedReport(filePath: string) {
4068fs.writeFileSync(
4169filePath,
@@ -695,6 +723,64 @@ describe("scripts/test-group-report child process guard", () => {
695723}
696724});
697725726+it("finishes promptly when timed process-group descendants exit cleanly", async () => {
727+if (process.platform === "win32") {
728+return;
729+}
730+731+const tempDir = makeTempDir();
732+const childPidPath = path.join(tempDir, "child.pid");
733+const readyPath = path.join(tempDir, "child.ready");
734+const cleanupPath = path.join(tempDir, "child.cleanup");
735+let childPid: number | undefined;
736+try {
737+const childScript = [
738+"const fs = require('node:fs');",
739+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
740+"process.on('SIGTERM', () => {",
741+" setTimeout(() => {",
742+` fs.writeFileSync(${JSON.stringify(cleanupPath)}, "clean");`,
743+" process.exit(0);",
744+" }, 75);",
745+"});",
746+`fs.writeFileSync(${JSON.stringify(readyPath)}, "ready");`,
747+"setInterval(() => {}, 1000);",
748+].join("\n");
749+const parentScript = [
750+"const { spawn } = require('node:child_process');",
751+`spawn(process.execPath, ["--eval", ${JSON.stringify(childScript)}], { stdio: "ignore" });`,
752+"process.on('SIGTERM', () => process.exit(0));",
753+"setInterval(() => {}, 1000);",
754+].join("\n");
755+756+const startedAt = Date.now();
757+const runPromise = spawnText(process.execPath, ["--eval", parentScript], {
758+cwd: process.cwd(),
759+env: process.env,
760+killGraceMs: 1_000,
761+timeoutMs: 1_000,
762+});
763+764+await waitForFile(readyPath, 2_000);
765+childPid = Number.parseInt(fs.readFileSync(childPidPath, "utf8"), 10);
766+const result = await runPromise;
767+768+expect(result).toMatchObject({
769+status: 1,
770+signal: null,
771+timedOut: true,
772+});
773+expect(fs.readFileSync(cleanupPath, "utf8")).toBe("clean");
774+expect(Date.now() - startedAt).toBeLessThan(1_700);
775+await waitForDead(childPid, 2_000);
776+} finally {
777+if (childPid !== undefined && isProcessAlive(childPid)) {
778+process.kill(childPid, "SIGKILL");
779+}
780+fs.rmSync(tempDir, { recursive: true, force: true });
781+}
782+});
783+698784it("streams large child output to a log path without retaining it", async () => {
699785const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-test-group-report-log-"));
700786const logPath = path.join(tempDir, "child.log");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。