usecase-must-be-wired returned {} when the manifest parsed to null,
silently disabling the error-level gate when the manifest existed but
could not be read. It now reports unparseableManifest on Program in
that case; a genuinely missing manifest stays a no-op (that is
feature-must-have-manifest's job).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
5.6 KiB
JavaScript
202 lines
5.6 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("reports unparseableManifest when the manifest exists but cannot be parsed", () => {
|
|
const { repoRoot, binderFile } = makeFixture({
|
|
manifestUseCases: {},
|
|
binderBody: `export function bindProductionDemo(ctx) {}`,
|
|
});
|
|
// Overwrite the manifest with a shape the AST walker cannot read
|
|
// (no defineFeature call) — the gate must fail loudly, not no-op.
|
|
fs.writeFileSync(
|
|
path.join(repoRoot, "packages", "demo", "src", "feature.manifest.ts"),
|
|
`export const demoManifest = { name: "demo" };`,
|
|
);
|
|
tester.run("usecase-must-be-wired", rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
filename: binderFile,
|
|
code: fs.readFileSync(binderFile, "utf8"),
|
|
options: [{ repoRoot }],
|
|
errors: [
|
|
{
|
|
messageId: "unparseableManifest",
|
|
data: { feature: "demo" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
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: [],
|
|
});
|
|
});
|
|
});
|