Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-test-density.mjs
T

92 lines
3.4 KiB
JavaScript

#!/usr/bin/env node
// Anti-regression gate: asserts a minimum test-file-per-source-file density
// per workspace. Catches "route added without test" without requiring full
// coverage tooling. Floors are deliberate snapshots of the current ratio;
// they must only go down via deliberate commits (= more tests added).
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..");
const TARGETS = [
{ name: "apps/api", root: "apps/api/src", minRatio: 0.18 },
{ name: "apps/worker", root: "apps/worker/src", minRatio: 0.18 },
{ name: "apps/web app routes", root: "apps/web/app/routes", minRatio: 0.05 },
{ name: "apps/web app modules", root: "apps/web/app/modules", minRatio: 0.04 },
{ name: "apps/web app utils", root: "apps/web/app/utils", minRatio: 0.10 },
{ name: "packages/foundation", root: "packages/foundation/src", minRatio: 0.10 },
{ name: "packages/db", root: "packages/db/src", minRatio: 0.05 },
{ name: "packages/mail", root: "packages/mail/src", minRatio: 0.30 },
{ name: "packages/queue", root: "packages/queue/src", minRatio: 0.10 },
{ name: "packages/i18n", root: "packages/i18n/src", minRatio: 0.30 },
{ name: "packages/brand", root: "packages/brand/src", minRatio: 0.50 },
{ name: "packages/config", root: "packages/config/src", minRatio: 0.50 },
];
const ignoredDirs = new Set(["node_modules", "dist", "build", "coverage", ".react-router", "generated", "__generated__"]);
const sourcePattern = /\.(?:ts|tsx)$/;
const testPattern = /\.(?:test|spec)\.(?:ts|tsx)$/;
const declarationPattern = /\.d\.ts$/;
export function walk(dir, acc = { source: 0, test: 0 }) {
if (!fs.existsSync(dir)) return acc;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (ignoredDirs.has(entry.name) || entry.name.startsWith(".")) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full, acc);
continue;
}
if (!entry.isFile()) continue;
if (declarationPattern.test(entry.name)) continue;
if (testPattern.test(entry.name)) {
acc.test += 1;
} else if (sourcePattern.test(entry.name)) {
acc.source += 1;
}
}
return acc;
}
export function evaluateTargets(targets, rootDir = repoRoot) {
const failures = [];
const passes = [];
for (const target of targets) {
const counts = walk(path.join(rootDir, target.root));
if (counts.source === 0) {
passes.push(`SKIP ${target.name} no source files`);
continue;
}
const ratio = counts.test / counts.source;
const line = `${target.name} source=${counts.source} tests=${counts.test} ratio=${ratio.toFixed(3)} min=${target.minRatio.toFixed(3)}`;
if (ratio >= target.minRatio) {
passes.push(`PASS ${line}`);
} else {
failures.push(`FAIL ${line} (drift=${(ratio - target.minRatio).toFixed(3)})`);
}
}
return { failures, passes };
}
export function formatSummary(result) {
return `Summary: ${result.passes.length} passed, 0 observed, ${result.failures.length} failed`;
}
function main() {
const result = evaluateTargets(TARGETS);
for (const line of result.passes) console.log(line);
for (const line of result.failures) console.error(line);
console.log(formatSummary(result));
if (result.failures.length > 0) process.exit(1);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}