Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
531
test/scripts/check-deadcode-unused-files.test.ts
Normal file
531
test/scripts/check-deadcode-unused-files.test.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
// 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 { pathToFileURL } from "node:url";
|
||||
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<void> {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
|
||||
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<void> {
|
||||
const deadlineAt = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadlineAt) {
|
||||
if (!isProcessAlive(pid)) {
|
||||
return;
|
||||
}
|
||||
await sleep(25);
|
||||
}
|
||||
throw new Error(`process still alive: ${pid}`);
|
||||
}
|
||||
|
||||
function waitForChildClose(
|
||||
child: ReturnType<typeof spawn>,
|
||||
timeoutMs = 5_000,
|
||||
): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("child did not close before timeout"));
|
||||
}, timeoutMs);
|
||||
child.once("close", (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ code, signal });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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<NodeJS.Signals | number | undefined> = [];
|
||||
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<typeof spawn>[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.skipIf(process.platform === "win32")(
|
||||
"cleans active Knip descendants before forwarding parent SIGTERM",
|
||||
async () => {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-knip-parent-signal-"));
|
||||
const childPidPath = path.join(root, "child.pid");
|
||||
const readyPath = path.join(root, "child.ready");
|
||||
const scriptUrl = pathToFileURL(path.resolve("scripts/check-deadcode-unused-files.mjs")).href;
|
||||
let childPid = 0;
|
||||
let runner: ReturnType<typeof spawn> | undefined;
|
||||
|
||||
try {
|
||||
const childScript = [
|
||||
"const fs = require('node:fs');",
|
||||
"process.on('SIGTERM', () => {});",
|
||||
`fs.writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
const parentScript = [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
`spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
|
||||
`require('node:fs').writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
const runnerScript = [
|
||||
"import { spawn } from 'node:child_process';",
|
||||
`import { runKnipUnusedFiles } from ${JSON.stringify(scriptUrl)};`,
|
||||
"await runKnipUnusedFiles({",
|
||||
" spawnCommand(_command, _args, options) {",
|
||||
` return spawn(process.execPath, ['-e', ${JSON.stringify(parentScript)}], options);`,
|
||||
" },",
|
||||
" timeoutMs: 60_000,",
|
||||
" writeStatus: () => {},",
|
||||
"});",
|
||||
].join("\n");
|
||||
|
||||
runner = spawn(process.execPath, ["--input-type=module", "-e", runnerScript], {
|
||||
cwd: process.cwd(),
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
await waitForFile(readyPath, 2_000);
|
||||
await waitForFile(childPidPath, 2_000);
|
||||
childPid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
|
||||
expect(isProcessAlive(childPid)).toBe(true);
|
||||
|
||||
runner.kill("SIGTERM");
|
||||
|
||||
await expect(waitForChildClose(runner)).resolves.toEqual({
|
||||
code: null,
|
||||
signal: "SIGTERM",
|
||||
});
|
||||
await waitForDead(childPid, 2_000);
|
||||
} finally {
|
||||
if (runner?.pid && isProcessAlive(runner.pid)) {
|
||||
runner.kill("SIGKILL");
|
||||
}
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user