// Check Deadcode Unused Files tests cover check deadcode unused files script behavior. import { spawn } from "node:child_process"; import { EventEmitter } from "node:events"; import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { checkUnusedFiles, compareUnusedFilesToAllowlist, KNIP_MAX_BUFFER_BYTES, parseKnipCompactUnusedFiles, runKnipUnusedFiles, } from "../../scripts/check-deadcode-unused-files.mjs"; class FakeKnipProcess extends EventEmitter { readonly stderr = new EventEmitter(); readonly stdout = new EventEmitter(); pid = 12345; } function finishFakeProcess( child: FakeKnipProcess, status: number | null, signal: NodeJS.Signals | null, ): void { child.emit("exit", status, signal); child.emit("close", status, signal); } function isProcessAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } async function sleep(ms: number): Promise { await new Promise((resolve) => { setTimeout(resolve, ms); }); } async function waitForFile(filePath: string, timeoutMs: number): Promise { const deadlineAt = Date.now() + timeoutMs; while (Date.now() < deadlineAt) { if (existsSync(filePath)) { return; } await sleep(25); } throw new Error(`timeout waiting for ${filePath}`); } async function waitForDead(pid: number, timeoutMs: number): Promise { const deadlineAt = Date.now() + timeoutMs; while (Date.now() < deadlineAt) { if (!isProcessAlive(pid)) { return; } await sleep(25); } throw new Error(`process still alive: ${pid}`); } describe("check-deadcode-unused-files", () => { it("parses the compact Knip unused-file section", () => { expect( parseKnipCompactUnusedFiles(` > openclaw@2026.4.27 deadcode:knip /repo > pnpm dlx knip --reporter compact --files Unused files (2) src/b.ts: src/b.ts src/a.ts: src/a.ts Unused dependencies (1) left-pad: package.json `), ).toEqual(["src/a.ts", "src/b.ts"]); }); it("parses Knip's files-only compact output", () => { expect(parseKnipCompactUnusedFiles("src/b.ts: src/b.ts\nsrc/a.ts: src/a.ts\n")).toEqual([ "src/a.ts", "src/b.ts", ]); }); it("ignores pnpm dlx progress lines in files-only compact output", () => { expect( parseKnipCompactUnusedFiles(` Progress: resolved 21, reused 0, downloaded 0, added 0 src/b.ts: src/b.ts Progress: resolved 65, reused 20, downloaded 1, added 21, done src/a.ts: src/a.ts `), ).toEqual(["src/a.ts", "src/b.ts"]); }); it("reports unexpected and stale allowlist entries", () => { expect( compareUnusedFilesToAllowlist(["src/a.ts", "src/new.ts"], ["src/a.ts", "src/old.ts"]), ).toStrictEqual({ actual: ["src/a.ts", "src/new.ts"], allowed: ["src/a.ts", "src/old.ts"], unexpected: ["src/new.ts"], stale: ["src/old.ts"], duplicateAllowedCount: 0, allowlistIsSorted: true, }); }); it("accepts optional allowlist entries whether Knip reports them or not", () => { expect( compareUnusedFilesToAllowlist( ["src/a.ts", "src/platform.ts"], ["src/a.ts"], ["src/platform.ts"], ), ).toStrictEqual({ actual: ["src/a.ts", "src/platform.ts"], allowed: ["src/a.ts"], allowlistIsSorted: true, duplicateAllowedCount: 0, unexpected: [], stale: [], }); expect( compareUnusedFilesToAllowlist(["src/a.ts"], ["src/a.ts"], ["src/platform.ts"]), ).toStrictEqual({ actual: ["src/a.ts"], allowed: ["src/a.ts"], allowlistIsSorted: true, duplicateAllowedCount: 0, unexpected: [], stale: [], }); }); it("accepts exactly allowlisted unused files", () => { expect(checkUnusedFiles("Unused files (1)\nsrc/a.ts: src/a.ts\n", ["src/a.ts"])).toStrictEqual({ comparison: { actual: ["src/a.ts"], allowed: ["src/a.ts"], allowlistIsSorted: true, duplicateAllowedCount: 0, stale: [], unexpected: [], }, ok: true, message: "", }); }); it("rejects unsorted allowlists", () => { expect( compareUnusedFilesToAllowlist(["src/a.ts", "src/b.ts"], ["src/b.ts", "src/a.ts"]), ).toStrictEqual({ actual: ["src/a.ts", "src/b.ts"], allowed: ["src/a.ts", "src/b.ts"], allowlistIsSorted: false, duplicateAllowedCount: 0, stale: [], unexpected: [], }); }); 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({ env: { PATH: "" }, npmExecPath: "", platform: "linux", spawnCommand(command: string, args: string[], options: unknown) { calls.push({ args, command, options }); const child = new FakeKnipProcess(); queueMicrotask(() => finishFakeProcess(child, 0, null)); return child; }, writeStatus: () => {}, }); await resultPromise; const call = calls[0] as { command: string }; expect(path.basename(call.command)).toBe("pnpm"); expect(call).toMatchObject({ args: [ "--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", ], options: { detached: process.platform !== "win32", shell: false, stdio: ["ignore", "pipe", "pipe"], }, }); }); it("emits heartbeat status and reports Knip timeouts", async () => { const statuses: string[] = []; const child = new FakeKnipProcess(); const originalKill = process.kill.bind(process); const kills: Array = []; process.kill = ((pid: number, signal?: NodeJS.Signals | number) => { if (Math.abs(pid) === child.pid) { if (signal === 0) { throw Object.assign(new Error("gone"), { code: "ESRCH" }); } kills.push(signal); finishFakeProcess(child, null, (signal as NodeJS.Signals | undefined) ?? "SIGTERM"); return true; } return originalKill(pid, signal as NodeJS.Signals); }) as typeof process.kill; try { const result = await runKnipUnusedFiles({ heartbeatMs: 1, killGraceMs: 50, maxBufferBytes: KNIP_MAX_BUFFER_BYTES, spawnCommand: () => child, timeoutMs: 5, writeStatus: (message: string) => statuses.push(message), }); expect(statuses.some((message) => message.includes("still running"))).toBe(true); expect(statuses.some((message) => message.includes("timed out"))).toBe(true); expect(kills).toContain("SIGTERM"); expect(result).toStrictEqual({ errorCode: "ETIMEDOUT", errorMessage: expect.stringContaining("Knip unused-file scan timed out"), output: "", signal: "SIGTERM", status: null, }); } finally { process.kill = originalKill; } }); it.skipIf(process.platform === "win32")( "waits for timed-out Knip process groups after the wrapper exits", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-knip-timeout-")); const childPidPath = path.join(root, "child.pid"); let childPid = 0; try { const childScript = [ "process.on('SIGTERM', () => {});", "setInterval(() => {}, 1000);", ].join(""); const parentScript = [ "const { spawn } = require('node:child_process');", "const fs = require('node:fs');", `const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`, "fs.writeFileSync(process.env.OPENCLAW_TEST_CHILD_PID, String(child.pid));", "process.on('SIGTERM', () => process.exit(0));", "setInterval(() => {}, 1000);", ].join(""); const resultPromise = runKnipUnusedFiles({ env: { ...process.env, OPENCLAW_TEST_CHILD_PID: childPidPath }, killGraceMs: 50, spawnCommand(_command: string, _args: string[], options: unknown) { return spawn(process.execPath, ["-e", parentScript], { ...(options as Parameters[2]), env: { ...process.env, OPENCLAW_TEST_CHILD_PID: childPidPath }, }); }, timeoutMs: 100, writeStatus: () => {}, }); await waitForFile(childPidPath, 2_000); childPid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10); expect(isProcessAlive(childPid)).toBe(true); await expect(resultPromise).resolves.toMatchObject({ errorCode: "ETIMEDOUT", }); await waitForDead(childPid, 2_000); } finally { if (childPid && isProcessAlive(childPid)) { process.kill(childPid, "SIGKILL"); } rmSync(root, { recursive: true, force: true }); } }, ); it("keeps output delivered after process exit but before stdio close", async () => { const child = new FakeKnipProcess(); const resultPromise = runKnipUnusedFiles({ spawnCommand: () => child, writeStatus: () => {}, }); child.stdout.emit("data", "before-exit\n"); child.emit("exit", 0, null); child.stdout.emit("data", "after-exit\n"); child.emit("close", 0, null); await expect(resultPromise).resolves.toStrictEqual({ errorCode: undefined, errorMessage: undefined, output: "before-exit\nafter-exit\n", signal: null, status: 0, }); }); it("bounds captured Knip output", async () => { const child = new FakeKnipProcess(); const originalKill = process.kill.bind(process); process.kill = ((pid: number, signal?: NodeJS.Signals | number) => { if (Math.abs(pid) === child.pid) { if (signal === 0) { throw Object.assign(new Error("gone"), { code: "ESRCH" }); } finishFakeProcess(child, null, (signal as NodeJS.Signals | undefined) ?? "SIGTERM"); return true; } return originalKill(pid, signal as NodeJS.Signals); }) as typeof process.kill; try { const resultPromise = runKnipUnusedFiles({ killGraceMs: 50, maxBufferBytes: 4, spawnCommand: () => child, timeoutMs: 1000, writeStatus: () => {}, }); child.stdout.emit("data", "too much output"); await expect(resultPromise).resolves.toStrictEqual({ errorCode: "ENOBUFS", errorMessage: "Knip unused-file scan exceeded 4 output bytes", output: "too ", signal: "SIGTERM", status: null, }); } finally { process.kill = originalKill; } }); it("reports spawn errors", async () => { const resultPromise = runKnipUnusedFiles({ spawnCommand: () => { const child = new FakeKnipProcess(); queueMicrotask(() => child.emit( "error", Object.assign(new Error("spawn pnpm ENOENT"), { code: "ENOENT", }), ), ); return child; }, writeStatus: () => {}, }); await expect(resultPromise).resolves.toStrictEqual({ errorCode: "ENOENT", errorMessage: "spawn pnpm ENOENT", output: "", signal: null, status: null, }); }); });