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

326 lines
16 KiB
JavaScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { afterEach, describe, it } from "vitest";
import { formatCiBaselineResult, verifyCiBaseline } from "./verify-ci-baseline.mjs";
const VALID_PACKAGE = {
private: true,
packageManager: "pnpm@10.33.0",
scripts: {
build: "pnpm --filter @toolctrl/db prisma:generate && pnpm --filter @toolctrl/db build && pnpm --filter @toolctrl/web build",
"build:ci": "pnpm run build",
lint: "eslint --max-warnings=0 --ext .tsx,.ts .",
test: "pnpm --filter @toolctrl/web test",
"test:packages": "node scripts/verify-package-test-scripts.mjs && pnpm --filter './packages/*' --if-present test",
"test:mobile": "pnpm --filter @toolctrl/mobile test",
typecheck: "tsc -b",
"verify:i18n": "tsx scripts/i18n-check.ts && node scripts/verify-hardcoded-strings.mjs && eslint --max-warnings=0 --ext .tsx,.ts packages/ui-app/src packages/ui-subframe/src packages/ui-mock/src",
"verify:ui-hot-swap": "tsx scripts/verify-ui-hot-swap.ts",
"test:hot-swap": "pnpm exec vitest run scripts/verify-ui-hot-swap.test.ts && pnpm run verify:ui-hot-swap",
"verify:test-coverage": "node scripts/verify-test-coverage.mjs",
"verify:sidebar-routes": "pnpm exec vitest run apps/web/app/application/sidebar/AdminSidebar.test.tsx apps/web/app/application/sidebar/AppSidebar.test.tsx apps/web/app/components/layouts/sidebars/shadcn/app-sidebar.test.tsx apps/web/app/routes/admin/admin-route-targets.test.ts apps/web/app/routes.test.ts",
"verify:a11y-coverage": "node scripts/verify-a11y-coverage.mjs",
"test:a11y": "pnpm exec vitest run apps/web/app/components/ui/button.a11y.test.tsx apps/web/app/components/ui/errors/ErrorPage.a11y.test.tsx apps/web/app/components/ui/*.axe.test.tsx apps/web/app/components/core/SkipToContent.test.tsx apps/web/app/components/aceternity/HoverEffect.a11y.test.tsx apps/web/app/components/ui/lists/ViewToggle.a11y.test.tsx apps/web/app/components/ui/lists/ViewToggleWithUrl.a11y.test.tsx apps/web/app/components/ui/tabs/TabsWithIcons.a11y.test.tsx scripts/verify-color-contrast.test.mjs",
},
};
const PHASE_C_DESCRIPTOR_SELECTORS = [
"Property[key.name=/^(title|label|placeholder|ariaLabel|alt|description|message|heading|subject)$/] > Literal[value=/[A-Za-zÄÖÜäöü]/]",
"Property[key.value=/^(title|label|placeholder|aria-label|ariaLabel|alt|description|message|heading|subject)$/] > Literal[value=/[A-Za-zÄÖÜäöü]/]",
"Property[key.name=/^(title|label|placeholder|ariaLabel|alt|description|message|heading|subject)$/] > TemplateLiteral",
"Property[key.value=/^(title|label|placeholder|aria-label|ariaLabel|alt|description|message|heading|subject)$/] > TemplateLiteral",
];
const VALID_ESLINTRC = {
root: true,
rules: {
"no-console": "warn",
"@typescript-eslint/no-unused-vars": "warn",
},
overrides: [
{
files: ["packages/ui-app/src/**/*.{ts,tsx}", "packages/ui-subframe/src/**/*.{ts,tsx}", "packages/ui-mock/src/**/*.{ts,tsx}"],
rules: {
"no-restricted-syntax": [
"error",
...PHASE_C_DESCRIPTOR_SELECTORS.map((selector) => ({
selector,
message: "Use i18n key fields instead of hardcoded user-facing strings in Phase C UI package descriptors.",
})),
],
},
},
{
files: ["packages/ui-app/src/**/*.{tsx}", "packages/ui-subframe/src/**/*.{tsx}", "packages/ui-mock/src/**/*.{tsx}"],
rules: {
"no-restricted-syntax": [
"error",
...PHASE_C_DESCRIPTOR_SELECTORS.map((selector) => ({
selector,
message: "Use i18n key fields instead of hardcoded user-facing strings in Phase C UI package descriptors.",
})),
{
selector: "JSXText[value=/\\S/]",
message: "Use i18n label keys or translated values instead of hardcoded JSX text in Phase C UI packages.",
},
{
selector: "JSXAttribute[name.name=/^(aria-label|title|placeholder)$/] > Literal[value=/[A-Za-zÄÖÜäöü]/]",
message: "Use i18n label keys or translated values instead of hardcoded JSX accessibility/title text in Phase C UI packages.",
},
],
},
},
],
};
const fixtures = [];
function writeFile(rootDir, relativePath, content) {
const absolutePath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
fs.writeFileSync(absolutePath, content);
}
function writeJson(rootDir, relativePath, value) {
writeFile(rootDir, relativePath, `${JSON.stringify(value, null, 2)}\n`);
}
function createFixture(overrides = {}) {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-baseline-"));
fixtures.push(rootDir);
writeJson(rootDir, "package.json", overrides.rootPackage ?? VALID_PACKAGE);
writeFile(rootDir, "pnpm-lock.yaml", overrides.lockfile ?? "lockfileVersion: '9.0'\n");
writeJson(rootDir, ".eslintrc", overrides.eslintrc ?? VALID_ESLINTRC);
writeFile(rootDir, "packages/ui-app/src/index.ts", "export const uiApp = true;\n");
writeFile(rootDir, "packages/ui-subframe/src/index.ts", "export const uiSubframe = true;\n");
writeFile(rootDir, "packages/ui-mock/src/index.ts", "export const uiMock = true;\n");
if (overrides.remove) {
for (const relativePath of overrides.remove) {
fs.rmSync(path.join(rootDir, relativePath), { recursive: true, force: true });
}
}
return rootDir;
}
function mutatePackage(mutator) {
const rootPackage = structuredClone(VALID_PACKAGE);
mutator(rootPackage);
return rootPackage;
}
afterEach(() => {
while (fixtures.length > 0) {
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
}
});
describe("verify-ci-baseline", () => {
it("passes for a repository with required CI prerequisites", () => {
const rootDir = createFixture();
const result = verifyCiBaseline(rootDir);
const output = formatCiBaselineResult(result);
assert.deepEqual(result.failures, []);
assert.ok(result.passes.some((pass) => pass === "package.json: packageManager is pnpm@10.33.0"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"typecheck\" is defined"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"test:mobile\" is defined"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"build:ci\" is defined"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"build:ci\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"lint\" invokes ESLint"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"lint\" fails on ESLint warnings"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"verify:i18n\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"verify:ui-hot-swap\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"test:hot-swap\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"verify:test-coverage\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"verify:sidebar-routes\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"verify:a11y-coverage\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"test:a11y\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"test:mobile\" matches the required CI command"));
assert.ok(result.passes.some((pass) => pass === "package.json: script \"verify:i18n\" runs ESLint over Phase C UI packages"));
assert.ok(result.passes.some((pass) => pass === "pnpm-lock.yaml: exists"));
assert.ok(result.passes.some((pass) => pass === ".eslintrc: rule \"no-console\" is warn"));
assert.ok(result.passes.some((pass) => pass === ".eslintrc: rule \"@typescript-eslint/no-unused-vars\" is warn"));
assert.ok(result.passes.some((pass) => pass.includes(".eslintrc: Phase C i18n ESLint selector \"JSXText")));
assert.match(output, /^PASS CI baseline verification/);
assert.match(output, /Summary: \d+ passed, 0 observed, 0 failed/);
});
it("fails when the lockfile is missing", () => {
const rootDir = createFixture({ remove: ["pnpm-lock.yaml"] });
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /pnpm-lock\.yaml: missing required CI baseline file/);
});
it("fails when the lint script is missing", () => {
const rootDir = createFixture({
rootPackage: mutatePackage((rootPackage) => {
delete rootPackage.scripts.lint;
}),
});
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /package\.json: missing required script "lint"/);
});
it("fails when the lint script does not invoke ESLint", () => {
const rootDir = createFixture({
rootPackage: mutatePackage((rootPackage) => {
rootPackage.scripts.lint = "prettier --check .";
}),
});
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /package\.json: script "lint" must invoke ESLint/);
});
it("fails when the lint script allows ESLint warnings", () => {
const rootDir = createFixture({
rootPackage: mutatePackage((rootPackage) => {
rootPackage.scripts.lint = "eslint --ext .tsx,.ts .";
}),
});
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /package\.json: script "lint" must include --max-warnings=0/);
});
it("fails when ESLint config is missing", () => {
const rootDir = createFixture({ remove: [".eslintrc"] });
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /\.eslintrc: missing required CI baseline file/);
});
it("fails when required ESLint warn baselines are not explicit", () => {
const rootDir = createFixture({
eslintrc: {
root: true,
rules: {
"no-console": "error",
"@typescript-eslint/no-unused-vars": "off",
},
},
});
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /\.eslintrc: rule "no-console" must be warn for the current CI baseline/);
assert.match(result.failures.join("\n"), /\.eslintrc: rule "@typescript-eslint\/no-unused-vars" must be warn for the current CI baseline/);
});
it("fails when verify:i18n does not run ESLint over Phase C UI packages", () => {
const rootDir = createFixture({
rootPackage: mutatePackage((rootPackage) => {
rootPackage.scripts["verify:i18n"] = "tsx scripts/i18n-check.ts";
}),
});
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /package\.json: script "verify:i18n" must run ESLint over Phase C UI packages/);
});
it("fails when critical verification script bodies are softened", () => {
const rootDir = createFixture({
rootPackage: mutatePackage((rootPackage) => {
rootPackage.scripts["verify:i18n"] = "eslint --ext .tsx,.ts packages/ui-app/src packages/ui-subframe/src packages/ui-mock/src";
rootPackage.scripts["verify:ui-hot-swap"] = "echo skipped";
rootPackage.scripts["test:hot-swap"] = "pnpm run verify:ui-hot-swap || true";
rootPackage.scripts["verify:test-coverage"] = "echo skipped";
rootPackage.scripts["verify:sidebar-routes"] = "echo skipped";
rootPackage.scripts["verify:a11y-coverage"] = "echo skipped";
rootPackage.scripts["test:a11y"] = "echo skipped";
}),
});
const result = verifyCiBaseline(rootDir);
const failures = result.failures.join("\n");
assert.match(failures, /package\.json: script "verify:i18n" must be exactly "tsx scripts\/i18n-check\.ts && node scripts\/verify-hardcoded-strings\.mjs && eslint --max-warnings=0 --ext \.tsx,\.ts packages\/ui-app\/src packages\/ui-subframe\/src packages\/ui-mock\/src"/);
assert.match(failures, /package\.json: script "verify:ui-hot-swap" must be exactly "tsx scripts\/verify-ui-hot-swap\.ts"/);
assert.match(failures, /package\.json: script "test:hot-swap" must be exactly "pnpm exec vitest run scripts\/verify-ui-hot-swap\.test\.ts && pnpm run verify:ui-hot-swap"/);
assert.match(failures, /package\.json: script "verify:test-coverage" must be exactly "node scripts\/verify-test-coverage\.mjs"/);
assert.match(failures, /package\.json: script "verify:sidebar-routes" must be exactly "pnpm exec vitest run apps\/web\/app\/application\/sidebar\/AdminSidebar\.test\.tsx apps\/web\/app\/application\/sidebar\/AppSidebar\.test\.tsx apps\/web\/app\/components\/layouts\/sidebars\/shadcn\/app-sidebar\.test\.tsx apps\/web\/app\/routes\/admin\/admin-route-targets\.test\.ts apps\/web\/app\/routes\.test\.ts"/);
assert.match(failures, /package\.json: script "verify:a11y-coverage" must be exactly "node scripts\/verify-a11y-coverage\.mjs"/);
assert.match(failures, /package\.json: script "test:a11y" must be exactly "pnpm exec vitest run apps\/web\/app\/components\/ui\/button\.a11y\.test\.tsx apps\/web\/app\/components\/ui\/errors\/ErrorPage\.a11y\.test\.tsx apps\/web\/app\/components\/ui\/\*\.axe\.test\.tsx apps\/web\/app\/components\/core\/SkipToContent\.test\.tsx apps\/web\/app\/components\/aceternity\/HoverEffect\.a11y\.test\.tsx apps\/web\/app\/components\/ui\/lists\/ViewToggle\.a11y\.test\.tsx apps\/web\/app\/components\/ui\/lists\/ViewToggleWithUrl\.a11y\.test\.tsx apps\/web\/app\/components\/ui\/tabs\/TabsWithIcons\.a11y\.test\.tsx scripts\/verify-color-contrast\.test\.mjs"/);
});
it("fails when build omits Prisma generation", () => {
const rootDir = createFixture({
rootPackage: mutatePackage((rootPackage) => {
rootPackage.scripts.build = "pnpm --filter @toolctrl/db build && pnpm --filter @toolctrl/web build";
}),
});
const result = verifyCiBaseline(rootDir);
assert.match(
result.failures.join("\n"),
/package\.json: script "build" must be exactly "pnpm --filter @toolctrl\/db prisma:generate && pnpm --filter @toolctrl\/db build && pnpm --filter @toolctrl\/web build"/,
);
});
it("fails when verify:i18n suppresses unmatched lint roots", () => {
const rootDir = createFixture({
rootPackage: mutatePackage((rootPackage) => {
rootPackage.scripts["verify:i18n"] = "eslint --no-error-on-unmatched-pattern --ext .tsx,.ts packages/ui-app/src packages/ui-subframe/src packages/ui-mock/src";
}),
});
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /package\.json: script "verify:i18n" must not include --no-error-on-unmatched-pattern/);
});
it("fails when a Phase C UI lint root is missing", () => {
const rootDir = createFixture({ remove: ["packages/ui-mock/src"] });
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /packages\/ui-mock\/src: missing Phase C UI lint root/);
});
it("fails when Phase C i18n JSX ESLint selectors are missing", () => {
const rootDir = createFixture({
eslintrc: {
...VALID_ESLINTRC,
overrides: [VALID_ESLINTRC.overrides[0]],
},
});
const result = verifyCiBaseline(rootDir);
assert.match(result.failures.join("\n"), /\.eslintrc: missing Phase C i18n ESLint selector "JSXText/);
assert.match(result.failures.join("\n"), /\.eslintrc: missing Phase C i18n ESLint selector "JSXAttribute/);
});
it("CLI exits non-zero and prints path-specific output for a missing prerequisite", () => {
const rootDir = createFixture({ remove: ["pnpm-lock.yaml"] });
const scriptPath = path.resolve("scripts/verify-ci-baseline.mjs");
assert.throws(
() => execFileSync(process.execPath, [scriptPath], { cwd: rootDir, encoding: "utf8", stdio: "pipe" }),
(error) => {
assert.equal(error.status, 1);
assert.match(error.stderr, /^FAIL CI baseline verification/);
assert.match(error.stderr, /FAIL pnpm-lock\.yaml: missing required CI baseline file/);
return true;
},
);
});
});