




















11// Secret Provider Integrations tests cover secret provider integrations 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";
@@ -29,6 +29,20 @@ async function waitFor(predicate: () => boolean, timeoutMs = 5_000): Promise<voi
2929throw new Error("condition was not met before timeout");
3030}
313132+async function waitForChildClose(child: ReturnType<typeof spawn>, timeoutMs = 5_000) {
33+return await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
34+(resolve, reject) => {
35+const timeout = setTimeout(() => {
36+reject(new Error("child did not close before timeout"));
37+}, timeoutMs);
38+child.once("close", (code, signal) => {
39+clearTimeout(timeout);
40+resolve({ code, signal });
41+});
42+},
43+);
44+}
45+3246function isProcessAlive(pid: number): boolean {
3347try {
3448process.kill(pid, 0);
@@ -644,6 +658,78 @@ describe("secret provider integration proof harness", () => {
644658await expect(command).rejects.toThrow("command aborted");
645659});
646660661+it.runIf(process.platform !== "win32")(
662+"cleans active command process groups before parent signal exit",
663+async () => {
664+const root = makeTempDir();
665+const scriptPath = path.join(root, "spawn-descendant-parent-signal.mjs");
666+const runnerPath = path.join(root, "runner.mjs");
667+const descendantPidPath = path.join(root, "descendant.pid");
668+const readyPath = path.join(root, "ready");
669+let descendantPid = 0;
670+let runner: ReturnType<typeof spawn> | undefined;
671+const descendantScript = [
672+"import fs from 'node:fs';",
673+"process.on('SIGTERM', () => {});",
674+"process.on('SIGHUP', () => {});",
675+`fs.writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
676+"setInterval(() => {}, 1000);",
677+].join("\n");
678+679+fs.writeFileSync(
680+scriptPath,
681+[
682+"import childProcess from 'node:child_process';",
683+"import fs from 'node:fs';",
684+"const descendant = childProcess.spawn(process.execPath, [",
685+" '--input-type=module',",
686+` '--eval', ${JSON.stringify(descendantScript)},`,
687+"], { stdio: 'ignore' });",
688+`fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(descendant.pid));`,
689+"process.on('SIGTERM', () => process.exit(0));",
690+"setInterval(() => {}, 1000);",
691+"",
692+].join("\n"),
693+);
694+fs.writeFileSync(
695+runnerPath,
696+[
697+`const proof = await import(${JSON.stringify(
698+ `${pathToFileURL(proofScriptPath).href}?case=parent-signal-${Date.now()}`,
699+ )});`,
700+`await proof.runCommand(process.execPath, [${JSON.stringify(scriptPath)}], { timeoutMs: 30_000 });`,
701+"",
702+].join("\n"),
703+);
704+705+try {
706+runner = spawn(process.execPath, [runnerPath], {
707+cwd: process.cwd(),
708+stdio: ["ignore", "ignore", "pipe"],
709+});
710+await waitFor(() => fs.existsSync(readyPath) && fs.existsSync(descendantPidPath));
711+descendantPid = Number.parseInt(fs.readFileSync(descendantPidPath, "utf8"), 10);
712+expect(Number.isInteger(descendantPid)).toBe(true);
713+expect(isProcessAlive(descendantPid)).toBe(true);
714+715+runner.kill("SIGTERM");
716+717+await expect(waitForChildClose(runner, 5_000)).resolves.toEqual({
718+code: null,
719+signal: "SIGTERM",
720+});
721+await waitFor(() => !isProcessAlive(descendantPid));
722+} finally {
723+if (descendantPid && isProcessAlive(descendantPid)) {
724+process.kill(descendantPid, "SIGKILL");
725+}
726+if (runner?.pid && isProcessAlive(runner.pid)) {
727+runner.kill("SIGKILL");
728+}
729+}
730+},
731+);
732+647733it("detects startup secret leaks after the retained output cap", () => {
648734const root = makeTempDir();
649735const fakeOpenClaw = writeLeakingStartupOpenClaw(root);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。