diff --git a/scripts/check-deadcode-unused-files.mjs b/scripts/check-deadcode-unused-files.mjs index 81ec79055275..cb3fe93c33b8 100644 --- a/scripts/check-deadcode-unused-files.mjs +++ b/scripts/check-deadcode-unused-files.mjs @@ -5,6 +5,7 @@ import { KNIP_OPTIONAL_UNUSED_FILE_ALLOWLIST, KNIP_UNUSED_FILE_ALLOWLIST, } from "./deadcode-unused-files.allowlist.mjs"; +import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs"; const KNIP_VERSION = "6.8.0"; export const KNIP_TIMEOUT_MS = 10 * 60 * 1000; @@ -158,7 +159,17 @@ export async function runKnipUnusedFiles(params = {}) { let exitStatus = null; let exitSignal = null; - const child = run("pnpm", args, { + const pnpm = createPnpmRunnerSpawnSpec({ + detached: process.platform !== "win32", + env: params.env, + nodeExecPath: params.nodeExecPath, + npmExecPath: params.npmExecPath, + platform: params.platform, + pnpmArgs: args, + stdio: ["ignore", "pipe", "pipe"], + }); + const child = run(pnpm.command, pnpm.args, { + ...pnpm.options, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], }); diff --git a/test/scripts/check-deadcode-unused-files.test.ts b/test/scripts/check-deadcode-unused-files.test.ts index 040f2150fb1b..c7f3608f2bfc 100644 --- a/test/scripts/check-deadcode-unused-files.test.ts +++ b/test/scripts/check-deadcode-unused-files.test.ts @@ -1,4 +1,7 @@ import { EventEmitter } from "node:events"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import { checkUnusedFiles, @@ -128,24 +131,82 @@ src/a.ts: src/a.ts it("runs Knip through a process-group-aware subprocess", async () => { const calls: unknown[] = []; + const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-knip-runner-")); + const pnpmExecPath = path.join(root, "pnpm.cjs"); + writeFileSync(pnpmExecPath, "console.log('pnpm');\n", "utf8"); + + try { + const resultPromise = runKnipUnusedFiles({ + nodeExecPath: "/test-node", + npmExecPath: pnpmExecPath, + spawnCommand(command: string, args: string[], options: unknown) { + calls.push({ args, command, options }); + const child = new FakeKnipProcess(); + queueMicrotask(() => { + child.stdout.emit("data", "partial stdout"); + child.stderr.emit("data", "partial stderr"); + finishFakeProcess(child, 0, null); + }); + return child; + }, + writeStatus: () => {}, + }); + + const result = await resultPromise; + + expect(calls).toHaveLength(1); + expect(calls[0]).toMatchObject({ + args: [ + pnpmExecPath, + "--config.minimum-release-age=0", + "dlx", + "--package", + "knip@6.8.0", + "knip", + "--config", + "config/knip.config.ts", + "--production", + "--no-progress", + "--reporter", + "compact", + "--files", + "--no-config-hints", + ], + command: "/test-node", + options: { + detached: process.platform !== "win32", + shell: false, + stdio: ["ignore", "pipe", "pipe"], + }, + }); + expect(result).toStrictEqual({ + errorCode: undefined, + errorMessage: undefined, + output: "partial stdoutpartial stderr", + signal: null, + status: 0, + }); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("falls back to bare pnpm when no managed pnpm runner is available", async () => { + const calls: unknown[] = []; const resultPromise = runKnipUnusedFiles({ + npmExecPath: "", spawnCommand(command: string, args: string[], options: unknown) { calls.push({ args, command, options }); const child = new FakeKnipProcess(); - queueMicrotask(() => { - child.stdout.emit("data", "partial stdout"); - child.stderr.emit("data", "partial stderr"); - finishFakeProcess(child, 0, null); - }); + queueMicrotask(() => finishFakeProcess(child, 0, null)); return child; }, writeStatus: () => {}, }); - const result = await resultPromise; + await resultPromise; - expect(calls).toHaveLength(1); expect(calls[0]).toMatchObject({ args: [ "--config.minimum-release-age=0", @@ -165,22 +226,16 @@ src/a.ts: src/a.ts command: "pnpm", options: { detached: process.platform !== "win32", + shell: false, stdio: ["ignore", "pipe", "pipe"], }, }); - expect(result).toStrictEqual({ - errorCode: undefined, - errorMessage: undefined, - output: "partial stdoutpartial stderr", - signal: null, - status: 0, - }); }); it("emits heartbeat status and reports Knip timeouts", async () => { const statuses: string[] = []; const child = new FakeKnipProcess(); - const originalKill = process.kill; + const originalKill = process.kill.bind(process); const kills: Array = []; process.kill = ((pid: number, signal?: NodeJS.Signals | number) => { if (Math.abs(pid) === child.pid) { @@ -238,7 +293,7 @@ src/a.ts: src/a.ts it("bounds captured Knip output", async () => { const child = new FakeKnipProcess(); - const originalKill = process.kill; + const originalKill = process.kill.bind(process); process.kill = ((pid: number, signal?: NodeJS.Signals | number) => { if (Math.abs(pid) === child.pid) { finishFakeProcess(child, null, (signal as NodeJS.Signals | undefined) ?? "SIGTERM");