fix: reject empty coverage counters
This commit is contained in:
@@ -48,11 +48,16 @@ const fullMetrics = metrics();
|
||||
function inventory(
|
||||
files: readonly string[],
|
||||
generatedExclusions: readonly string[] = [],
|
||||
nonExecutableModules: readonly string[] = [],
|
||||
): ProductionModuleInventory {
|
||||
return {
|
||||
files,
|
||||
preExclusionTotal: files.length + generatedExclusions.length,
|
||||
generatedExclusions,
|
||||
executableModules: files.filter(
|
||||
(file) => !nonExecutableModules.includes(file),
|
||||
),
|
||||
nonExecutableModules,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,6 +160,8 @@ describe("repository-aware risk coverage", () => {
|
||||
files: ["src/a.ts", "src/nested/b.tsx"],
|
||||
preExclusionTotal: 3,
|
||||
generatedExclusions: ["src/generated.ts"],
|
||||
executableModules: ["src/a.ts", "src/nested/b.tsx"],
|
||||
nonExecutableModules: [],
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
status: "FAIL",
|
||||
@@ -318,6 +325,170 @@ describe("repository-aware risk coverage", () => {
|
||||
).toThrow(/coverage total\.lines does not match recomputed inventory total/u);
|
||||
});
|
||||
|
||||
it("fails an exact-consistent all-zero coverage universe", () => {
|
||||
const parsedPolicy = parseRiskCoveragePolicy(
|
||||
policy({ repositoryBaseline: 1, generatedPaths: [] }),
|
||||
{ now },
|
||||
);
|
||||
const result = evaluateRiskCoverage({
|
||||
repositoryRoot: "/repository",
|
||||
inventory: inventory(["src/a.ts"]),
|
||||
policy: parsedPolicy,
|
||||
summary: {
|
||||
total: metrics(0),
|
||||
"src/a.ts": metrics(0),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "FAIL",
|
||||
selectedTotal: 0,
|
||||
repositoryTotal: 1,
|
||||
uncoveredModules: ["src/a.ts"],
|
||||
});
|
||||
expect(result.failures).toContain(
|
||||
"production module has zero coverage totals: src/a.ts",
|
||||
);
|
||||
expect(result.failures).toEqual(
|
||||
expect.arrayContaining([
|
||||
"total.lines coverage total must be greater than 0",
|
||||
"total.statements coverage total must be greater than 0",
|
||||
"total.functions coverage total must be greater than 0",
|
||||
"total.branches coverage total must be greater than 0",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts exact all-zero rows only for statically non-executable modules", () => {
|
||||
const parsedPolicy = parseRiskCoveragePolicy(
|
||||
policy({ repositoryBaseline: 2, generatedPaths: [] }),
|
||||
{ now },
|
||||
);
|
||||
const productionInventory = inventory(
|
||||
["src/a.ts", "src/type-only.ts"],
|
||||
[],
|
||||
["src/type-only.ts"],
|
||||
);
|
||||
const result = evaluateRiskCoverage({
|
||||
repositoryRoot: "/repository",
|
||||
inventory: productionInventory,
|
||||
policy: parsedPolicy,
|
||||
summary: {
|
||||
total: fullMetrics,
|
||||
"src/a.ts": fullMetrics,
|
||||
"src/type-only.ts": metrics(0),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "PASS",
|
||||
selectedTotal: 2,
|
||||
repositoryTotal: 2,
|
||||
executableTotal: 1,
|
||||
instrumentedExecutableTotal: 1,
|
||||
nonExecutableTotal: 1,
|
||||
nonExecutableModules: ["src/type-only.ts"],
|
||||
uncoveredModules: [],
|
||||
});
|
||||
|
||||
const mismatch = evaluateRiskCoverage({
|
||||
repositoryRoot: "/repository",
|
||||
inventory: productionInventory,
|
||||
policy: parsedPolicy,
|
||||
summary: {
|
||||
total: metrics(2),
|
||||
"src/a.ts": fullMetrics,
|
||||
"src/type-only.ts": fullMetrics,
|
||||
},
|
||||
});
|
||||
expect(mismatch.failures).toContain(
|
||||
"non-executable module has coverage counters: src/type-only.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("forbids critical and high-risk modules from being non-executable", () => {
|
||||
const parsedPolicy = parseRiskCoveragePolicy(
|
||||
policy({ repositoryBaseline: 1, generatedPaths: [] }),
|
||||
{ now },
|
||||
);
|
||||
const result = evaluateRiskCoverage({
|
||||
repositoryRoot: "/repository",
|
||||
inventory: inventory(["src/a.ts"], [], ["src/a.ts"]),
|
||||
policy: parsedPolicy,
|
||||
summary: { total: metrics(0), "src/a.ts": metrics(0) },
|
||||
});
|
||||
|
||||
expect(result.failures).toEqual(
|
||||
expect.arrayContaining([
|
||||
"critical module cannot be non-executable: src/a.ts",
|
||||
"high-risk module cannot be non-executable: src/a.ts",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails a critical threshold metric with a zero total even when pct is 100", () => {
|
||||
const parsedPolicy = parseRiskCoveragePolicy(
|
||||
policy({ repositoryBaseline: 2, generatedPaths: [] }),
|
||||
{ now },
|
||||
);
|
||||
const criticalMetrics = { ...fullMetrics, lines: counter(0) };
|
||||
const result = evaluateRiskCoverage({
|
||||
repositoryRoot: "/repository",
|
||||
inventory: inventory(["src/a.ts", "src/nested/b.tsx"]),
|
||||
policy: parsedPolicy,
|
||||
summary: {
|
||||
total: {
|
||||
lines: counter(1),
|
||||
statements: counter(2),
|
||||
functions: counter(2),
|
||||
branches: counter(2),
|
||||
},
|
||||
"src/a.ts": criticalMetrics,
|
||||
"src/nested/b.tsx": fullMetrics,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.status).toBe("FAIL");
|
||||
expect(result.selectedTotal).toBe(2);
|
||||
expect(result.failures).toContain(
|
||||
"src/a.ts.lines coverage total must be greater than 0",
|
||||
);
|
||||
expect(
|
||||
result.results.find(
|
||||
({ scope, metric }) => scope === "src/a.ts" && metric === "lines",
|
||||
),
|
||||
).toMatchObject({ received: 100, passed: false });
|
||||
});
|
||||
|
||||
it("keeps a noncritical zero-function row when other metrics and repository functions exist", () => {
|
||||
const parsedPolicy = parseRiskCoveragePolicy(
|
||||
policy({ repositoryBaseline: 2, generatedPaths: [] }),
|
||||
{ now },
|
||||
);
|
||||
const noncriticalMetrics = { ...fullMetrics, functions: counter(0) };
|
||||
const result = evaluateRiskCoverage({
|
||||
repositoryRoot: "/repository",
|
||||
inventory: inventory(["src/a.ts", "src/nested/b.tsx"]),
|
||||
policy: parsedPolicy,
|
||||
summary: {
|
||||
total: {
|
||||
lines: counter(2),
|
||||
statements: counter(2),
|
||||
functions: counter(1),
|
||||
branches: counter(2),
|
||||
},
|
||||
"src/a.ts": fullMetrics,
|
||||
"src/nested/b.tsx": noncriticalMetrics,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "PASS",
|
||||
selectedTotal: 2,
|
||||
uncoveredModules: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts and validates Vitest branchesTrue without evaluating it", () => {
|
||||
const parsedPolicy = parseRiskCoveragePolicy(
|
||||
policy({ repositoryBaseline: 1, generatedPaths: [] }),
|
||||
@@ -369,6 +540,75 @@ describe("repository-aware risk coverage", () => {
|
||||
expect(observedFlags & constants.O_NOFOLLOW).toBe(constants.O_NOFOLLOW);
|
||||
});
|
||||
|
||||
it("fails closed when an opened inventory file has no stable identity", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildProductionModuleInventory({
|
||||
repositoryRoot,
|
||||
openFile: async (target, flags) => {
|
||||
const handle = await open(target, flags);
|
||||
return {
|
||||
stat: async () => {
|
||||
const metadata = await handle.stat();
|
||||
Object.defineProperties(metadata, {
|
||||
dev: { value: 0 },
|
||||
ino: { value: 0 },
|
||||
});
|
||||
return metadata;
|
||||
},
|
||||
readFile: async (encoding) => handle.readFile(encoding),
|
||||
close: async () => handle.close(),
|
||||
};
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/stable file identity unavailable/u);
|
||||
});
|
||||
|
||||
it("classifies type-only and barrel modules separately from runtime statements", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
path.join(repositoryRoot, "src/type-only.ts"),
|
||||
"export interface Shape { readonly id: string }\nexport type ShapeId = Shape['id'];\n",
|
||||
),
|
||||
writeFile(
|
||||
path.join(repositoryRoot, "src/barrel.ts"),
|
||||
"export type { Shape, ShapeId } from './type-only.ts';\nexport { type Shape as PublicShape } from './type-only.ts';\n",
|
||||
),
|
||||
writeFile(
|
||||
path.join(repositoryRoot, "src/runtime-export.ts"),
|
||||
"export const runtimeValue = 1;\n",
|
||||
),
|
||||
writeFile(
|
||||
path.join(repositoryRoot, "src/side-effect.ts"),
|
||||
"void globalThis;\n",
|
||||
),
|
||||
writeFile(
|
||||
path.join(repositoryRoot, "src/side-effect-import.ts"),
|
||||
"import './a.ts';\n",
|
||||
),
|
||||
]);
|
||||
|
||||
const productionInventory = await buildProductionModuleInventory({
|
||||
repositoryRoot,
|
||||
generatedPaths: ["src/generated.ts"],
|
||||
});
|
||||
|
||||
expect(productionInventory.nonExecutableModules).toEqual([
|
||||
"src/barrel.ts",
|
||||
"src/type-only.ts",
|
||||
]);
|
||||
expect(productionInventory.executableModules).toEqual(
|
||||
expect.arrayContaining([
|
||||
"src/a.ts",
|
||||
"src/nested/b.tsx",
|
||||
"src/runtime-export.ts",
|
||||
"src/side-effect-import.ts",
|
||||
"src/side-effect.ts",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a post-lstat file identity swap even without relying on O_NOFOLLOW", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-race-"));
|
||||
|
||||
Reference in New Issue
Block a user