fix: reject empty coverage counters
This commit is contained in:
@@ -90,16 +90,16 @@ function hasStableIdentity(metadata: Stats): boolean {
|
|||||||
return (
|
return (
|
||||||
Number.isSafeInteger(metadata.dev) &&
|
Number.isSafeInteger(metadata.dev) &&
|
||||||
Number.isSafeInteger(metadata.ino) &&
|
Number.isSafeInteger(metadata.ino) &&
|
||||||
(metadata.dev !== 0 || metadata.ino !== 0)
|
metadata.dev > 0 &&
|
||||||
|
metadata.ino > 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameFileIdentity(before: Stats, after: Stats): boolean {
|
function sameFileIdentity(before: Stats, after: Stats): boolean {
|
||||||
return (
|
if (!hasStableIdentity(before) || !hasStableIdentity(after)) {
|
||||||
!hasStableIdentity(before) ||
|
throw new TypeError("stable file identity unavailable");
|
||||||
!hasStableIdentity(after) ||
|
}
|
||||||
(before.dev === after.dev && before.ino === after.ino)
|
return before.dev === after.dev && before.ino === after.ino;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectSymlinkAncestors(
|
async function rejectSymlinkAncestors(
|
||||||
@@ -251,16 +251,20 @@ export async function resolveRiskCoverageArtifactPath(input: Readonly<{
|
|||||||
if (!hasErrorCode(error, "ENOENT")) throw error;
|
if (!hasErrorCode(error, "ENOENT")) throw error;
|
||||||
}
|
}
|
||||||
if (destinationMetadata) {
|
if (destinationMetadata) {
|
||||||
|
if (!hasStableIdentity(destinationMetadata)) {
|
||||||
|
throw new TypeError("artifact stable file identity unavailable");
|
||||||
|
}
|
||||||
const destinationRealpath = await realpath(absolutePath);
|
const destinationRealpath = await realpath(absolutePath);
|
||||||
for (const inputPath of normalizedInputs) {
|
for (const inputPath of normalizedInputs) {
|
||||||
const inputAbsolutePath = path.resolve(repositoryRoot, inputPath);
|
const inputAbsolutePath = path.resolve(repositoryRoot, inputPath);
|
||||||
const inputRealpath = await realpath(inputAbsolutePath);
|
const inputRealpath = await realpath(inputAbsolutePath);
|
||||||
const inputMetadata = await lstat(inputAbsolutePath);
|
const inputMetadata = await lstat(inputAbsolutePath);
|
||||||
|
if (!hasStableIdentity(inputMetadata)) {
|
||||||
|
throw new TypeError(`input stable file identity unavailable: ${inputPath}`);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
inputRealpath === destinationRealpath ||
|
inputRealpath === destinationRealpath ||
|
||||||
(hasStableIdentity(inputMetadata) &&
|
(inputMetadata.dev === destinationMetadata.dev &&
|
||||||
hasStableIdentity(destinationMetadata) &&
|
|
||||||
inputMetadata.dev === destinationMetadata.dev &&
|
|
||||||
inputMetadata.ino === destinationMetadata.ino)
|
inputMetadata.ino === destinationMetadata.ino)
|
||||||
) {
|
) {
|
||||||
throw new TypeError(`artifact path is the same file as an input: ${inputPath}`);
|
throw new TypeError(`artifact path is the same file as an input: ${inputPath}`);
|
||||||
|
|||||||
+204
-12
@@ -4,9 +4,9 @@ import {
|
|||||||
open,
|
open,
|
||||||
readdir,
|
readdir,
|
||||||
realpath,
|
realpath,
|
||||||
type FileHandle,
|
|
||||||
} from "node:fs/promises";
|
} from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import babelParser from "@babel/eslint-parser";
|
||||||
|
|
||||||
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
|
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
|
||||||
|
|
||||||
@@ -63,12 +63,18 @@ export type ProductionModuleInventory = Readonly<{
|
|||||||
files: readonly string[];
|
files: readonly string[];
|
||||||
preExclusionTotal: number;
|
preExclusionTotal: number;
|
||||||
generatedExclusions: readonly string[];
|
generatedExclusions: readonly string[];
|
||||||
|
executableModules: readonly string[];
|
||||||
|
nonExecutableModules: readonly string[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type RiskCoverageResult = Readonly<{
|
export type RiskCoverageResult = Readonly<{
|
||||||
status: "PASS" | "FAIL";
|
status: "PASS" | "FAIL";
|
||||||
selectedTotal: number;
|
selectedTotal: number;
|
||||||
repositoryTotal: number;
|
repositoryTotal: number;
|
||||||
|
executableTotal: number;
|
||||||
|
instrumentedExecutableTotal: number;
|
||||||
|
nonExecutableTotal: number;
|
||||||
|
nonExecutableModules: readonly string[];
|
||||||
preExclusionTotal: number;
|
preExclusionTotal: number;
|
||||||
generatedExclusionCount: number;
|
generatedExclusionCount: number;
|
||||||
generatedExclusions: readonly string[];
|
generatedExclusions: readonly string[];
|
||||||
@@ -86,7 +92,11 @@ export type RiskCoverageResult = Readonly<{
|
|||||||
failures: readonly string[];
|
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<{
|
type InventoryOptions = Readonly<{
|
||||||
repositoryRoot?: string;
|
repositoryRoot?: string;
|
||||||
generatedPaths?: readonly string[];
|
generatedPaths?: readonly string[];
|
||||||
@@ -97,6 +107,7 @@ type InventoryOptions = Readonly<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
class FileIdentityChangedError extends Error {}
|
class FileIdentityChangedError extends Error {}
|
||||||
|
class StableFileIdentityUnavailableError extends Error {}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
@@ -333,16 +344,18 @@ function hasStableIdentity(metadata: Stats): boolean {
|
|||||||
return (
|
return (
|
||||||
Number.isSafeInteger(metadata.dev) &&
|
Number.isSafeInteger(metadata.dev) &&
|
||||||
Number.isSafeInteger(metadata.ino) &&
|
Number.isSafeInteger(metadata.ino) &&
|
||||||
(metadata.dev !== 0 || metadata.ino !== 0)
|
metadata.dev > 0 &&
|
||||||
|
metadata.ino > 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameFileIdentity(before: Stats, after: Stats): boolean {
|
function sameFileIdentity(before: Stats, after: Stats): boolean {
|
||||||
return (
|
if (!hasStableIdentity(before) || !hasStableIdentity(after)) {
|
||||||
!hasStableIdentity(before) ||
|
throw new StableFileIdentityUnavailableError(
|
||||||
!hasStableIdentity(after) ||
|
"stable file identity unavailable",
|
||||||
(before.dev === after.dev && before.ino === after.ino)
|
);
|
||||||
);
|
}
|
||||||
|
return before.dev === after.dev && before.ino === after.ino;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isProductionModulePath(relativePath: string): boolean {
|
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(
|
export async function buildProductionModuleInventory(
|
||||||
options: InventoryOptions = {},
|
options: InventoryOptions = {},
|
||||||
): Promise<ProductionModuleInventory> {
|
): Promise<ProductionModuleInventory> {
|
||||||
@@ -377,6 +481,8 @@ export async function buildProductionModuleInventory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allModules: string[] = [];
|
const allModules: string[] = [];
|
||||||
|
const executableModules: string[] = [];
|
||||||
|
const nonExecutableModules: string[] = [];
|
||||||
async function visit(relativeDirectory: string): Promise<void> {
|
async function visit(relativeDirectory: string): Promise<void> {
|
||||||
const absoluteDirectory = path.join(repositoryRoot, relativeDirectory);
|
const absoluteDirectory = path.join(repositoryRoot, relativeDirectory);
|
||||||
const entries = await readDirectory(absoluteDirectory);
|
const entries = await readDirectory(absoluteDirectory);
|
||||||
@@ -403,6 +509,7 @@ export async function buildProductionModuleInventory(
|
|||||||
throw new TypeError(`production inventory path is outside repository: ${relativeTarget}`);
|
throw new TypeError(`production inventory path is outside repository: ${relativeTarget}`);
|
||||||
}
|
}
|
||||||
let handle: ReadableFileHandle | undefined;
|
let handle: ReadableFileHandle | undefined;
|
||||||
|
let source: string;
|
||||||
try {
|
try {
|
||||||
handle = await openFile(
|
handle = await openFile(
|
||||||
absoluteTarget,
|
absoluteTarget,
|
||||||
@@ -415,7 +522,14 @@ export async function buildProductionModuleInventory(
|
|||||||
if (!sameFileIdentity(metadata, openedMetadata)) {
|
if (!sameFileIdentity(metadata, openedMetadata)) {
|
||||||
throw new FileIdentityChangedError("opened file identity changed");
|
throw new FileIdentityChangedError("opened file identity changed");
|
||||||
}
|
}
|
||||||
|
source = await handle.readFile("utf8");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof StableFileIdentityUnavailableError) {
|
||||||
|
throw new Error(
|
||||||
|
`production inventory stable file identity unavailable: ${relativeTarget}`,
|
||||||
|
{ cause: error },
|
||||||
|
);
|
||||||
|
}
|
||||||
if (error instanceof FileIdentityChangedError) {
|
if (error instanceof FileIdentityChangedError) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`production inventory file changed during validation: ${relativeTarget}`,
|
`production inventory file changed during validation: ${relativeTarget}`,
|
||||||
@@ -429,6 +543,11 @@ export async function buildProductionModuleInventory(
|
|||||||
await handle?.close();
|
await handle?.close();
|
||||||
}
|
}
|
||||||
allModules.push(relativeTarget);
|
allModules.push(relativeTarget);
|
||||||
|
if (hasExecutableTypeScriptStatements(source, relativeTarget)) {
|
||||||
|
executableModules.push(relativeTarget);
|
||||||
|
} else {
|
||||||
|
nonExecutableModules.push(relativeTarget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await visit("src");
|
await visit("src");
|
||||||
@@ -446,6 +565,12 @@ export async function buildProductionModuleInventory(
|
|||||||
files: Object.freeze(inventory),
|
files: Object.freeze(inventory),
|
||||||
preExclusionTotal: allModules.length,
|
preExclusionTotal: allModules.length,
|
||||||
generatedExclusions: Object.freeze([...generatedPaths].sort()),
|
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) {
|
if (new Set(inventory).size !== inventory.length) {
|
||||||
throw new TypeError("production module inventory contains a duplicate path");
|
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) =>
|
const generatedExclusions = input.inventory.generatedExclusions.map((file) =>
|
||||||
exactSourcePath(file, "generated exclusion"),
|
exactSourcePath(file, "generated exclusion"),
|
||||||
);
|
);
|
||||||
@@ -630,6 +772,25 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
const metrics = selected.get(file);
|
const metrics = selected.get(file);
|
||||||
return metrics ? [metrics] : [];
|
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()]));
|
assertMatchingTotal(totalMetrics, aggregateCoverage([...selected.values()]));
|
||||||
const recomputedInventoryMetrics = aggregateCoverage(inventoryMetrics);
|
const recomputedInventoryMetrics = aggregateCoverage(inventoryMetrics);
|
||||||
|
|
||||||
@@ -645,9 +806,14 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
for (const metric of coverageMetrics) {
|
for (const metric of coverageMetrics) {
|
||||||
const threshold = minimum[metric];
|
const threshold = minimum[metric];
|
||||||
const received = actual[metric].pct;
|
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 });
|
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}`,
|
`repository module baseline expected >= ${input.policy.repositoryBaseline}, received ${inventory.length}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const uncoveredModules = inventory.filter((file) => !selected.has(file)).sort();
|
const uncoveredModules = inventory
|
||||||
failures.push(...uncoveredModules.map((file) => `production module missing from coverage: ${file}`));
|
.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) {
|
for (const modulePolicy of input.policy.criticalModules) {
|
||||||
if (!inventorySet.has(modulePolicy.path)) {
|
if (!inventorySet.has(modulePolicy.path)) {
|
||||||
failures.push(`critical module is outside production inventory: ${modulePolicy.path}`);
|
failures.push(`critical module is outside production inventory: ${modulePolicy.path}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (nonExecutableSet.has(modulePolicy.path)) {
|
||||||
|
failures.push(`critical module cannot be non-executable: ${modulePolicy.path}`);
|
||||||
|
}
|
||||||
const actual = selected.get(modulePolicy.path);
|
const actual = selected.get(modulePolicy.path);
|
||||||
if (!actual) {
|
if (!actual) {
|
||||||
failures.push(`critical module missing from coverage: ${modulePolicy.path}`);
|
failures.push(`critical module missing from coverage: ${modulePolicy.path}`);
|
||||||
@@ -678,6 +863,9 @@ export function evaluateRiskCoverage(input: Readonly<{
|
|||||||
if (!inventorySet.has(highRiskPath)) {
|
if (!inventorySet.has(highRiskPath)) {
|
||||||
failures.push(`high-risk module is outside production inventory: ${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) {
|
for (const waiver of input.policy.waivers) {
|
||||||
if (!inventorySet.has(waiver.path)) failures.push(`coverage waiver is stale: ${waiver.path}`);
|
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",
|
status: failures.length === 0 ? "PASS" : "FAIL",
|
||||||
selectedTotal: inventory.length - uncoveredModules.length,
|
selectedTotal: inventory.length - uncoveredModules.length,
|
||||||
repositoryTotal: inventory.length,
|
repositoryTotal: inventory.length,
|
||||||
|
executableTotal: executableModules.length,
|
||||||
|
instrumentedExecutableTotal,
|
||||||
|
nonExecutableTotal: nonExecutableModules.length,
|
||||||
|
nonExecutableModules: Object.freeze([...nonExecutableModules].sort()),
|
||||||
preExclusionTotal: input.inventory.preExclusionTotal,
|
preExclusionTotal: input.inventory.preExclusionTotal,
|
||||||
generatedExclusionCount: generatedExclusions.length,
|
generatedExclusionCount: generatedExclusions.length,
|
||||||
generatedExclusions: Object.freeze([...generatedExclusions].sort()),
|
generatedExclusions: Object.freeze([...generatedExclusions].sort()),
|
||||||
|
|||||||
+18
-18
@@ -1,50 +1,50 @@
|
|||||||
{
|
{
|
||||||
"total": {
|
"total": {
|
||||||
"lines": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 },
|
"lines": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
|
||||||
"statements": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 },
|
"statements": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
|
||||||
"functions": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 },
|
"functions": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
|
||||||
"branches": { "total": 0, "covered": 0, "skipped": 0, "pct": 100 }
|
"branches": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 }
|
||||||
},
|
},
|
||||||
"src/adapters/http/bounded-body-reader.ts": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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": {
|
"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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,36 @@ describe("risk coverage CLI files", () => {
|
|||||||
).rejects.toThrow(/changed during validation/u);
|
).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 () => {
|
it("confines artifact output and rejects input overwrite or symlink ancestors", async () => {
|
||||||
const repositoryRoot = await fixture();
|
const repositoryRoot = await fixture();
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -48,11 +48,16 @@ const fullMetrics = metrics();
|
|||||||
function inventory(
|
function inventory(
|
||||||
files: readonly string[],
|
files: readonly string[],
|
||||||
generatedExclusions: readonly string[] = [],
|
generatedExclusions: readonly string[] = [],
|
||||||
|
nonExecutableModules: readonly string[] = [],
|
||||||
): ProductionModuleInventory {
|
): ProductionModuleInventory {
|
||||||
return {
|
return {
|
||||||
files,
|
files,
|
||||||
preExclusionTotal: files.length + generatedExclusions.length,
|
preExclusionTotal: files.length + generatedExclusions.length,
|
||||||
generatedExclusions,
|
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"],
|
files: ["src/a.ts", "src/nested/b.tsx"],
|
||||||
preExclusionTotal: 3,
|
preExclusionTotal: 3,
|
||||||
generatedExclusions: ["src/generated.ts"],
|
generatedExclusions: ["src/generated.ts"],
|
||||||
|
executableModules: ["src/a.ts", "src/nested/b.tsx"],
|
||||||
|
nonExecutableModules: [],
|
||||||
});
|
});
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
status: "FAIL",
|
status: "FAIL",
|
||||||
@@ -318,6 +325,170 @@ describe("repository-aware risk coverage", () => {
|
|||||||
).toThrow(/coverage total\.lines does not match recomputed inventory total/u);
|
).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", () => {
|
it("accepts and validates Vitest branchesTrue without evaluating it", () => {
|
||||||
const parsedPolicy = parseRiskCoveragePolicy(
|
const parsedPolicy = parseRiskCoveragePolicy(
|
||||||
policy({ repositoryBaseline: 1, generatedPaths: [] }),
|
policy({ repositoryBaseline: 1, generatedPaths: [] }),
|
||||||
@@ -369,6 +540,75 @@ describe("repository-aware risk coverage", () => {
|
|||||||
expect(observedFlags & constants.O_NOFOLLOW).toBe(constants.O_NOFOLLOW);
|
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 () => {
|
it("rejects a post-lstat file identity swap even without relying on O_NOFOLLOW", async () => {
|
||||||
const repositoryRoot = await repositoryFixture();
|
const repositoryRoot = await repositoryFixture();
|
||||||
const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-race-"));
|
const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-race-"));
|
||||||
|
|||||||
Reference in New Issue
Block a user