Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-i18n-hardcoded-baseline.test.mjs
T

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");
});
});