Catches manifest use cases that aren't wired through wireUseCase(...) in bind-production.ts / bind-dev-seed.ts. wireUseCase is the canonical helper that attaches __instrumented / __captured / __audited brands — skipping it produces an unbranded binding that assertFeatureConformance would reject at boot. This rule shifts that detection from ~3s (boot) to <1s (lint), keeping the layered conformance pattern: TS brands (compile), ESLint (lint), boot assertion (dev), smoke tests (CI). CLAUDE.md + conformance-quickref.md updated for the new rule (5 → 6).
173 lines
4.7 KiB
JavaScript
173 lines
4.7 KiB
JavaScript
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-be-wired.js";
|
|
|
|
function makeFixture({
|
|
manifestUseCases,
|
|
binderBody,
|
|
binderName = "bind-production.ts",
|
|
}) {
|
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "umbw-"));
|
|
const featureDir = path.join(repoRoot, "packages", "demo");
|
|
fs.mkdirSync(path.join(featureDir, "src", "di"), { recursive: true });
|
|
const useCasesObj = Object.entries(manifestUseCases)
|
|
.map(
|
|
([name, uc]) =>
|
|
` ${name}: { mutates: ${uc.mutates}, audits: [${uc.audits.map((a) => `"${a}"`).join(", ")}], publishes: [], consumes: [] },`,
|
|
)
|
|
.join("\n");
|
|
fs.writeFileSync(
|
|
path.join(featureDir, "src", "feature.manifest.ts"),
|
|
`export const demoManifest = defineFeature({
|
|
name: "demo",
|
|
requiredCores: [],
|
|
useCases: {
|
|
${useCasesObj}
|
|
},
|
|
realtimeChannels: [],
|
|
jobs: [],
|
|
} as const);`,
|
|
);
|
|
const binderFile = path.join(featureDir, "src", "di", binderName);
|
|
fs.writeFileSync(binderFile, binderBody);
|
|
return { repoRoot, binderFile };
|
|
}
|
|
|
|
const tester = new RuleTester({
|
|
languageOptions: {
|
|
parser: await import("@typescript-eslint/parser"),
|
|
ecmaVersion: "latest",
|
|
sourceType: "module",
|
|
},
|
|
});
|
|
|
|
describe("usecase-must-be-wired", () => {
|
|
it("passes when every manifest use case has a wireUseCase call", () => {
|
|
const { repoRoot, binderFile } = makeFixture({
|
|
manifestUseCases: {
|
|
signIn: { mutates: false, audits: [] },
|
|
signUp: { mutates: true, audits: ["user.created"] },
|
|
},
|
|
binderBody: `
|
|
export function bindProductionDemo(ctx) {
|
|
wireUseCase({ name: "signIn", factory: signInUseCase });
|
|
wireUseCase({ name: "signUp", factory: signUpUseCase });
|
|
}
|
|
`,
|
|
});
|
|
tester.run("usecase-must-be-wired", rule, {
|
|
valid: [
|
|
{
|
|
filename: binderFile,
|
|
code: fs.readFileSync(binderFile, "utf8"),
|
|
options: [{ repoRoot }],
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
it("fires when a manifest use case is missing a wireUseCase call", () => {
|
|
const { repoRoot, binderFile } = makeFixture({
|
|
manifestUseCases: {
|
|
signIn: { mutates: false, audits: [] },
|
|
signUp: { mutates: true, audits: [] },
|
|
},
|
|
binderBody: `
|
|
export function bindProductionDemo(ctx) {
|
|
wireUseCase({ name: "signIn", factory: signInUseCase });
|
|
}
|
|
`,
|
|
});
|
|
tester.run("usecase-must-be-wired", rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
filename: binderFile,
|
|
code: fs.readFileSync(binderFile, "utf8"),
|
|
options: [{ repoRoot }],
|
|
errors: [
|
|
{
|
|
messageId: "missing",
|
|
data: { useCase: "signUp", feature: "demo" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("also applies to bind-dev-seed.ts", () => {
|
|
const { repoRoot, binderFile } = makeFixture({
|
|
binderName: "bind-dev-seed.ts",
|
|
manifestUseCases: { signIn: { mutates: false, audits: [] } },
|
|
binderBody: `
|
|
export async function bindDevSeedDemo(ctx) {
|
|
// forgot to call wireUseCase
|
|
}
|
|
`,
|
|
});
|
|
tester.run("usecase-must-be-wired", rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
filename: binderFile,
|
|
code: fs.readFileSync(binderFile, "utf8"),
|
|
options: [{ repoRoot }],
|
|
errors: [
|
|
{
|
|
messageId: "missing",
|
|
data: { useCase: "signIn", feature: "demo" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("is a no-op on files outside src/di/bind-*.ts", () => {
|
|
const { repoRoot } = makeFixture({
|
|
manifestUseCases: { signIn: { mutates: false, audits: [] } },
|
|
binderBody: `// not a binder`,
|
|
});
|
|
const otherFile = path.join(
|
|
repoRoot,
|
|
"packages",
|
|
"demo",
|
|
"src",
|
|
"other.ts",
|
|
);
|
|
fs.writeFileSync(otherFile, `export const x = 1;`);
|
|
tester.run("usecase-must-be-wired", rule, {
|
|
valid: [
|
|
{
|
|
filename: otherFile,
|
|
code: fs.readFileSync(otherFile, "utf8"),
|
|
options: [{ repoRoot }],
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
it("is a no-op when the feature manifest declares no use cases", () => {
|
|
const { repoRoot, binderFile } = makeFixture({
|
|
manifestUseCases: {},
|
|
binderBody: `export function bindProductionDemo(ctx) {}`,
|
|
});
|
|
tester.run("usecase-must-be-wired", rule, {
|
|
valid: [
|
|
{
|
|
filename: binderFile,
|
|
code: fs.readFileSync(binderFile, "utf8"),
|
|
options: [{ repoRoot }],
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
});
|