import fs from "node:fs"; import path from "node:path"; /** * Entity models (`entities/models/.ts`) are pure domain logic — schemas, * invariants, derivations. They are the cheapest layer to test and the most * expensive to get wrong, so every model file must carry a sibling test. * * Scope is `entities/models/` only. Error classes (`entities/errors/`) are * conventionally covered by a consolidated `errors.test.ts`, and barrels * (`index.ts`) hold no logic — both are excluded. */ function isEntityModelFile(filename) { const normalized = filename.replace(/\\/g, "/"); if (!normalized.includes("/entities/models/")) return false; if (!normalized.endsWith(".ts")) return false; if (normalized.endsWith(".test.ts")) return false; if (normalized.endsWith(".d.ts")) return false; if (normalized.endsWith("/index.ts")) return false; return true; } /** @type {import("eslint").Rule.RuleModule} */ export default { meta: { type: "problem", docs: { description: "Every entity model file (entities/models/.ts) must have a sibling .test.ts.", }, schema: [], messages: { missingTest: "Entity model {{filename}} has no sibling test at {{expected}}. Entity models are pure domain logic — cover them with a unit test.", }, }, create(context) { return { Program(node) { const filename = context.filename; if (!isEntityModelFile(filename)) return; const expected = filename.replace(/\.ts$/, ".test.ts"); if (fs.existsSync(expected)) return; context.report({ node, messageId: "missingTest", data: { filename: path.basename(filename), expected: path.basename(expected), }, }); }, }; }, };