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
+13 -9
View File
@@ -90,16 +90,16 @@ function hasStableIdentity(metadata: Stats): boolean {
return (
Number.isSafeInteger(metadata.dev) &&
Number.isSafeInteger(metadata.ino) &&
(metadata.dev !== 0 || metadata.ino !== 0)
metadata.dev > 0 &&
metadata.ino > 0
);
}
function sameFileIdentity(before: Stats, after: Stats): boolean {
return (
!hasStableIdentity(before) ||
!hasStableIdentity(after) ||
(before.dev === after.dev && before.ino === after.ino)
);
if (!hasStableIdentity(before) || !hasStableIdentity(after)) {
throw new TypeError("stable file identity unavailable");
}
return before.dev === after.dev && before.ino === after.ino;
}
async function rejectSymlinkAncestors(
@@ -251,16 +251,20 @@ export async function resolveRiskCoverageArtifactPath(input: Readonly<{
if (!hasErrorCode(error, "ENOENT")) throw error;
}
if (destinationMetadata) {
if (!hasStableIdentity(destinationMetadata)) {
throw new TypeError("artifact stable file identity unavailable");
}
const destinationRealpath = await realpath(absolutePath);
for (const inputPath of normalizedInputs) {
const inputAbsolutePath = path.resolve(repositoryRoot, inputPath);
const inputRealpath = await realpath(inputAbsolutePath);
const inputMetadata = await lstat(inputAbsolutePath);
if (!hasStableIdentity(inputMetadata)) {
throw new TypeError(`input stable file identity unavailable: ${inputPath}`);
}
if (
inputRealpath === destinationRealpath ||
(hasStableIdentity(inputMetadata) &&
hasStableIdentity(destinationMetadata) &&
inputMetadata.dev === destinationMetadata.dev &&
(inputMetadata.dev === destinationMetadata.dev &&
inputMetadata.ino === destinationMetadata.ino)
) {
throw new TypeError(`artifact path is the same file as an input: ${inputPath}`);
+204 -12
View File
@@ -4,9 +4,9 @@ import {
open,
readdir,
realpath,
type FileHandle,
} from "node:fs/promises";
import path from "node:path";
import babelParser from "@babel/eslint-parser";
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
@@ -63,12 +63,18 @@ export type ProductionModuleInventory = Readonly<{
files: readonly string[];
preExclusionTotal: number;
generatedExclusions: readonly string[];
executableModules: readonly string[];
nonExecutableModules: readonly string[];
}>;
export type RiskCoverageResult = Readonly<{
status: "PASS" | "FAIL";
selectedTotal: number;
repositoryTotal: number;
executableTotal: number;
instrumentedExecutableTotal: number;
nonExecutableTotal: number;
nonExecutableModules: readonly string[];
preExclusionTotal: number;
generatedExclusionCount: number;
generatedExclusions: readonly string[];
@@ -86,7 +92,11 @@ export type RiskCoverageResult = Readonly<{
failures: readonly string[];
}>;
type ReadableFileHandle = Pick<FileHandle, "close" | "stat">;
type ReadableFileHandle = Readonly<{
stat(): Promise<Stats>;
readFile(encoding: "utf8"): Promise<string>;
close(): Promise<unknown>;
}>;
type InventoryOptions = Readonly<{
repositoryRoot?: string;
generatedPaths?: readonly string[];
@@ -97,6 +107,7 @@ type InventoryOptions = Readonly<{
}>;
class FileIdentityChangedError extends Error {}
class StableFileIdentityUnavailableError extends Error {}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -333,16 +344,18 @@ function hasStableIdentity(metadata: Stats): boolean {
return (
Number.isSafeInteger(metadata.dev) &&
Number.isSafeInteger(metadata.ino) &&
(metadata.dev !== 0 || metadata.ino !== 0)
metadata.dev > 0 &&
metadata.ino > 0
);
}
function sameFileIdentity(before: Stats, after: Stats): boolean {
return (
!hasStableIdentity(before) ||
!hasStableIdentity(after) ||
(before.dev === after.dev && before.ino === after.ino)
);
if (!hasStableIdentity(before) || !hasStableIdentity(after)) {
throw new StableFileIdentityUnavailableError(
"stable file identity unavailable",
);
}
return before.dev === after.dev && before.ino === after.ino;
}
export function isProductionModulePath(relativePath: string): boolean {
@@ -353,6 +366,97 @@ export function isProductionModulePath(relativePath: string): boolean {
);
}
type TypeScriptAstNode = Readonly<{
type?: unknown;
body?: unknown;
declaration?: unknown;
specifiers?: unknown;
declare?: unknown;
const?: unknown;
importKind?: unknown;
}>;
function statementIsExecutable(value: unknown): boolean {
if (!isRecord(value) || typeof value.type !== "string") {
throw new TypeError("TypeScript parser returned an invalid statement");
}
const statement = value as TypeScriptAstNode;
if (
[
"EmptyStatement",
"TSDeclareFunction",
"TSInterfaceDeclaration",
"TSNamespaceExportDeclaration",
"TSTypeAliasDeclaration",
].includes(statement.type as string)
) {
return false;
}
if (statement.type === "ImportDeclaration") {
return Array.isArray(statement.specifiers) && statement.specifiers.length === 0;
}
if (
statement.type === "ExportAllDeclaration" ||
statement.type === "TSExportAssignment"
) {
return statement.type === "TSExportAssignment";
}
if (
statement.type === "ExportNamedDeclaration" ||
statement.type === "ExportDefaultDeclaration"
) {
return statement.declaration !== null && statement.declaration !== undefined
? statementIsExecutable(statement.declaration)
: false;
}
if (
statement.type === "FunctionDeclaration" ||
statement.type === "VariableDeclaration" ||
statement.type === "ClassDeclaration" ||
statement.type === "TSModuleDeclaration"
) {
return statement.declare !== true &&
(statement.type !== "FunctionDeclaration" || statement.body !== null);
}
if (statement.type === "TSEnumDeclaration") {
return statement.declare !== true && statement.const !== true;
}
if (statement.type === "TSImportEqualsDeclaration") {
return statement.importKind !== "type" && statement.declare !== true;
}
return true;
}
export function hasExecutableTypeScriptStatements(
source: string,
relativePath: string,
): boolean {
let parsed: unknown;
try {
parsed = babelParser.parse(source, {
sourceType: "module",
requireConfigFile: false,
filePath: relativePath,
babelOptions: {
parserOpts: {
plugins: [
"typescript",
...(relativePath.endsWith(".tsx") ? ["jsx"] : []),
],
},
},
});
} catch (error) {
throw new TypeError(`production module has invalid TypeScript syntax: ${relativePath}`, {
cause: error,
});
}
if (!isRecord(parsed) || !Array.isArray(parsed.body)) {
throw new TypeError("TypeScript parser returned an invalid program");
}
return parsed.body.some(statementIsExecutable);
}
export async function buildProductionModuleInventory(
options: InventoryOptions = {},
): Promise<ProductionModuleInventory> {
@@ -377,6 +481,8 @@ export async function buildProductionModuleInventory(
}
const allModules: string[] = [];
const executableModules: string[] = [];
const nonExecutableModules: string[] = [];
async function visit(relativeDirectory: string): Promise<void> {
const absoluteDirectory = path.join(repositoryRoot, relativeDirectory);
const entries = await readDirectory(absoluteDirectory);
@@ -403,6 +509,7 @@ export async function buildProductionModuleInventory(
throw new TypeError(`production inventory path is outside repository: ${relativeTarget}`);
}
let handle: ReadableFileHandle | undefined;
let source: string;
try {
handle = await openFile(
absoluteTarget,
@@ -415,7 +522,14 @@ export async function buildProductionModuleInventory(
if (!sameFileIdentity(metadata, openedMetadata)) {
throw new FileIdentityChangedError("opened file identity changed");
}
source = await handle.readFile("utf8");
} catch (error) {
if (error instanceof StableFileIdentityUnavailableError) {
throw new Error(
`production inventory stable file identity unavailable: ${relativeTarget}`,
{ cause: error },
);
}
if (error instanceof FileIdentityChangedError) {
throw new Error(
`production inventory file changed during validation: ${relativeTarget}`,
@@ -429,6 +543,11 @@ export async function buildProductionModuleInventory(
await handle?.close();
}
allModules.push(relativeTarget);
if (hasExecutableTypeScriptStatements(source, relativeTarget)) {
executableModules.push(relativeTarget);
} else {
nonExecutableModules.push(relativeTarget);
}
}
}
await visit("src");
@@ -446,6 +565,12 @@ export async function buildProductionModuleInventory(
files: Object.freeze(inventory),
preExclusionTotal: allModules.length,
generatedExclusions: Object.freeze([...generatedPaths].sort()),
executableModules: Object.freeze(
executableModules.filter((file) => !generated.has(file)).sort(),
),
nonExecutableModules: Object.freeze(
nonExecutableModules.filter((file) => !generated.has(file)).sort(),
),
});
}
@@ -593,6 +718,23 @@ export function evaluateRiskCoverage(input: Readonly<{
if (new Set(inventory).size !== inventory.length) {
throw new TypeError("production module inventory contains a duplicate path");
}
const executableModules = input.inventory.executableModules.map((file) =>
exactSourcePath(file, "executable inventory path"),
);
const nonExecutableModules = input.inventory.nonExecutableModules.map((file) =>
exactSourcePath(file, "non-executable inventory path"),
);
const executableSet = new Set(executableModules);
const nonExecutableSet = new Set(nonExecutableModules);
const partition = [...executableModules, ...nonExecutableModules].sort();
if (
executableSet.size !== executableModules.length ||
nonExecutableSet.size !== nonExecutableModules.length ||
executableModules.some((file) => nonExecutableSet.has(file)) ||
partition.join("\n") !== [...inventory].sort().join("\n")
) {
throw new TypeError("production inventory executable provenance is inconsistent");
}
const generatedExclusions = input.inventory.generatedExclusions.map((file) =>
exactSourcePath(file, "generated exclusion"),
);
@@ -630,6 +772,25 @@ export function evaluateRiskCoverage(input: Readonly<{
const metrics = selected.get(file);
return metrics ? [metrics] : [];
});
const zeroCoverageModules = inventory.filter((file) => {
const metrics = selected.get(file);
return (
metrics !== undefined &&
coverageMetrics.every((metric) => metrics[metric].total === 0)
);
});
const zeroCoverageSet = new Set(zeroCoverageModules);
const zeroExecutableModules = zeroCoverageModules.filter((file) =>
executableSet.has(file),
);
const nonExecutableWithCounters = nonExecutableModules.filter((file) => {
const metrics = selected.get(file);
return metrics !== undefined && !zeroCoverageSet.has(file);
});
const instrumentedExecutableTotal = executableModules.filter((file) => {
const metrics = selected.get(file);
return metrics !== undefined && !zeroCoverageSet.has(file);
}).length;
assertMatchingTotal(totalMetrics, aggregateCoverage([...selected.values()]));
const recomputedInventoryMetrics = aggregateCoverage(inventoryMetrics);
@@ -645,9 +806,14 @@ export function evaluateRiskCoverage(input: Readonly<{
for (const metric of coverageMetrics) {
const threshold = minimum[metric];
const received = actual[metric].pct;
const passed = received >= threshold;
const hasCoverageTotal = actual[metric].total > 0;
const passed = hasCoverageTotal && received >= threshold;
results.push({ scope, metric, threshold, received, passed });
if (!passed) failures.push(`${scope}.${metric} expected >= ${threshold}, received ${received}`);
if (!hasCoverageTotal) {
failures.push(`${scope}.${metric} coverage total must be greater than 0`);
} else if (!passed) {
failures.push(`${scope}.${metric} expected >= ${threshold}, received ${received}`);
}
}
}
@@ -657,13 +823,32 @@ export function evaluateRiskCoverage(input: Readonly<{
`repository module baseline expected >= ${input.policy.repositoryBaseline}, received ${inventory.length}`,
);
}
const uncoveredModules = inventory.filter((file) => !selected.has(file)).sort();
failures.push(...uncoveredModules.map((file) => `production module missing from coverage: ${file}`));
const uncoveredModules = inventory
.filter(
(file) =>
!selected.has(file) ||
(executableSet.has(file) && zeroCoverageSet.has(file)),
)
.sort();
failures.push(
...inventory
.filter((file) => !selected.has(file))
.map((file) => `production module missing from coverage: ${file}`),
...zeroExecutableModules.map(
(file) => `production module has zero coverage totals: ${file}`,
),
...nonExecutableWithCounters.map(
(file) => `non-executable module has coverage counters: ${file}`,
),
);
for (const modulePolicy of input.policy.criticalModules) {
if (!inventorySet.has(modulePolicy.path)) {
failures.push(`critical module is outside production inventory: ${modulePolicy.path}`);
continue;
}
if (nonExecutableSet.has(modulePolicy.path)) {
failures.push(`critical module cannot be non-executable: ${modulePolicy.path}`);
}
const actual = selected.get(modulePolicy.path);
if (!actual) {
failures.push(`critical module missing from coverage: ${modulePolicy.path}`);
@@ -678,6 +863,9 @@ export function evaluateRiskCoverage(input: Readonly<{
if (!inventorySet.has(highRiskPath)) {
failures.push(`high-risk module is outside production inventory: ${highRiskPath}`);
}
if (nonExecutableSet.has(highRiskPath)) {
failures.push(`high-risk module cannot be non-executable: ${highRiskPath}`);
}
}
for (const waiver of input.policy.waivers) {
if (!inventorySet.has(waiver.path)) failures.push(`coverage waiver is stale: ${waiver.path}`);
@@ -693,6 +881,10 @@ export function evaluateRiskCoverage(input: Readonly<{
status: failures.length === 0 ? "PASS" : "FAIL",
selectedTotal: inventory.length - uncoveredModules.length,
repositoryTotal: inventory.length,
executableTotal: executableModules.length,
instrumentedExecutableTotal,
nonExecutableTotal: nonExecutableModules.length,
nonExecutableModules: Object.freeze([...nonExecutableModules].sort()),
preExclusionTotal: input.inventory.preExclusionTotal,
generatedExclusionCount: generatedExclusions.length,
generatedExclusions: Object.freeze([...generatedExclusions].sort()),
+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-"));