






















@@ -4,6 +4,7 @@ import { EventEmitter } from "node:events";
44import fs from "node:fs";
55import os from "node:os";
66import path from "node:path";
7+import { pathToFileURL } from "node:url";
78import { afterEach, describe, expect, it } from "vitest";
89import {
910acquireBoundaryCheckLock,
@@ -84,6 +85,21 @@ async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
8485throw new Error(`process still alive: ${pid}`);
8586}
868788+function waitForChildClose(
89+child: ReturnType<typeof spawn>,
90+timeoutMs = 5_000,
91+): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
92+return new Promise((resolve, reject) => {
93+const timeout = setTimeout(() => {
94+reject(new Error("child did not close before timeout"));
95+}, timeoutMs);
96+child.once("close", (code, signal) => {
97+clearTimeout(timeout);
98+resolve({ code, signal });
99+});
100+});
101+}
102+87103afterEach(() => {
88104for (const rootDir of tempRoots) {
89105fs.rmSync(rootDir, { force: true, recursive: true });
@@ -652,6 +668,67 @@ describe("check-extension-package-tsc-boundary", () => {
652668},
653669);
654670671+it.skipIf(process.platform === "win32")(
672+"cleans active async node step descendants before forwarding parent SIGTERM",
673+async () => {
674+const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-extension-tsc-signal-"));
675+tempRoots.add(root);
676+const childPidPath = path.join(root, "child.pid");
677+const readyPath = path.join(root, "child.ready");
678+const scriptUrl = pathToFileURL(
679+path.resolve("scripts/check-extension-package-tsc-boundary.mjs"),
680+).href;
681+let childPid = 0;
682+let runner: ReturnType<typeof spawn> | undefined;
683+const childScript = [
684+"const fs = require('node:fs');",
685+"process.on('SIGTERM', () => {});",
686+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
687+"setInterval(() => {}, 1000);",
688+].join("");
689+const parentScript = [
690+"const { spawn } = require('node:child_process');",
691+`spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
692+`require('node:fs').writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
693+"process.on('SIGTERM', () => process.exit(0));",
694+"setInterval(() => {}, 1000);",
695+].join("");
696+const runnerScript = [
697+`import { runNodeStepAsync } from ${JSON.stringify(scriptUrl)};`,
698+`await runNodeStepAsync('parent-signal-step-group', ['--eval', ${JSON.stringify(
699+ parentScript,
700+ )}], 60_000);`,
701+].join("\n");
702+703+try {
704+runner = spawn(process.execPath, ["--input-type=module", "-e", runnerScript], {
705+cwd: process.cwd(),
706+stdio: ["ignore", "ignore", "pipe"],
707+});
708+709+await waitForFile(readyPath, 2_000);
710+await waitForFile(childPidPath, 2_000);
711+childPid = Number.parseInt(fs.readFileSync(childPidPath, "utf8"), 10);
712+expect(isProcessAlive(childPid)).toBe(true);
713+714+runner.kill("SIGTERM");
715+716+await expect(waitForChildClose(runner)).resolves.toEqual({
717+code: null,
718+signal: "SIGTERM",
719+});
720+await waitForDead(childPid, 2_000);
721+} finally {
722+if (runner?.pid && isProcessAlive(runner.pid)) {
723+runner.kill("SIGKILL");
724+}
725+if (childPid && isProcessAlive(childPid)) {
726+process.kill(childPid, "SIGKILL");
727+}
728+}
729+},
730+);
731+655732it("passes successful step timing metadata to onSuccess handlers", async () => {
656733const elapsedTimes: number[] = [];
657734此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。