// @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"], }); }); });