178 lines
9.0 KiB
JavaScript
178 lines
9.0 KiB
JavaScript
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { failureCount, gates, renderReport, runI18nBrandInventory } from "./verify-i18n-hardcoded-baseline.mjs";
|
|
|
|
describe("i18n Hardcoded Strings baseline runner failure counting", () => {
|
|
it("counts TypeScript diagnostics from error TS markers", () => {
|
|
expect(failureCount("typecheck", "app/a.ts(1,1): error TS2322: nope\napp/b.ts(2,2): error TS7006: nope", "", 2)).toBe(2);
|
|
});
|
|
|
|
it("counts ESLint errors from the summary problem line", () => {
|
|
expect(failureCount("lint", "✖ 5 problems (3 errors, 2 warnings)", "", 1)).toBe(3);
|
|
});
|
|
|
|
it("counts Vitest failed tests from the i18n smoke summary", () => {
|
|
expect(failureCount("vitest-i18n-smoke", "Tests 4 failed | 8 passed", "", 1)).toBe(4);
|
|
});
|
|
|
|
it("counts CI baseline FAIL lines conservatively", () => {
|
|
expect(failureCount("ci-baseline", "FAIL package.json: missing script\nFAIL .eslintrc: missing rule", "", 1)).toBe(2);
|
|
});
|
|
|
|
it("counts hardcoded-string guard totals and falls back conservatively on non-empty failures", () => {
|
|
expect(failureCount("hardcoded-string-guard", "i18n Hardcoded Strings hardcoded-string guard\ntotal forbidden matches: 3", "", 1)).toBe(3);
|
|
expect(failureCount("hardcoded-string-guard", "", "unexpected scanner failure", 1)).toBe(1);
|
|
});
|
|
|
|
it("returns zero for clean exits and read-only inventory gates", () => {
|
|
expect(failureCount("typecheck", "", "", 0)).toBe(0);
|
|
expect(failureCount("i18n-brand-hardcoded-inventory", "ToolCTRL: 1", "", 1)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("i18n Hardcoded Strings baseline gate configuration", () => {
|
|
it("runs the hardcoded-string guard immediately before the read-only inventory", () => {
|
|
const guardIndex = gates.findIndex((gate) => gate.id === "hardcoded-string-guard");
|
|
|
|
expect(guardIndex).toBeGreaterThan(-1);
|
|
expect(gates[guardIndex]).toMatchObject({ command: "node", args: ["scripts/verify-hardcoded-strings.mjs"] });
|
|
expect(gates[guardIndex + 1]?.id).toBe("i18n-brand-hardcoded-inventory");
|
|
});
|
|
});
|
|
|
|
describe("i18n Hardcoded Strings i18n/brand/hardcoded inventory", () => {
|
|
it("scans existing roots, records missing roots, and ignores generated docs dependencies and TC paths", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "i18nHardcodedStrings-baseline-inventory-"));
|
|
await mkdir(join(root, "apps", "web", "app", "routes"), { recursive: true });
|
|
await mkdir(join(root, "packages", "i18n", "src"), { recursive: true });
|
|
await mkdir(join(root, "apps", "web", "public", "locales", "de"), { recursive: true });
|
|
await mkdir(join(root, "docs"), { recursive: true });
|
|
await mkdir(join(root, ".tc"), { recursive: true });
|
|
await mkdir(join(root, "apps", "web", "app", "generated"), { recursive: true });
|
|
await mkdir(join(root, "node_modules", "ignored"), { recursive: true });
|
|
|
|
await writeFile(
|
|
join(root, "apps", "web", "app", "routes", "sample.tsx"),
|
|
[
|
|
"const { t } = useTranslation();",
|
|
"const title = t('dashboard.title');",
|
|
"const label = 'Welcome aboard';",
|
|
"export function Component() { return <h1>Welcome aboard</h1>; }",
|
|
"const brand = 'ToolCTRL';",
|
|
"const link = 'https://tool-ctrl.de/datenschutz';",
|
|
"const subject = 'Welcome email subject';",
|
|
"const logoAlt = 'Company logo';",
|
|
"const legal = 'Terms and privacy policy';",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
await writeFile(join(root, "packages", "i18n", "src", "index.ts"), "export const defaultLocale = 'de'; export const supportedLocales = ['de', 'en'];\nimport i18next from 'i18next';\n", "utf8");
|
|
await mkdir(join(root, "packages", "brand", "src"), { recursive: true });
|
|
await writeFile(
|
|
join(root, "packages", "brand", "src", "index.ts"),
|
|
[
|
|
"export const defaultBrandTokens = { productName: 'ToolCTRL' };",
|
|
"export function createBrandTokens() { return defaultBrandTokens; }",
|
|
"export function resolveTransactionalBrandTokens() { return createBrandTokens(); }",
|
|
"export function brandCssVariables() { return {}; }",
|
|
"export type BrandTokens = typeof defaultBrandTokens;",
|
|
"export type BrandTokenOverrides = Partial<BrandTokens>;",
|
|
"export type ToolCTRLBrandTokens = BrandTokens;",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
await writeFile(
|
|
join(root, "apps", "web", "app", "routes", "transactional.ts"),
|
|
[
|
|
"import { defaultBrandTokens } from '@toolctrl/brand';",
|
|
"import { renderTransactionalMailTemplate, MailTemplateError } from '../../packages/mail/src/templates/transactional';",
|
|
"export type TransactionalTemplateName = 'welcome';",
|
|
"export type TransactionalTemplateData = { action_url: string };",
|
|
"export type RenderedTransactionalMail = { html: string };",
|
|
"const html = renderTransactionalMailTemplate('welcome', { action_url: 'https://example.test' }, { brand: defaultBrandTokens });",
|
|
"console.log(MailTemplateError, html);",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
await writeFile(join(root, "apps", "web", "public", "locales", "de", "translations.json"), "{ \"hello\": \"Hallo\" }\n", "utf8");
|
|
const ignoredSignal = "ToolCTRL @toolctrl/brand packages/brand defaultBrandTokens renderTransactionalMailTemplate MailTemplateError useTranslation title = 'Ignored Signal'\n";
|
|
await writeFile(join(root, "docs", "ignored.tsx"), ignoredSignal, "utf8");
|
|
await writeFile(join(root, ".tc", "ignored.tsx"), ignoredSignal, "utf8");
|
|
await writeFile(join(root, "apps", "web", "app", "generated", "ignored.tsx"), ignoredSignal, "utf8");
|
|
await writeFile(join(root, "node_modules", "ignored", "ignored.tsx"), ignoredSignal, "utf8");
|
|
|
|
const inventory = await runI18nBrandInventory(root);
|
|
const allMatches = inventory.matches.join("\n");
|
|
|
|
expect(inventory.roots).toEqual(["apps/web/app", "apps/web/public", "packages"]);
|
|
expect(inventory.missingRoots).toEqual(["apps/api", "apps/worker", "emails", "prisma", "server"]);
|
|
expect(inventory.filesScanned).toBe(5);
|
|
expect(inventory.counts).toMatchObject({
|
|
"locale-file-reference": 2,
|
|
"translation-hook-or-call": 2,
|
|
"i18next-reference": 2,
|
|
"brand-package-reference": 1,
|
|
"brand-token-contract": 14,
|
|
"transactional-mail-renderer": 7,
|
|
"brand-literal": 3,
|
|
"email-template-reference": 0,
|
|
"logo-domain-legal-link": 4,
|
|
"hardcoded-jsx-text": 1,
|
|
"hardcoded-user-facing-string": 2,
|
|
});
|
|
expect(allMatches).toContain("apps/web/app/routes/sample.tsx");
|
|
expect(allMatches).toContain("apps/web/app/routes/transactional.ts");
|
|
expect(allMatches).toContain("packages/i18n/src/index.ts");
|
|
expect(allMatches).toContain("packages/brand/src/index.ts");
|
|
expect(inventory.roots).toContain("apps/web/public");
|
|
expect(allMatches).not.toContain("docs/ignored.tsx");
|
|
expect(allMatches).not.toContain(".tc/ignored.tsx");
|
|
expect(allMatches).not.toContain("apps/web/app/generated/ignored.tsx");
|
|
expect(allMatches).not.toContain("node_modules/ignored/ignored.tsx");
|
|
});
|
|
});
|
|
|
|
describe("i18n Hardcoded Strings baseline report rendering", () => {
|
|
it("renders gate identifiers, exit codes, failure counts, and fingerprints", () => {
|
|
const report = renderReport([
|
|
{ id: "typecheck", command: "pnpm run typecheck", exitCode: 2, durationMs: 1500, failureCount: 7, fingerprint: { hash: "abc123def456", excerpt: "error TS2322" } },
|
|
{
|
|
id: "hardcoded-string-guard",
|
|
command: "node scripts/verify-hardcoded-strings.mjs",
|
|
exitCode: 1,
|
|
durationMs: 23,
|
|
failureCount: 2,
|
|
fingerprint: { hash: "987654fedcba", excerpt: "total forbidden matches: 2" },
|
|
},
|
|
{
|
|
id: "i18n-brand-hardcoded-inventory",
|
|
command: "read-only i18n/brand/hardcoded-string inventory",
|
|
exitCode: 0,
|
|
durationMs: 42,
|
|
failureCount: 0,
|
|
fingerprint: { hash: "fed456abc123", excerpt: "brand-token-contract: 1\ntransactional-mail-renderer: 1" },
|
|
},
|
|
]);
|
|
|
|
expect(report).toContain("@toolctrl/brand package references");
|
|
expect(report).toContain("transactional mail renderer contracts");
|
|
expect(report).toContain("forbidden user-facing/brand literal drift");
|
|
expect(report).toContain("# i18n Hardcoded Strings Baseline Health");
|
|
expect(report).toContain("## typecheck");
|
|
expect(report).toContain("## hardcoded-string-guard");
|
|
expect(report).toContain("## i18n-brand-hardcoded-inventory");
|
|
expect(report).toContain("- Exitcode: 2");
|
|
expect(report).toContain("- Exitcode: 1");
|
|
expect(report).toContain("- Exitcode: 0");
|
|
expect(report).toContain("- Failure Count: 7");
|
|
expect(report).toContain("- Failure Count: 2");
|
|
expect(report).toContain("- Failure Count: 0");
|
|
expect(report).toContain("- Output Fingerprint: abc123def456");
|
|
expect(report).toContain("- Output Fingerprint: 987654fedcba");
|
|
expect(report).toContain("- Output Fingerprint: fed456abc123");
|
|
});
|
|
});
|