

























@@ -37,6 +37,41 @@ async function expectPathMissing(targetPath: string) {
3737expect(Reflect.get(statError, "code")).toBe("ENOENT");
3838}
393940+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 sleep(ms: number): Promise<void> {
50+await new Promise((resolve) => setTimeout(resolve, ms));
51+}
52+53+async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
54+const deadlineAt = Date.now() + timeoutMs;
55+while (Date.now() < deadlineAt) {
56+if (fs.existsSync(filePath)) {
57+return;
58+}
59+await sleep(25);
60+}
61+throw new Error(`timed out waiting for ${filePath}`);
62+}
63+64+async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
65+const deadlineAt = Date.now() + timeoutMs;
66+while (Date.now() < deadlineAt) {
67+if (!isProcessAlive(pid)) {
68+return;
69+}
70+await sleep(25);
71+}
72+throw new Error(`timed out waiting for pid ${pid} to exit`);
73+}
74+4075describe("resolveTsdownBuildInvocation", () => {
4176it("parses wrapper help before any tsdown work", () => {
4277expect(parseTsdownBuildArgs(["--help"])).toEqual({ forwardedArgs: [], help: true });
@@ -610,4 +645,58 @@ describe("runTsdownBuildInvocation", () => {
610645expect(result.signal).toBe("SIGTERM");
611646expect(output.chunks.join("")).toContain("timeout after 50ms");
612647});
648+649+it.skipIf(process.platform === "win32")(
650+"kills timed-out tsdown process groups when the wrapper exits first",
651+async () => {
652+const rootDir = createTempDir("openclaw-tsdown-timeout-");
653+const childPidPath = path.join(rootDir, "child.pid");
654+let childPid = 0;
655+const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
656+const parentScript = [
657+"const { spawn } = require('node:child_process');",
658+"const fs = require('node:fs');",
659+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
660+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(child.pid));`,
661+"process.on('SIGTERM', () => process.exit(0));",
662+"setInterval(() => {}, 1000);",
663+].join("");
664+665+try {
666+const output = createWriteSink();
667+const runPromise = runTsdownBuildInvocation(
668+{
669+command: process.execPath,
670+args: ["-e", parentScript],
671+options: {
672+stdio: ["ignore", "pipe", "pipe"],
673+shell: false,
674+env: process.env,
675+},
676+},
677+{
678+stdout: output.sink,
679+stderr: output.sink,
680+env: {
681+ ...process.env,
682+OPENCLAW_TSDOWN_HEARTBEAT_MS: "0",
683+OPENCLAW_TSDOWN_TIMEOUT_MS: "50",
684+},
685+},
686+);
687+688+await waitForFile(childPidPath, 2_000);
689+childPid = Number.parseInt(fs.readFileSync(childPidPath, "utf8"), 10);
690+expect(isProcessAlive(childPid)).toBe(true);
691+const result = await runPromise;
692+693+expect(result.timedOut).toBe(true);
694+await waitForDead(childPid, 2_000);
695+} finally {
696+if (childPid && isProcessAlive(childPid)) {
697+process.kill(childPid, "SIGKILL");
698+}
699+}
700+},
701+);
613702});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。