





















11// Dev Tooling Safety tests cover dev tooling safety script behavior.
2-import { spawnSync } from "node:child_process";
2+import { spawn, spawnSync } from "node:child_process";
33import { EventEmitter } from "node:events";
4+import { existsSync } from "node:fs";
45import fs from "node:fs/promises";
56import os from "node:os";
67import path from "node:path";
78import { Readable } from "node:stream";
9+import { pathToFileURL } from "node:url";
810import { afterEach, describe, expect, it, vi } from "vitest";
911import { testing as promptProbeTesting } from "../../scripts/anthropic-prompt-probe.ts";
1012import { testing as claudeUsageTesting } from "../../scripts/debug-claude-usage.ts";
@@ -22,6 +24,43 @@ import {
22242325const tempDirs: string[] = [];
242627+async function waitForCondition(predicate: () => boolean, timeoutMs = 5_000): Promise<void> {
28+const started = Date.now();
29+while (Date.now() - started < timeoutMs) {
30+if (predicate()) {
31+return;
32+}
33+await new Promise((resolve) => {
34+setTimeout(resolve, 50);
35+});
36+}
37+throw new Error("timed out waiting for condition");
38+}
39+40+function isProcessAlive(pid: number): boolean {
41+try {
42+process.kill(pid, 0);
43+return true;
44+} catch {
45+return false;
46+}
47+}
48+49+async function waitForChildExit(
50+child: ReturnType<typeof spawn>,
51+timeoutMs = 8_000,
52+): Promise<{ status: number | null; signal: NodeJS.Signals | null }> {
53+return await Promise.race([
54+new Promise<{ status: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
55+child.once("error", reject);
56+child.once("exit", (status, signal) => resolve({ status, signal }));
57+}),
58+new Promise<never>((_, reject) => {
59+setTimeout(() => reject(new Error("timed out waiting for child exit")), timeoutMs);
60+}),
61+]);
62+}
63+2564afterEach(async () => {
2665vi.useRealTimers();
2766for (const dir of tempDirs.splice(0)) {
@@ -674,6 +713,136 @@ describe("script-specific dev tooling hardening", () => {
674713expect(closeCalls).toBe(1);
675714});
676715716+it.runIf(process.platform !== "win32")(
717+"cleans Anthropic prompt gateway descendants after leader exit",
718+async () => {
719+const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-prompt-gateway-tree-"));
720+tempDirs.push(tempRoot);
721+const descendantPidPath = path.join(tempRoot, "descendant.pid");
722+let descendantPid = 0;
723+const descendantScript = [
724+"process.on('SIGINT', () => {});",
725+"process.on('SIGTERM', () => {});",
726+"setInterval(() => {}, 1000);",
727+].join("");
728+const leaderScript = [
729+"import childProcess from 'node:child_process';",
730+"import fs from 'node:fs';",
731+"const descendant = childProcess.spawn(process.execPath, [",
732+" '--input-type=module',",
733+` '--eval', ${JSON.stringify(descendantScript)},`,
734+"], { stdio: 'ignore' });",
735+`fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(descendant.pid));`,
736+"process.on('SIGINT', () => process.exit(0));",
737+"setInterval(() => {}, 1000);",
738+].join("\n");
739+const child = spawn(process.execPath, ["--input-type=module", "--eval", leaderScript], {
740+detached: true,
741+stdio: "ignore",
742+});
743+let closeCalls = 0;
744+745+try {
746+await waitForCondition(() => isProcessAlive(child.pid!) && existsSync(descendantPidPath));
747+descendantPid = Number.parseInt(await fs.readFile(descendantPidPath, "utf8"), 10);
748+expect(Number.isInteger(descendantPid)).toBe(true);
749+expect(isProcessAlive(descendantPid)).toBe(true);
750+751+const stopped = await promptProbeTesting.stopGatewayPromptChild(
752+child as Parameters<typeof promptProbeTesting.stopGatewayPromptChild>[0],
753+{
754+close: async () => {
755+closeCalls += 1;
756+},
757+},
758+50,
759+1_000,
760+);
761+762+expect(stopped).toBe(true);
763+expect(closeCalls).toBe(1);
764+await waitForCondition(() => !isProcessAlive(descendantPid));
765+} finally {
766+if (child.pid && isProcessAlive(child.pid)) {
767+process.kill(-child.pid, "SIGKILL");
768+}
769+if (descendantPid && isProcessAlive(descendantPid)) {
770+process.kill(descendantPid, "SIGKILL");
771+}
772+}
773+},
774+);
775+776+it.runIf(process.platform !== "win32")(
777+"cleans Anthropic prompt gateway descendants on parent signal",
778+async () => {
779+const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-prompt-parent-signal-"));
780+tempDirs.push(tempRoot);
781+const descendantPidPath = path.join(tempRoot, "descendant.pid");
782+const readyPath = path.join(tempRoot, "ready");
783+const runnerPath = path.join(tempRoot, "parent-signal-runner.mjs");
784+let descendantPid = 0;
785+const descendantScript = [
786+"process.on('SIGINT', () => {});",
787+"process.on('SIGTERM', () => {});",
788+"setInterval(() => {}, 1000);",
789+].join("");
790+const leaderScript = [
791+"import childProcess from 'node:child_process';",
792+"import fs from 'node:fs';",
793+"const descendant = childProcess.spawn(process.execPath, [",
794+" '--input-type=module',",
795+` '--eval', ${JSON.stringify(descendantScript)},`,
796+"], { stdio: 'ignore' });",
797+`fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(descendant.pid));`,
798+"process.on('SIGINT', () => process.exit(0));",
799+"setInterval(() => {}, 1000);",
800+].join("\n");
801+await fs.writeFile(
802+runnerPath,
803+[
804+"import childProcess from 'node:child_process';",
805+"import fs from 'node:fs';",
806+`const { testing } = await import(${JSON.stringify(
807+ pathToFileURL(path.resolve("scripts/anthropic-prompt-probe.ts")).href,
808+ )});`,
809+`const child = childProcess.spawn(process.execPath, ['--input-type=module', '--eval', ${JSON.stringify(leaderScript)}], { detached: true, stdio: 'ignore' });`,
810+"let stopPromise;",
811+"const stopGateway = () => {",
812+" stopPromise ??= testing.stopGatewayPromptChild(child, { close: async () => {} }, 50, 1000);",
813+" return stopPromise;",
814+"};",
815+"testing.installGatewayPromptParentSignalHandlers(child, stopGateway);",
816+`fs.writeFileSync(${JSON.stringify(readyPath)}, String(process.pid));`,
817+"setInterval(() => {}, 1000);",
818+].join("\n"),
819+"utf8",
820+);
821+const runner = spawn(process.execPath, ["--import", "tsx", runnerPath], {
822+stdio: "ignore",
823+});
824+825+try {
826+await waitForCondition(() => existsSync(readyPath) && existsSync(descendantPidPath));
827+descendantPid = Number.parseInt(await fs.readFile(descendantPidPath, "utf8"), 10);
828+expect(Number.isInteger(descendantPid)).toBe(true);
829+expect(isProcessAlive(descendantPid)).toBe(true);
830+831+const runnerExit = waitForChildExit(runner);
832+process.kill(runner.pid!, "SIGTERM");
833+await expect(runnerExit).resolves.toEqual({ status: 143, signal: null });
834+await waitForCondition(() => !isProcessAlive(descendantPid));
835+} finally {
836+if (runner.pid && isProcessAlive(runner.pid)) {
837+process.kill(runner.pid, "SIGKILL");
838+}
839+if (descendantPid && isProcessAlive(descendantPid)) {
840+process.kill(descendantPid, "SIGKILL");
841+}
842+}
843+},
844+);
845+677846it("waits for Anthropic prompt gateway log writes before closing the log file", async () => {
678847let resolveWrite: (() => void) | undefined;
679848const order: string[] = [];
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。