

























@@ -4,6 +4,7 @@ import { EventEmitter } from "node:events";
44import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
55import os from "node:os";
66import path from "node:path";
7+import { pathToFileURL } from "node:url";
78import { describe, expect, it } from "vitest";
89import {
910checkUnusedFiles,
@@ -65,6 +66,21 @@ async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
6566throw new Error(`process still alive: ${pid}`);
6667}
676869+function waitForChildClose(
70+child: ReturnType<typeof spawn>,
71+timeoutMs = 5_000,
72+): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
73+return new Promise((resolve, reject) => {
74+const timeout = setTimeout(() => {
75+reject(new Error("child did not close before timeout"));
76+}, timeoutMs);
77+child.once("close", (code, signal) => {
78+clearTimeout(timeout);
79+resolve({ code, signal });
80+});
81+});
82+}
83+6884describe("check-deadcode-unused-files", () => {
6985it("parses the compact Knip unused-file section", () => {
7086expect(
@@ -366,6 +382,71 @@ src/a.ts: src/a.ts
366382},
367383);
368384385+it.skipIf(process.platform === "win32")(
386+"cleans active Knip descendants before forwarding parent SIGTERM",
387+async () => {
388+const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-knip-parent-signal-"));
389+const childPidPath = path.join(root, "child.pid");
390+const readyPath = path.join(root, "child.ready");
391+const scriptUrl = pathToFileURL(path.resolve("scripts/check-deadcode-unused-files.mjs")).href;
392+let childPid = 0;
393+let runner: ReturnType<typeof spawn> | undefined;
394+395+try {
396+const childScript = [
397+"const fs = require('node:fs');",
398+"process.on('SIGTERM', () => {});",
399+`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
400+"setInterval(() => {}, 1000);",
401+].join("");
402+const parentScript = [
403+"const { spawn } = require('node:child_process');",
404+`spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
405+`require('node:fs').writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
406+"process.on('SIGTERM', () => process.exit(0));",
407+"setInterval(() => {}, 1000);",
408+].join("");
409+const runnerScript = [
410+"import { spawn } from 'node:child_process';",
411+`import { runKnipUnusedFiles } from ${JSON.stringify(scriptUrl)};`,
412+"await runKnipUnusedFiles({",
413+" spawnCommand(_command, _args, options) {",
414+` return spawn(process.execPath, ['-e', ${JSON.stringify(parentScript)}], options);`,
415+" },",
416+" timeoutMs: 60_000,",
417+" writeStatus: () => {},",
418+"});",
419+].join("\n");
420+421+runner = spawn(process.execPath, ["--input-type=module", "-e", runnerScript], {
422+cwd: process.cwd(),
423+stdio: ["ignore", "ignore", "pipe"],
424+});
425+426+await waitForFile(readyPath, 2_000);
427+await waitForFile(childPidPath, 2_000);
428+childPid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
429+expect(isProcessAlive(childPid)).toBe(true);
430+431+runner.kill("SIGTERM");
432+433+await expect(waitForChildClose(runner)).resolves.toEqual({
434+code: null,
435+signal: "SIGTERM",
436+});
437+await waitForDead(childPid, 2_000);
438+} finally {
439+if (runner?.pid && isProcessAlive(runner.pid)) {
440+runner.kill("SIGKILL");
441+}
442+if (childPid && isProcessAlive(childPid)) {
443+process.kill(childPid, "SIGKILL");
444+}
445+rmSync(root, { recursive: true, force: true });
446+}
447+},
448+);
449+369450it("keeps output delivered after process exit but before stdio close", async () => {
370451const child = new FakeKnipProcess();
371452const resultPromise = runKnipUnusedFiles({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。