openclaw/test/scripts/check-deadcode-reporting.test.ts
Peter Steinberger 10b6bf71e2
fix(test): release-gated fixtures still poll release files; add release and exit receipts to the fixture receipt helper (#163063)
Tests whose fixtures had to be told to proceed, or whose fixture exit could not be observed, still polled generated release and readiness files against 3-15 s wall-clock deadlines and failed on loaded hosts: the Vitest progress and report-owner alpha/beta fixtures, node-host workspace retention, release preflight scheduling, the upgrade-survivor systemd PTY startup gate, check-deadcode reporting (scanner barrier, compiler start/drain), and the PR git-maintenance hook.

The shared fixture receipt helper (test/helpers/fixture-receipts.ts) was one-way, so these sites were left on file-polling loops in earlier batches. It now owns both directions instead of a second broker: release(source, token) sends a retained release over the existing socket to connections associated with the source, and fixtures call awaitRelease(source, token), which keeps the fixture alive only while a release is pending; waitForExit(source) resolves when every connection that announced the source has closed (Node sockets are close-on-exec, so close means process exit for a fixture that never ends its socket); openFixtureReleaseFifo gives shell fixtures and synchronously blocked callers a POSIX FIFO release with no polling. A new test/helpers/fixture-receipts.test.ts covers exit receipts (normal exit, SIGKILL, late callers), release before and after the fixture waits, FIFO release of a shell child, close rejecting pending waits, and malformed input. The CI checkout backoff site was retired upstream by #162925. No product source, test timeout, product timeout, or assertion meaning changed.

Part of the polling audit from #162274. Proof on Blacksmith Testbox: original/fixed delay probes and forced-abort probes for each new waiting pattern, 80/80 standalone runs on the rebased head (20 per file including the helper test), 9/9 owning-shard replays, OpenGrep, deadcode, type-aware lint, base-aware timeout-race ratchet; Codex autoreview focused on the helper contract and ClawSweeper clean.
2026-10-01 16:05:20 -07:00

196 lines
7.2 KiB
TypeScript

import { spawn, type ChildProcess } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
resolveRuntimeWorkerArgv,
resolveRuntimeWorkerUrl,
} from "../../src/infra/runtime-worker-url.js";
import { createBoundedChildOutput } from "../helpers/bounded-child-output.js";
import {
fixtureReceiptClientSource,
openFixtureReceiptChannel,
} from "../helpers/fixture-receipts.js";
import { awaitGateBeforeSettlement, withinTest } from "../helpers/promise.js";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
import { toolingMtsEntrypoints } from "./tooling-mts-runtime.test-support.mts";
type Outcome = "failure" | "finding" | "success";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
describe("deadcode command reporting", () => {
it.for<{
wrapper: "exports" | "unused-files";
outcomes: Outcome[];
}>([
{ wrapper: "exports", outcomes: ["failure", "finding", "success"] },
{ wrapper: "unused-files", outcomes: ["failure", "finding"] },
{ wrapper: "unused-files", outcomes: ["success", "finding"] },
{ wrapper: "exports", outcomes: ["success", "success", "success"] },
{ wrapper: "unused-files", outcomes: ["success", "success"] },
])("reports every $wrapper outcome: $outcomes", async ({ wrapper, outcomes }, { signal }) => {
const root = tempDirs.make("openclaw-deadcode-reporting-");
const pnpm = path.join(root, "pnpm.mjs");
const scopes = ["production", "full-tree", "script"].slice(0, outcomes.length);
const configs = [
"config/knip.config.ts",
"config/knip.all-exports.config.ts",
"config/knip.scripts-exports.config.ts",
].slice(0, outcomes.length);
const kind = wrapper === "exports" ? "unused-export" : "unused-file";
const receipts = await openFixtureReceiptChannel();
let child: ChildProcess | undefined;
let completion:
| Promise<{
error: Error | undefined;
signal: NodeJS.Signals | null;
status: number | null;
stdout: string;
stderr: string;
}>
| undefined;
try {
// Synthetic Knip output only: keep the real CLI, launcher and child processes.
// No child completes until all scopes start, so serial launch fails the barrier.
writeFileSync(
pnpm,
`
${fixtureReceiptClientSource(receipts.endpoint)}
import fs from "node:fs";
import path from "node:path";
const args = process.argv.slice(2);
const configs = ${JSON.stringify(configs)};
const outcomes = ${JSON.stringify(outcomes)};
const index = configs.indexOf(args[args.indexOf("--config") + 1]);
if (index < 0) throw new Error("Unexpected scan config");
const marker = (i, phase) => path.join(${JSON.stringify(root)}, i + "." + phase);
fs.writeFileSync(marker(index, "started"), JSON.stringify(args));
sendReceipt(String(index), "started");
await awaitRelease(String(index), "run");
const outcome = outcomes[index];
if (outcome === "failure") {
console.error("SYNTHETIC_SCAN_FAILURE_" + index);
process.exitCode = 2;
} else if (outcome === "finding") {
console.log(args.includes("--files")
? "Unused files (1)\\nsrc/diagnostic-fixture.ts: src/diagnostic-fixture.ts"
: "Unused exports (1)\\nsrc/diagnostic-fixture.ts: syntheticFinding");
process.exitCode = 1;
}
fs.writeFileSync(marker(index, "completed"), outcome);
sendReceipt(String(index), "completed");
`,
);
child = spawn(
process.execPath,
resolveRuntimeWorkerArgv(
resolveRuntimeWorkerUrl(
wrapper === "exports"
? toolingMtsEntrypoints.deadcodeExports
: toolingMtsEntrypoints.deadcodeUnusedFiles,
),
),
{
cwd: process.cwd(),
env: { ...process.env, npm_execpath: pnpm },
stdio: ["ignore", "pipe", "pipe"],
},
);
const stdout = createBoundedChildOutput();
const stderr = createBoundedChildOutput();
child.stdout?.on("data", stdout.append);
child.stderr?.on("data", stderr.append);
completion = new Promise((resolve) => {
let error: Error | undefined;
child?.once("error", (cause) => {
error = cause;
});
child?.once("close", (status, exitSignal) => {
resolve({
error,
status,
signal: exitSignal,
stdout: stdout.text(),
stderr: stderr.text(),
});
});
});
const completed = completion;
const waitForPhase = (index: number, phase: string) =>
awaitGateBeforeSettlement(
receipts.waitFor(String(index), phase),
completed,
"Fixture concurrency barrier timed out",
).catch((error: unknown) => {
// Scan output and receipts use different pipes; its durable marker wins the race.
if (!existsSync(path.join(root, `${index}.${phase}`))) {
throw error;
}
});
await withinTest(
Promise.all(outcomes.map((_, index) => waitForPhase(index, "started"))),
signal,
);
for (const index of outcomes.keys()) {
receipts.release(String(index), "run");
await withinTest(waitForPhase(index, "completed"), signal);
}
const result = await withinTest(completed, signal);
const output = result.stdout + result.stderr;
expect(result.error, output).toBeUndefined();
expect(result.signal, output).toBeNull();
expect(result.status, output).toBe(
outcomes.every((outcome) => outcome === "success") ? 0 : 1,
);
for (const [index, outcome] of outcomes.entries()) {
expect(readFileSync(path.join(root, `${index}.completed`), "utf8")).toBe(outcome);
const scanArgs = ["--config", configs[index]];
if (index === 0) {
scanArgs.push("--production");
}
if (index === 2) {
scanArgs.push("--include-entry-exports");
}
expect(JSON.parse(readFileSync(path.join(root, `${index}.started`), "utf8"))).toEqual([
"dlx",
"--package",
"knip@6.32.2",
"knip",
...scanArgs,
"--no-progress",
"--reporter",
"compact",
...(wrapper === "exports"
? ["--include", "exports,nsExports,types,nsTypes,enumMembers,namespaceMembers"]
: ["--files"]),
"--no-config-hints",
]);
const scanName = `${scopes[index]} ${kind} scan`;
expect(
output.split("\n").filter((line) => line.includes(scanName)),
output,
).toHaveLength(1);
if (outcome === "success") {
expect(result.stdout).toContain(`[deadcode] Knip ${scanName} passed with 0 entries.`);
} else {
expect(result.stdout).not.toContain(scanName);
expect(result.stderr).toContain(
outcome === "failure" ? `SYNTHETIC_SCAN_FAILURE_${index}` : "src/diagnostic-fixture.ts",
);
}
}
} finally {
for (const index of outcomes.keys()) {
receipts.release(String(index), "run");
}
if (child?.exitCode === null && child.signalCode === null) {
child.kill("SIGTERM");
}
await completion;
await receipts.close();
}
});
});