216 lines
7.6 KiB
JavaScript
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"],
|
|
});
|
|
});
|
|
});
|