Files
suaveplan/scripts/audit-test-helper-usage.ts
2026-05-27 23:47:53 +01:00

220 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
/**
* Layer 2 audit: scan every package's tests for opportunities to use
* @suaveplan/testing helpers (FakeClock, Factory, FixtureManager, MemoryStore)
* that are currently being hand-rolled.
*
* Heuristics (each one is a strong signal, not a guarantee):
*
* FakeClock — test files using `new Date(`, `Date.now(`, or
* `setTimeout(` without already importing `FakeClock`.
* Factory — test files with ≥3 occurrences of an inline literal
* object that has the shape of test data (id/name/etc).
* FixtureManager — test files with paired `beforeEach` + `afterEach`
* blocks doing setup+cleanup.
* MemoryStore — test files defining `const store = new Map(` or
* `const cache = {}` and treating it like a kv store.
*
* Output: a per-package report listing potential opportunities, NOT
* automatic edits. The fix list goes to stdout; the user reviews and
* applies selectively.
*
* Usage:
* bun scripts/audit-test-helper-usage.ts # full report
* bun scripts/audit-test-helper-usage.ts --json # machine-readable
* bun scripts/audit-test-helper-usage.ts --summary # counts only
*/
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
const repoRoot = "/mnt/s/web/genesis";
const packagesRoot = join(repoRoot, "packages");
interface Finding {
pkg: string;
file: string;
helper: "FakeClock" | "Factory" | "FixtureManager" | "MemoryStore";
signal: string;
line: number;
}
const args = process.argv.slice(2);
const jsonMode = args.includes("--json");
const summaryMode = args.includes("--summary");
function* walkTestFiles(dir: string): Generator<string> {
if (!existsSync(dir) || !statSync(dir).isDirectory()) return;
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const s = statSync(p);
if (s.isDirectory()) {
if (
name === "node_modules" ||
name === "dist" ||
name === ".turbo" ||
name === "coverage"
)
continue;
yield* walkTestFiles(p);
} else if (
s.isFile() &&
(name.endsWith(".test.ts") || name.endsWith(".test.tsx"))
) {
yield p;
}
}
}
function scan(file: string, pkg: string): Finding[] {
const out: Finding[] = [];
const content = readFileSync(file, "utf8");
const lines = content.split("\n");
const usesFakeClock = /import\s+\{[^}]*\bFakeClock\b/.test(content);
const usesFactory = /import\s+\{[^}]*\b(Factory|defineFactory)\b/.test(
content
);
const usesFixtureMgr = /import\s+\{[^}]*\bFixtureManager\b/.test(content);
const usesMemoryStore = /import\s+\{[^}]*\bMemoryStore\b/.test(content);
let beforeEach = 0;
let afterEach = 0;
let dateNow = 0;
let newDate = 0;
let setTimeoutCount = 0;
let _mapAsStore = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i] ?? "";
if (/\bnew\s+Date\s*\(/.test(line)) {
if (newDate === 0 && !usesFakeClock) {
out.push({
pkg,
file,
helper: "FakeClock",
signal: "uses `new Date(` directly",
line: i + 1,
});
}
newDate += 1;
}
if (/\bDate\.now\s*\(/.test(line) && !usesFakeClock) {
if (dateNow === 0)
out.push({
pkg,
file,
helper: "FakeClock",
signal: "uses `Date.now()` directly",
line: i + 1,
});
dateNow += 1;
}
if (/\bsetTimeout\s*\(/.test(line) && !usesFakeClock) {
if (setTimeoutCount === 0)
out.push({
pkg,
file,
helper: "FakeClock",
signal: "uses real `setTimeout` (consider `FakeClock.tick`)",
line: i + 1,
});
setTimeoutCount += 1;
}
if (/\bbeforeEach\s*\(/.test(line)) beforeEach += 1;
if (/\bafterEach\s*\(/.test(line)) afterEach += 1;
if (
/(const|let)\s+\w+\s*[:=]\s*new\s+Map<.*string/.test(line) &&
!usesMemoryStore
) {
out.push({
pkg,
file,
helper: "MemoryStore",
signal: "hand-rolled Map<string,…> (consider `MemoryStore`)",
line: i + 1,
});
_mapAsStore += 1;
}
}
if (beforeEach >= 1 && afterEach >= 1 && !usesFixtureMgr) {
out.push({
pkg,
file,
helper: "FixtureManager",
signal: `paired beforeEach/afterEach × ${Math.min(beforeEach, afterEach)} (consider \`FixtureManager\`)`,
line: 0,
});
}
const literalCount = (content.match(/\{\s*id:\s*["'][^"']+["']/g) ?? [])
.length;
if (literalCount >= 3 && !usesFactory) {
out.push({
pkg,
file,
helper: "Factory",
signal: `${literalCount} inline {id: "…"} test-data literals (consider \`defineFactory\`)`,
line: 0,
});
}
return out;
}
function main(): void {
const findings: Finding[] = [];
const packagesScanned: string[] = [];
for (const plugin of readdirSync(packagesRoot)) {
const pluginPath = join(packagesRoot, plugin);
if (!statSync(pluginPath).isDirectory()) continue;
for (const pkg of readdirSync(pluginPath)) {
const pkgPath = join(pluginPath, pkg);
if (!statSync(pkgPath).isDirectory()) continue;
if (!existsSync(join(pkgPath, "package.json"))) continue;
const id = `${plugin}/${pkg}`;
packagesScanned.push(id);
for (const file of walkTestFiles(pkgPath)) {
findings.push(...scan(file, id));
}
}
}
if (jsonMode) {
process.stdout.write(
`${JSON.stringify({ packagesScanned: packagesScanned.length, findings }, null, 2)}\n`
);
return;
}
const byHelper = new Map<string, number>();
for (const f of findings)
byHelper.set(f.helper, (byHelper.get(f.helper) ?? 0) + 1);
console.log(`Scanned ${packagesScanned.length} packages.`);
console.log(`Findings: ${findings.length}`);
console.log("");
console.log("By helper:");
for (const [h, n] of [...byHelper.entries()].sort((a, b) => b[1] - a[1])) {
console.log(` ${h.padEnd(16)} ${n}`);
}
if (summaryMode) return;
const byPkg = new Map<string, Finding[]>();
for (const f of findings) {
if (!byPkg.has(f.pkg)) byPkg.set(f.pkg, []);
byPkg.get(f.pkg)?.push(f);
}
const ranked = [...byPkg.entries()].sort(
(a, b) => b[1].length - a[1].length
);
console.log("");
console.log("Top 20 packages by finding count:");
for (const [pkg, fs] of ranked.slice(0, 20)) {
console.log(` ${pkg.padEnd(40)} ${fs.length}`);
}
}
main();