Skip to content

Commit c2c1a1b

Browse files
committed
test(deadcode): add keeper tests for the dead-export guard
1 parent 1cff13a commit c2c1a1b

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

‎scripts/check-dead-exports.test.ts‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import { spawnSync } from "node:child_process";
2+
import {
3+
existsSync,
4+
readdirSync,
5+
readFileSync,
6+
rmSync,
7+
writeFileSync,
8+
} from "node:fs";
9+
import { join, relative } from "node:path";
110
import { describe, expect, test } from "bun:test";
211

312
import {
@@ -8,6 +17,8 @@ import {
817
parseTsPruneLine,
918
} from "./check-dead-exports.js";
1019

20+
const repoRoot = join(import.meta.dir, "..");
21+
1122
// A probe dead export in one of the scoped files must fail the guard: the
1223
// exact-name exemptions cover only the five deferred-cleanup flags, never
1324
// the whole module.
@@ -116,3 +127,88 @@ describe("stale allowlist entries", () => {
116127
expect(outcome.unused).toEqual([]);
117128
});
118129
});
130+
131+
// The unit tests above prove the rule engine flags a probe; this one proves
132+
// the wired-up gate does. A temp probe export lands in the tsconfig-covered
133+
// scripts/ tree, the real guard runs as a subprocess, and the run must exit
134+
// nonzero naming the probe. The probe lives only for the test (never
135+
// committed) so the keep-alive check cannot itself become a dead export.
136+
describe("violation end to end", () => {
137+
test("a temp dead export fails the guard, which names it", () => {
138+
const probeFile = `dead-export-guard-probe-${process.pid}.ts`;
139+
const probeName = `deadExportGuardProbe${process.pid}`;
140+
const probePath = join(repoRoot, "scripts", probeFile);
141+
writeFileSync(probePath, `export const ${probeName} = 1;\n`);
142+
try {
143+
const ran = spawnSync(
144+
process.execPath,
145+
["scripts/check-dead-exports.ts"],
146+
{ cwd: repoRoot, encoding: "utf8" },
147+
);
148+
expect(ran.status).toBe(1);
149+
expect(`${ran.stdout}\n${ran.stderr}`).toContain(
150+
`${probeFile}: ${probeName}`,
151+
);
152+
} finally {
153+
rmSync(probePath, { force: true });
154+
}
155+
}, 60_000);
156+
});
157+
158+
// The purge deleted four fully-dead barrel files; a re-created barrel (or a
159+
// new import of its path) would silently resurrect the surface the guard was
160+
// built to shrink. Pin the paths, not the file text.
161+
describe("deleted barrels stay deleted", () => {
162+
const barrels = [
163+
"src/agent/directors/index.ts",
164+
"src/auth/codex/index.ts",
165+
"src/auth/xai/index.ts",
166+
"src/web/index.ts",
167+
];
168+
const barrelDirs = barrels.map((barrel) =>
169+
barrel.slice(0, -"/index.ts".length),
170+
);
171+
const barrelTails = [
172+
"agent/directors/index",
173+
"auth/codex/index",
174+
"auth/xai/index",
175+
"web/index",
176+
];
177+
const roots = ["src", "tests", "evals", "scripts", "packages"];
178+
179+
test("the barrel files do not exist", () => {
180+
for (const barrel of barrels) {
181+
expect(existsSync(join(repoRoot, barrel))).toBe(false);
182+
}
183+
});
184+
185+
test("no source file imports the deleted barrel paths", () => {
186+
const offenders: string[] = [];
187+
const walk = (dir: string): void => {
188+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
189+
const absolute = join(dir, entry.name);
190+
if (entry.isDirectory()) {
191+
walk(absolute);
192+
continue;
193+
}
194+
if (!entry.name.endsWith(".ts")) continue;
195+
const rel = relative(repoRoot, absolute).split("/").join("/");
196+
const dirRel = rel.slice(0, -entry.name.length - 1);
197+
const text = readFileSync(absolute, "utf8");
198+
const specifiers = [
199+
...text.matchAll(/(?:from\s*["']|import\s*\(\s*["'])([^"']+)["']/g),
200+
].map((match) => (match[1] ?? "").replace(/\.(?:js|ts)$/, ""));
201+
for (const spec of specifiers) {
202+
const hitsBarrel =
203+
barrelTails.some(
204+
(tail) => spec === tail || spec.endsWith(`/${tail}`),
205+
) ||
206+
(spec === "./index" && barrelDirs.includes(dirRel));
207+
if (hitsBarrel) offenders.push(`${rel}: ${spec}`);
208+
}
209+
}
210+
};
211+
for (const root of roots) walk(join(repoRoot, root));
212+
expect(offenders).toEqual([]);
213+
});
214+
});

0 commit comments

Comments
 (0)