fix: reject empty coverage counters

This commit is contained in:
DongHyeonka
2026-08-02 09:10:35 +09:00
parent 8d6fbb97e9
commit 6e05a35790
5 changed files with 505 additions and 39 deletions
+18 -18
View File
@@ -1,50 +1,50 @@
{
"total": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 },
"statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 },
"functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 },
"branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
"statements": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
"functions": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
"branches": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 }
},
"src/adapters/http/bounded-body-reader.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/http/bounded-json.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/http/http-execution-v3.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/http/request-builder.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/http/retry-policy.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/query-cache/server-state-scope-runtime.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/service-worker/service-worker-lifecycle.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/storage/browser-storage-adapter.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/telemetry/best-effort-telemetry.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/create-application.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/policies/compatibility.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/policies/performance-budgets.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/policies/promotion-readiness.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/use-cases/decide-chunk-recovery.ts": {
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }, "branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
}
}
+30
View File
@@ -131,6 +131,36 @@ describe("risk coverage CLI files", () => {
).rejects.toThrow(/changed during validation/u);
});
it("fails closed when an opened input has no stable file identity", async () => {
const repositoryRoot = await fixture();
await expect(
readRiskCoverageInput(
{
repositoryRoot,
relativePath: "config/testing/policy.json",
label: "policy",
},
{
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("confines artifact output and rejects input overwrite or symlink ancestors", async () => {
const repositoryRoot = await fixture();
await expect(
+240
View File
@@ -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-"));