

























11// Tsdown Build tests cover tsdown build script behavior.
2-import { spawnSync } from "node:child_process";
2+import { spawn, spawnSync } from "node:child_process";
33import fs from "node:fs";
44import fsPromises from "node:fs/promises";
55import path from "node:path";
6+import { pathToFileURL } from "node:url";
67import { describe, expect, it, vi } from "vitest";
78import {
89cleanTsdownOutputRoots,
@@ -72,6 +73,21 @@ async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
7273throw new Error(`timed out waiting for pid ${pid} to exit`);
7374}
747576+function waitForChildClose(
77+child: ReturnType<typeof spawn>,
78+timeoutMs = 5_000,
79+): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
80+return new Promise((resolve, reject) => {
81+const timeout = setTimeout(() => {
82+reject(new Error("child did not close before timeout"));
83+}, timeoutMs);
84+child.once("close", (code, signal) => {
85+clearTimeout(timeout);
86+resolve({ code, signal });
87+});
88+});
89+}
90+7591describe("resolveTsdownBuildInvocation", () => {
7692it("parses wrapper help before any tsdown work", () => {
7793expect(parseTsdownBuildArgs(["--help"])).toEqual({ forwardedArgs: [], help: true });
@@ -767,4 +783,64 @@ describe("runTsdownBuildInvocation", () => {
767783}
768784},
769785);
786+787+it.skipIf(process.platform === "win32")(
788+"cleans process-group descendants before forwarding parent SIGTERM",
789+async () => {
790+const rootDir = createTempDir("openclaw-tsdown-parent-signal-");
791+const childPidPath = path.join(rootDir, "child.pid");
792+const readyPath = path.join(rootDir, "child.ready");
793+const scriptUrl = pathToFileURL(path.resolve("scripts/tsdown-build.mjs")).href;
794+let childPid = 0;
795+let runner: ReturnType<typeof spawn> | undefined;
796+797+try {
798+const childScript = [
799+"const fs = require('node:fs');",
800+"process.on('SIGTERM', () => {});",
801+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
802+"setInterval(() => {}, 1000);",
803+].join("");
804+const parentScript = [
805+"const { spawn } = require('node:child_process');",
806+`spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
807+`require('node:fs').writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
808+"process.on('SIGTERM', () => process.exit(0));",
809+"setInterval(() => {}, 1000);",
810+].join("");
811+const runnerScript = [
812+`import { runTsdownBuildInvocation } from ${JSON.stringify(scriptUrl)};`,
813+"await runTsdownBuildInvocation(",
814+` { command: process.execPath, args: ['-e', ${JSON.stringify(parentScript)}], options: { stdio: ['ignore', 'pipe', 'pipe'], shell: false, env: process.env } },`,
815+" { env: { ...process.env, OPENCLAW_TSDOWN_HEARTBEAT_MS: '0' } },",
816+");",
817+].join("\n");
818+819+runner = spawn(process.execPath, ["--input-type=module", "-e", runnerScript], {
820+cwd: process.cwd(),
821+stdio: ["ignore", "ignore", "pipe"],
822+});
823+824+await waitForFile(readyPath, 2_000);
825+await waitForFile(childPidPath, 2_000);
826+childPid = Number.parseInt(fs.readFileSync(childPidPath, "utf8"), 10);
827+expect(isProcessAlive(childPid)).toBe(true);
828+829+runner.kill("SIGTERM");
830+831+await expect(waitForChildClose(runner)).resolves.toEqual({
832+code: null,
833+signal: "SIGTERM",
834+});
835+await waitForDead(childPid, 2_000);
836+} finally {
837+if (runner?.pid && isProcessAlive(runner.pid)) {
838+runner.kill("SIGKILL");
839+}
840+if (childPid && isProcessAlive(childPid)) {
841+process.kill(childPid, "SIGKILL");
842+}
843+}
844+},
845+);
770846});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。