eslint.config.test.ts
Rama main · réplica de lectura del repositorio original.
import { ESLint } from "eslint";
import { describe, expect, test } from "vitest";
describe("eslint custom rules", () => {
test("flags raw process.env outside the env boundary", async () => {
const eslint = new ESLint();
const results = await eslint.lintText("export const x = process.env.SECRET;\n", {
filePath: "src/features/demo/leak.ts",
});
const messages = results[0]!.messages.map((m) => m.message).join(" ");
expect(messages).toContain("Read env only via src/config/env.ts");
});
test("does NOT flag process.env inside src/config/env.ts", async () => {
const eslint = new ESLint();
const results = await eslint.lintText("export const x = process.env.SECRET;\n", {
filePath: "src/config/env.ts",
});
const envMessages = results[0]!.messages
.filter((m) => m.message.includes("Read env only via"))
.map((m) => m.message);
expect(envMessages).toHaveLength(0);
});
test("flags raw throw new Error in app code", async () => {
const eslint = new ESLint();
const results = await eslint.lintText("export function f(): void { throw new Error('x'); }\n", {
filePath: "src/features/demo/boom.ts",
});
const messages = results[0]!.messages.map((m) => m.message).join(" ");
expect(messages).toContain("Throw AppError");
});
test("does NOT flag throw new Error in src/types/result.ts", async () => {
const eslint = new ESLint();
const results = await eslint.lintText(
"export function assertNever(x: never): never { throw new Error('Unexpected value'); }\n",
{ filePath: "src/types/result.ts" },
);
const syntaxMessages = results[0]!.messages
.filter((m) => m.message.includes("Throw AppError"))
.map((m) => m.message);
expect(syntaxMessages).toHaveLength(0);
});
test("does NOT flag throw new Error in src/config/env.ts", async () => {
const eslint = new ESLint();
const results = await eslint.lintText(
"export function loadOrThrow(): never { throw new Error('Invalid environment'); }\n",
{ filePath: "src/config/env.ts" },
);
const syntaxMessages = results[0]!.messages
.filter((m) => m.message.includes("Throw AppError"))
.map((m) => m.message);
expect(syntaxMessages).toHaveLength(0);
});
test("does NOT flag throw new Error in test files", async () => {
const eslint = new ESLint();
const results = await eslint.lintText("test('x', () => { throw new Error('fail'); });\n", {
filePath: "src/features/demo/boom.test.ts",
});
const syntaxMessages = results[0]!.messages
.filter((m) => m.message.includes("Throw AppError"))
.map((m) => m.message);
expect(syntaxMessages).toHaveLength(0);
});
test("flags fetch() call without a signal argument", async () => {
const eslint = new ESLint();
const results = await eslint.lintText(
"export async function f() { return fetch('https://example.com/api'); }\n",
{ filePath: "src/features/demo/call.ts" },
);
const messages = results[0]!.messages.map((m) => m.message).join(" ");
expect(messages).toContain("fetch() must pass { signal }");
});
test("does NOT flag fetch() called with a second (options) argument", async () => {
const eslint = new ESLint();
const results = await eslint.lintText(
"export async function f(s: AbortSignal) { return fetch('https://example.com/api', { signal: s }); }\n",
{ filePath: "src/features/demo/call.ts" },
);
const fetchMessages = results[0]!.messages
.filter((m) => m.message.includes("fetch() must pass { signal }"))
.map((m) => m.message);
expect(fetchMessages).toHaveLength(0);
});
test("flags a hand-rolled inline <svg> in app code", async () => {
const eslint = new ESLint();
const results = await eslint.lintText(
'export function Icon() { return <svg viewBox="0 0 24 24" />; }\n',
{ filePath: "src/components/shell/NavIcons.tsx" },
);
const messages = results[0]!.messages.map((m) => m.message).join(" ");
expect(messages).toContain("Inline <svg> icons are banned");
});
test("does NOT flag an inline <svg> inside src/components/ui/**", async () => {
const eslint = new ESLint();
const results = await eslint.lintText(
'export function Icon() { return <svg viewBox="0 0 24 24" />; }\n',
{ filePath: "src/components/ui/dropdown-menu.tsx" },
);
const svgMessages = results[0]!.messages.filter((m) =>
m.message.includes("Inline <svg> icons are banned"),
);
expect(svgMessages).toHaveLength(0);
});
test("import-x/no-cycle rule is configured as error", async () => {
const eslint = new ESLint();
const config = await eslint.calculateConfigForFile("src/features/demo/a.ts");
// ESLint stores severity as a number: 0=off, 1=warn, 2=error
const rules = config.rules as Record<string, [number, ...unknown[]] | number | undefined>;
const cycleEntry = rules["import-x/no-cycle"];
expect(cycleEntry).toBeDefined();
const severity = Array.isArray(cycleEntry) ? cycleEntry[0] : cycleEntry;
expect(severity).toBe(2); // 2 = "error"
});
test("import-x/no-unresolved rule is present (off by design; TypeScript handles resolution)", async () => {
const eslint = new ESLint();
const config = await eslint.calculateConfigForFile("src/features/demo/a.ts");
const rules = config.rules as Record<string, [number, ...unknown[]] | number | undefined>;
const unresolvedEntry = rules["import-x/no-unresolved"];
// Rule must be explicitly configured (off is intentional per brief: TS catches these)
expect(unresolvedEntry).toBeDefined();
const severity = Array.isArray(unresolvedEntry) ? unresolvedEntry[0] : unresolvedEntry;
expect(typeof severity).toBe("number"); // 0=off, 1=warn, 2=error
});
});