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

216 lines
7.6 KiB
JavaScript

// @vitest-environment node
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { resolveSummaryPath } from "./coverage-summary.mjs";
import { buildVitestCoverageArgs, buildVitestCoverageEnv, evaluateTargets, isIgnorableCoverageTempFailure } from "./verify-test-coverage.mjs";
const repoRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..");
function coveredFile(relativePath, statements = { covered: 8, total: 10 }, branches = { covered: 4, total: 5 }) {
return {
[path.join(repoRoot, relativePath)]: {
statements,
branches,
},
};
}
describe("verify-test-coverage", () => {
it("keeps coverage-only artifact excludes out of coverage collection", async () => {
const config = await import("../vitest.config.ts?coverage-excludes=1");
expect(config.default.test?.coverage?.exclude).toEqual(
expect.arrayContaining([
"**/dist/**",
"**/build/**",
"**/.react-router/**",
"**/storybook-static/**",
"**/coverage/**",
"**/coverage-gate/**",
"**/generated/**",
"**/*.generated.*",
])
);
});
it("preserves full dev coverage reporters and the legacy coverage directory by default", async () => {
const previous = process.env.TOOLCTRL_COVERAGE_GATE;
delete process.env.TOOLCTRL_COVERAGE_GATE;
try {
const config = await import("../vitest.config.ts?coverage-default=1");
expect(config.default.test?.coverage?.reporter).toEqual(["text-summary", "json-summary", "html", "lcov"]);
expect(config.default.test?.coverage?.reportsDirectory).toBe("coverage");
} finally {
if (previous === undefined) {
delete process.env.TOOLCTRL_COVERAGE_GATE;
} else {
process.env.TOOLCTRL_COVERAGE_GATE = previous;
}
}
});
it("uses the lightweight coverage-gate reporters when the threshold gate env is set", async () => {
const previous = process.env.TOOLCTRL_COVERAGE_GATE;
process.env.TOOLCTRL_COVERAGE_GATE = "1";
try {
const gateConfig = await import("../vitest.config.ts?coverage-gate=1");
expect(gateConfig.default.test?.coverage?.reporter).toEqual(["text-summary", "json-summary"]);
expect(gateConfig.default.test?.coverage?.reportsDirectory).toBe("coverage-gate");
} finally {
if (previous === undefined) {
delete process.env.TOOLCTRL_COVERAGE_GATE;
} else {
process.env.TOOLCTRL_COVERAGE_GATE = previous;
}
}
});
it("runs the broad coverage gate on the threads pool to avoid fork coverage temp races", () => {
expect(buildVitestCoverageArgs(["packages/url/src"])).toEqual(["exec", "vitest", "run", "--coverage", "--pool=threads", "packages/url/src"]);
});
it("forces the lightweight coverage gate env for the spawned Vitest run", () => {
expect(buildVitestCoverageEnv({ CI: "1", TOOLCTRL_COVERAGE_GATE: "0" })).toEqual({ CI: "1", TOOLCTRL_COVERAGE_GATE: "1" });
});
it("ignores only coverage temp cleanup ENOENTs after tests passed and a summary exists", () => {
const output = [
" Test Files 396 passed | 3 skipped (399)",
" Tests 3734 passed | 18 skipped (3752)",
"Error: ENOENT: no such file or directory, lstat '/repo/coverage/.tmp'",
].join("\n");
expect(isIgnorableCoverageTempFailure({ status: 1, output, summaryExists: true })).toBe(true);
expect(isIgnorableCoverageTempFailure({ status: 1, output, summaryExists: false })).toBe(false);
expect(isIgnorableCoverageTempFailure({ status: 1, output: output.replace("396 passed", "1 failed | 395 passed"), summaryExists: true })).toBe(false);
expect(isIgnorableCoverageTempFailure({ status: 1, output: output.replace("coverage/.tmp", "coverage-summary.json"), summaryExists: true })).toBe(false);
});
it("also recognizes coverage-gate temp cleanup ENOENTs", () => {
const output = [
" Test Files 396 passed | 3 skipped (399)",
" Tests 3734 passed | 18 skipped (3752)",
"Error: ENOENT: no such file or directory, lstat '/repo/coverage-gate/.tmp'",
].join("\n");
expect(isIgnorableCoverageTempFailure({ status: 1, output, summaryExists: true })).toBe(true);
});
it("resolves coverage summary paths from env, coverage-gate, then legacy coverage", () => {
const root = path.join(repoRoot, "tmp-root");
const seen = new Set([path.join(root, "coverage-gate", "coverage-summary.json")]);
const fsApi = { existsSync: (candidate) => seen.has(candidate) };
expect(resolveSummaryPath({ root, env: {}, fsApi })).toBe(path.join(root, "coverage-gate", "coverage-summary.json"));
seen.clear();
seen.add(path.join(root, "coverage", "coverage-summary.json"));
expect(resolveSummaryPath({ root, env: {}, fsApi })).toBe(path.join(root, "coverage", "coverage-summary.json"));
expect(resolveSummaryPath({ root, env: { COVERAGE_SUMMARY_PATH: "custom/summary.json" }, fsApi })).toBe(path.join(root, "custom", "summary.json"));
});
it("fails a missing critical runtime target instead of counting it as skipped", () => {
const result = evaluateTargets(
[
{
name: "apps/api/src/auth (critical-path: Auth)",
pathPrefixes: ["apps/api/src/auth/"],
statements: 99,
branches: 92,
criticalPath: true,
},
],
{
total: {},
...coveredFile("apps/api/src/other/file.ts"),
}
);
expect(result).toEqual({
passes: [],
failures: ["FAIL apps/api/src/auth (critical-path: Auth) [CRITICAL] no covered files matched"],
});
});
it("does not allow critical runtime targets to skip via allowEmpty", () => {
const result = evaluateTargets(
[
{
name: "apps/web/app/routes/register (critical-path: Signup)",
pathPrefixes: ["apps/web/app/routes/register/"],
statements: 95,
branches: 90,
criticalPath: true,
allowEmpty: true,
},
],
{ total: {} }
);
expect(result).toEqual({
passes: [],
failures: ["FAIL apps/web/app/routes/register (critical-path: Signup) [CRITICAL] no covered files matched"],
});
});
it("allows explicitly empty targets to skip when no covered files match", () => {
const result = evaluateTargets(
[
{
name: "packages/generated-client",
pathPrefixes: ["packages/generated-client/src/"],
statements: 80,
branches: 70,
allowEmpty: true,
},
],
{ total: {} }
);
expect(result).toEqual({
passes: ["SKIP packages/generated-client no covered files matched"],
failures: [],
});
});
it("allows 0/0 type-only targets to skip when no covered files match", () => {
const result = evaluateTargets(
[
{
name: "packages/api-types (type-only — runtime coverage near zero by design)",
pathPrefixes: ["packages/api-types/src/"],
statements: 0,
branches: 0,
},
],
{ total: {} }
);
expect(result).toEqual({
passes: ["SKIP packages/api-types (type-only — runtime coverage near zero by design) no covered files matched"],
failures: [],
});
});
it("does not treat every 0/0 target as type-only", () => {
const result = evaluateTargets(
[
{
name: "packages/runtime-placeholder",
pathPrefixes: ["packages/runtime-placeholder/src/"],
statements: 0,
branches: 0,
},
],
{ total: {} }
);
expect(result).toEqual({
passes: [],
failures: ["FAIL packages/runtime-placeholder no covered files matched"],
});
});
});