Two CLAUDE.md conventions had no mechanical gate, so both drifted: entity models shipped without sibling tests, and feature test files imported src modules via `../` instead of the `@/` alias. - `entity-must-have-test` — every entities/models/<x>.ts needs a sibling <x>.test.ts (errors and barrels excluded). - `no-relative-parent-import-in-tests` — feature test files must import src via `@/`, not `../`. Scoped to feature packages; core packages are governed by their own generator templates. Both register at warn level, bringing the conformance rule count to 15.
56 lines
1.8 KiB
JavaScript
56 lines
1.8 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
/**
|
|
* Entity models (`entities/models/<x>.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/<x>.ts) must have a sibling <x>.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),
|
|
},
|
|
});
|
|
},
|
|
};
|
|
},
|
|
};
|