diff --git a/packages/core-eslint/rules/usecase-must-have-test-file.js b/packages/core-eslint/rules/usecase-must-have-test-file.js new file mode 100644 index 0000000..c7e3b71 --- /dev/null +++ b/packages/core-eslint/rules/usecase-must-have-test-file.js @@ -0,0 +1,32 @@ +import fs from "node:fs"; + +/** @type {import("eslint").Rule.RuleModule} */ +export default { + meta: { + type: "problem", + docs: { + description: + "Every *.use-case.ts file must have a sibling *.use-case.test.ts (TDD discipline).", + }, + schema: [], + messages: { + missingTestFile: + "Use case {{filename}} has no sibling test file at {{expected}}. Write the red test first.", + }, + }, + create(context) { + return { + Program(node) { + const filename = context.filename; + if (!filename.endsWith(".use-case.ts")) return; + const expected = filename.replace(/\.use-case\.ts$/, ".use-case.test.ts"); + if (fs.existsSync(expected)) return; + context.report({ + node, + messageId: "missingTestFile", + data: { filename: filename.split("/").pop(), expected: expected.split("/").pop() }, + }); + }, + }; + }, +}; diff --git a/packages/core-eslint/rules/usecase-must-have-test-file.test.js b/packages/core-eslint/rules/usecase-must-have-test-file.test.js new file mode 100644 index 0000000..fc870f1 --- /dev/null +++ b/packages/core-eslint/rules/usecase-must-have-test-file.test.js @@ -0,0 +1,42 @@ +import { describe, it } from "vitest"; +import { RuleTester } from "eslint"; +import path from "node:path"; +import os from "node:os"; +import fs from "node:fs"; +import rule from "./usecase-must-have-test-file.js"; + +function makeUseCaseFixture({ withTest }) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "umht-")); + const useCase = path.join(dir, "sign-in.use-case.ts"); + fs.writeFileSync(useCase, `export const signInUseCase = () => async () => {};`); + if (withTest) { + fs.writeFileSync(path.join(dir, "sign-in.use-case.test.ts"), `import { it } from "vitest"; it("works", () => {});`); + } + return { useCase }; +} + +const tester = new RuleTester({ languageOptions: { ecmaVersion: "latest", sourceType: "module" } }); + +describe("usecase-must-have-test-file", () => { + it("passes when a sibling .test.ts exists", () => { + const { useCase } = makeUseCaseFixture({ withTest: true }); + tester.run("usecase-must-have-test-file", rule, { + valid: [{ filename: useCase, code: fs.readFileSync(useCase, "utf8") }], + invalid: [], + }); + }); + + it("fires when no sibling test file exists", () => { + const { useCase } = makeUseCaseFixture({ withTest: false }); + tester.run("usecase-must-have-test-file", rule, { + valid: [], + invalid: [ + { + filename: useCase, + code: fs.readFileSync(useCase, "utf8"), + errors: [{ messageId: "missingTestFile" }], + }, + ], + }); + }); +});