#!/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 { 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 (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(); 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(); 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();