From 67cd37659dcca2af0c6bef2df92f61a79b54431c Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Sun, 2 Aug 2026 08:20:25 +0900 Subject: [PATCH] fix: harden repository coverage evidence --- scripts/check-risk-coverage.ts | 80 +-- scripts/lib/risk-coverage-files.ts | 249 ++++++++ scripts/lib/risk-coverage.ts | 502 +++++++++------ .../coverage/repository-omission.json | 36 +- tests/unit/risk-coverage-files.test.ts | 240 ++++++++ tests/unit/risk-coverage.test.ts | 580 ++++++++++-------- 6 files changed, 1178 insertions(+), 509 deletions(-) create mode 100644 scripts/lib/risk-coverage-files.ts create mode 100644 tests/unit/risk-coverage-files.test.ts diff --git a/scripts/check-risk-coverage.ts b/scripts/check-risk-coverage.ts index 910b1fc..6233641 100644 --- a/scripts/check-risk-coverage.ts +++ b/scripts/check-risk-coverage.ts @@ -1,6 +1,9 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; +import { + readRiskCoverageInput, + writeRiskCoverageArtifactAtomic, +} from "./lib/risk-coverage-files.ts"; import { buildProductionModuleInventory, evaluateRiskCoverage, @@ -18,71 +21,56 @@ function requiredArgument(name: string, fallback: string): string { return value; } -function parseChangedFiles(value: unknown): readonly string[] { - if ( - !Array.isArray(value) || - value.some((entry) => typeof entry !== "string") || - new Set(value).size !== value.length - ) { - throw new TypeError("changed files input must be an array of unique paths"); - } - return Object.freeze([...value] as string[]); -} - const repositoryRoot = path.resolve( requiredArgument("--repository-root", process.cwd()), ); -const policyPath = requiredArgument( - "--policy", - "config/testing/risk-coverage.json", -); -const summaryPath = requiredArgument( - "--summary", - "artifacts/tests/coverage/coverage-summary.json", -); +const policyInput = await readRiskCoverageInput({ + repositoryRoot, + relativePath: requiredArgument( + "--policy", + "config/testing/risk-coverage.json", + ), + label: "policy", +}); +const summaryInput = await readRiskCoverageInput({ + repositoryRoot, + relativePath: requiredArgument( + "--summary", + "artifacts/tests/coverage/coverage-summary.json", + ), + label: "summary", +}); const artifactPath = requiredArgument( "--artifact", "artifacts/quality/risk-coverage.json", ); -const changedFilesPath = argumentValue("--changed-files"); const policy = parseRepositoryRiskCoveragePolicy( - JSON.parse(await readFile(path.resolve(repositoryRoot, policyPath), "utf8")) as unknown, + JSON.parse(policyInput.text) as unknown, ); const inventory = await buildProductionModuleInventory({ repositoryRoot, generatedPaths: policy.generatedPaths, }); -const changedFiles = changedFilesPath - ? parseChangedFiles( - JSON.parse( - await readFile(path.resolve(repositoryRoot, changedFilesPath), "utf8"), - ) as unknown, - ) - : []; const result = evaluateRiskCoverage({ repositoryRoot, inventory, policy, - summary: JSON.parse( - await readFile(path.resolve(repositoryRoot, summaryPath), "utf8"), - ) as unknown, - changedFiles, + summary: JSON.parse(summaryInput.text) as unknown, +}); +await writeRiskCoverageArtifactAtomic({ + repositoryRoot, + relativePath: artifactPath, + inputPaths: [policyInput.relativePath, summaryInput.relativePath], + value: { + schemaVersion: 2, + policy: policyInput.relativePath, + summary: summaryInput.relativePath, + ...result, + }, }); -const artifact = { - schemaVersion: 2, - policy: policyPath, - summary: summaryPath, - changedFiles: changedFilesPath ?? null, - ...result, -}; -const resolvedArtifactPath = path.resolve(repositoryRoot, artifactPath); -await mkdir(path.dirname(resolvedArtifactPath), { recursive: true }); -await writeFile(resolvedArtifactPath, `${JSON.stringify(artifact, null, 2)}\n`); if (result.failures.length > 0) { - process.stderr.write( - `Risk coverage failed:\n- ${result.failures.join("\n- ")}\n`, - ); + process.stderr.write(`Risk coverage failed:\n- ${result.failures.join("\n- ")}\n`); process.exit(1); } process.stdout.write( diff --git a/scripts/lib/risk-coverage-files.ts b/scripts/lib/risk-coverage-files.ts new file mode 100644 index 0000000..aa5b747 --- /dev/null +++ b/scripts/lib/risk-coverage-files.ts @@ -0,0 +1,249 @@ +import { randomUUID } from "node:crypto"; +import { constants, type Stats } from "node:fs"; +import { + lstat, + mkdir, + open, + realpath, + rename, + rm, +} from "node:fs/promises"; +import path from "node:path"; + +import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts"; + +type WritableHandle = Readonly<{ + writeFile(data: string): Promise; + sync(): Promise; + close(): Promise; +}>; + +type DirectoryHandle = Readonly<{ + sync(): Promise; + close(): Promise; +}>; + +export type RiskCoverageArtifactFileSystem = Readonly<{ + openFile(target: string, flags: number, mode: number): Promise; + openDirectory(target: string): Promise; + rename(source: string, destination: string): Promise; + rm(target: string, options: Readonly<{ force: true }>): Promise; +}>; + +type WriterDependencies = Readonly<{ + createNonce?: () => string; + fileSystem?: RiskCoverageArtifactFileSystem; +}>; + +const defaultFileSystem: RiskCoverageArtifactFileSystem = Object.freeze({ + openFile: async (target, flags, mode) => { + const handle = await open(target, flags, mode); + return { + writeFile: async (data) => handle.writeFile(data, "utf8"), + sync: async () => handle.sync(), + close: async () => handle.close(), + }; + }, + openDirectory: async (target) => { + const handle = await open(target, constants.O_RDONLY); + return { + sync: async () => handle.sync(), + close: async () => handle.close(), + }; + }, + rename, + rm, +}); + +function hasErrorCode(error: unknown, code: string): boolean { + return ( + typeof error === "object" && + error !== null && + "code" in error && + error.code === code + ); +} + +function isWithin(root: string, target: string): boolean { + const relative = path.relative(root, target); + return ( + relative === "" || + (relative !== ".." && + !relative.startsWith(`..${path.sep}`) && + !path.isAbsolute(relative)) + ); +} + +function assertDirectory(metadata: Stats, relativePath: string): void { + if (metadata.isSymbolicLink()) { + throw new TypeError(`artifact ancestor is a symlink: ${relativePath}`); + } + if (!metadata.isDirectory()) { + throw new TypeError(`artifact ancestor is not a directory: ${relativePath}`); + } +} + +export async function readRiskCoverageInput(input: Readonly<{ + repositoryRoot: string; + relativePath: string; + label: string; +}>): Promise> { + const relativePath = normalizeRepositoryRelativePath( + input.relativePath, + `${input.label} path`, + ); + const repositoryRoot = path.resolve(input.repositoryRoot); + const repositoryRealpath = await realpath(repositoryRoot); + const absolutePath = path.resolve(repositoryRoot, relativePath); + const metadata = await lstat(absolutePath); + if (metadata.isSymbolicLink()) { + throw new TypeError(`${input.label} path is a symlink: ${relativePath}`); + } + if (!metadata.isFile()) { + throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`); + } + const resolvedPath = await realpath(absolutePath); + if (!isWithin(repositoryRealpath, resolvedPath)) { + throw new TypeError(`${input.label} path is outside repository: ${relativePath}`); + } + + const handle = await open( + absolutePath, + constants.O_RDONLY | constants.O_NOFOLLOW, + ); + try { + const openedMetadata = await handle.stat(); + if (!openedMetadata.isFile()) { + throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`); + } + const text = await handle.readFile("utf8"); + return Object.freeze({ relativePath, absolutePath, text }); + } finally { + await handle.close(); + } +} + +export async function resolveRiskCoverageArtifactPath(input: Readonly<{ + repositoryRoot: string; + relativePath: string; + inputPaths: readonly string[]; +}>): Promise { + const relativePath = normalizeRepositoryRelativePath( + input.relativePath, + "artifact path", + ); + if (!relativePath.startsWith("artifacts/quality/")) { + throw new TypeError("artifact path must be below artifacts/quality"); + } + const normalizedInputs = input.inputPaths.map((inputPath) => + normalizeRepositoryRelativePath(inputPath, "input path"), + ); + if (normalizedInputs.includes(relativePath)) { + throw new TypeError(`artifact path must not overwrite an input: ${relativePath}`); + } + + const repositoryRoot = path.resolve(input.repositoryRoot); + const repositoryRealpath = await realpath(repositoryRoot); + const relativeDirectory = path.posix.dirname(relativePath); + let currentDirectory = repositoryRoot; + let currentRelative = ""; + for (const segment of relativeDirectory.split("/")) { + currentDirectory = path.join(currentDirectory, segment); + currentRelative = currentRelative ? `${currentRelative}/${segment}` : segment; + let metadata: Stats; + try { + metadata = await lstat(currentDirectory); + } catch (error) { + if (!hasErrorCode(error, "ENOENT")) throw error; + try { + await mkdir(currentDirectory); + } catch (mkdirError) { + if (!hasErrorCode(mkdirError, "EEXIST")) throw mkdirError; + } + metadata = await lstat(currentDirectory); + } + assertDirectory(metadata, currentRelative); + const resolvedDirectory = await realpath(currentDirectory); + if (!isWithin(repositoryRealpath, resolvedDirectory)) { + throw new TypeError(`artifact ancestor is outside repository: ${currentRelative}`); + } + } + + const absolutePath = path.resolve(repositoryRoot, relativePath); + try { + const metadata = await lstat(absolutePath); + if (metadata.isSymbolicLink()) { + throw new TypeError(`artifact path is a symlink: ${relativePath}`); + } + if (!metadata.isFile()) { + throw new TypeError(`artifact path is not a regular file: ${relativePath}`); + } + } catch (error) { + if (!hasErrorCode(error, "ENOENT")) throw error; + } + return absolutePath; +} + +export async function writeRiskCoverageArtifactAtomic( + input: Readonly<{ + repositoryRoot: string; + relativePath: string; + inputPaths: readonly string[]; + value: unknown; + }>, + dependencies: WriterDependencies = {}, +): Promise { + const serialized = JSON.stringify(input.value, null, 2); + if (serialized === undefined) { + throw new TypeError("risk coverage artifact is not JSON serializable"); + } + const destination = await resolveRiskCoverageArtifactPath(input); + const temporaryPath = path.join( + path.dirname(destination), + `.${path.basename(destination)}.${(dependencies.createNonce ?? randomUUID)()}.tmp`, + ); + const fileSystem = dependencies.fileSystem ?? defaultFileSystem; + let ownsTemporaryFile = false; + try { + const handle = await fileSystem.openFile( + temporaryPath, + constants.O_WRONLY | + constants.O_CREAT | + constants.O_EXCL | + constants.O_NOFOLLOW, + 0o600, + ); + ownsTemporaryFile = true; + let primaryFailure: unknown; + try { + await handle.writeFile(`${serialized}\n`); + await handle.sync(); + } catch (error) { + primaryFailure = error; + } + try { + await handle.close(); + } catch (error) { + primaryFailure ??= error; + } + if (primaryFailure !== undefined) throw primaryFailure; + + await fileSystem.rename(temporaryPath, destination); + ownsTemporaryFile = false; + const directoryHandle = await fileSystem.openDirectory(path.dirname(destination)); + try { + await directoryHandle.sync(); + } finally { + await directoryHandle.close(); + } + } catch (error) { + if (ownsTemporaryFile) { + try { + await fileSystem.rm(temporaryPath, { force: true }); + } catch { + // Preserve the publication failure and clean only our nonce-owned path. + } + } + throw error; + } +} diff --git a/scripts/lib/risk-coverage.ts b/scripts/lib/risk-coverage.ts index ca103e7..add9050 100644 --- a/scripts/lib/risk-coverage.ts +++ b/scripts/lib/risk-coverage.ts @@ -1,5 +1,11 @@ -import { open, readdir, realpath, lstat } from "node:fs/promises"; -import type { Dirent, Stats } from "node:fs"; +import { constants, type Dirent, type Stats } from "node:fs"; +import { + lstat, + open, + readdir, + realpath, + type FileHandle, +} from "node:fs/promises"; import path from "node:path"; import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts"; @@ -10,6 +16,8 @@ const coverageMetrics = [ "functions", "branches", ] as const; +const teamIdPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u; +const maximumWaiverDurationMs = 90 * 24 * 60 * 60 * 1_000; export const REQUIRED_HIGH_RISK_PATHS = Object.freeze([ "src/adapters/http/http-execution-v3.ts", @@ -23,10 +31,14 @@ export const REQUIRED_HIGH_RISK_PATHS = Object.freeze([ ] as const); type CoverageMetric = (typeof coverageMetrics)[number]; -type Thresholds = Readonly>>; -type CoverageMetrics = Readonly< - Record> ->; +type Thresholds = Readonly>; +type CoverageCounter = Readonly<{ + total: number; + covered: number; + skipped: number; + pct: number; +}>; +type CoverageMetrics = Readonly>; export type RiskCoveragePolicy = Readonly<{ schemaVersion: 2; @@ -47,12 +59,23 @@ export type RiskCoveragePolicy = Readonly<{ }>[]; }>; +export type ProductionModuleInventory = Readonly<{ + files: readonly string[]; + preExclusionTotal: number; + generatedExclusions: readonly string[]; +}>; + export type RiskCoverageResult = Readonly<{ status: "PASS" | "FAIL"; selectedTotal: number; repositoryTotal: number; + preExclusionTotal: number; + generatedExclusionCount: number; + generatedExclusions: readonly string[]; + ownershipScope: "ALL_POLICY_HIGH_RISK"; + ownedHighRiskPaths: readonly string[]; + waivedHighRiskPaths: readonly string[]; uncoveredModules: readonly string[]; - ignoredCoveragePaths: readonly string[]; results: readonly Readonly<{ scope: string; metric: CoverageMetric; @@ -63,13 +86,14 @@ export type RiskCoverageResult = Readonly<{ failures: readonly string[]; }>; +type ReadableFileHandle = Pick; type InventoryOptions = Readonly<{ repositoryRoot?: string; generatedPaths?: readonly string[]; readDirectory?: (target: string) => Promise; lstatPath?: (target: string) => Promise; realpathPath?: (target: string) => Promise; - assertReadable?: (target: string) => Promise; + openFile?: (target: string, flags: number) => Promise; }>; function isRecord(value: unknown): value is Record { @@ -90,9 +114,7 @@ function assertExactKeys( function exactSourcePath(value: unknown, label: string): string { if ( typeof value !== "string" || - ["*", "?", "[", "]", "{", "}"].some((character) => - value.includes(character), - ) + ["*", "?", "[", "]", "{", "}"].some((character) => value.includes(character)) ) { throw new TypeError(`${label} must be an exact repository-relative POSIX path`); } @@ -103,20 +125,15 @@ function exactSourcePath(value: unknown, label: string): string { return normalized; } -function nonBlank(value: unknown, label: string): string { - if (typeof value !== "string" || !value.trim()) { - throw new TypeError(`${label} must be a nonblank string`); - } - return value.trim(); -} - function uniquePaths( value: unknown, label: string, options: Readonly<{ allowEmpty: boolean }> = { allowEmpty: true }, ): readonly string[] { if (!Array.isArray(value) || (!options.allowEmpty && value.length === 0)) { - throw new TypeError(`${label} must be an array${options.allowEmpty ? "" : " with at least one path"}`); + throw new TypeError( + `${label} must be an array${options.allowEmpty ? "" : " with at least one path"}`, + ); } const paths = value.map((entry) => exactSourcePath(entry, `${label} entry`)); if (new Set(paths).size !== paths.length) { @@ -125,43 +142,60 @@ function uniquePaths( return Object.freeze(paths); } -function thresholds( - value: unknown, - label: string, - options: Readonly<{ requireAll: boolean }>, -): Thresholds { - if (!isRecord(value)) { - throw new TypeError(`${label} must be an object`); +function teamId(value: unknown, label: string): string { + if (typeof value !== "string" || !teamIdPattern.test(value)) { + throw new TypeError(`${label} must be a canonical team id`); } + return value; +} + +function thresholds(value: unknown, label: string): Thresholds { + if (!isRecord(value)) throw new TypeError(`${label} must be an object`); assertExactKeys(value, coverageMetrics, label); - if ( - Object.keys(value).length === 0 || - (options.requireAll && coverageMetrics.some((metric) => !(metric in value))) - ) { - throw new TypeError(`${label} must define ${options.requireAll ? "all " : "at least one "}coverage metric`); + if (coverageMetrics.some((metric) => !(metric in value))) { + throw new TypeError(`${label} must define all coverage metrics`); } - const parsed: Partial> = {}; - for (const [metric, threshold] of Object.entries(value)) { + const parsed = {} as Record; + for (const metric of coverageMetrics) { + const threshold = value[metric]; if ( typeof threshold !== "number" || !Number.isFinite(threshold) || - threshold < 0 || + threshold <= 0 || threshold > 100 ) { - throw new TypeError(`${label}.${metric} minimum must be a finite number from 0 to 100`); + throw new TypeError( + `${label}.${metric} minimum must be a finite number greater than 0 and at most 100`, + ); } - parsed[metric as CoverageMetric] = threshold; + parsed[metric] = threshold; } return Object.freeze(parsed); } +function waiverReason(value: unknown, label: string): string { + if (typeof value !== "string" || value.length < 12 || value.length > 240) { + throw new TypeError(`${label} must contain 12 to 240 characters`); + } + if (value !== value.trim()) { + throw new TypeError(`${label} must not contain surrounding whitespace`); + } + if ( + [...value].some((character) => { + const codePoint = character.codePointAt(0) ?? 0; + return codePoint <= 31 || codePoint === 127; + }) + ) { + throw new TypeError(`${label} must not contain control characters`); + } + return value; +} + export function parseRiskCoveragePolicy( value: unknown, options: Readonly<{ now?: number }> = {}, ): RiskCoveragePolicy { - if (!isRecord(value)) { - throw new TypeError("risk coverage policy must be an object"); - } + if (!isRecord(value)) throw new TypeError("risk coverage policy must be an object"); assertExactKeys( value, [ @@ -180,11 +214,12 @@ export function parseRiskCoveragePolicy( } if ( typeof value.repositoryBaseline !== "number" || - !Number.isInteger(value.repositoryBaseline) || + !Number.isSafeInteger(value.repositoryBaseline) || value.repositoryBaseline <= 0 ) { - throw new TypeError("repositoryBaseline must be a positive integer"); + throw new TypeError("repositoryBaseline must be a positive safe integer"); } + const generatedPaths = uniquePaths(value.generatedPaths, "generatedPaths"); const highRiskPaths = uniquePaths(value.highRiskPaths, "highRiskPaths", { allowEmpty: false, @@ -199,48 +234,65 @@ export function parseRiskCoveragePolicy( assertExactKeys(candidate, ["path", "owner", "minimum"], `criticalModules[${index}]`); return Object.freeze({ path: exactSourcePath(candidate.path, `criticalModules[${index}].path`), - owner: nonBlank(candidate.owner, `criticalModules[${index}].owner`), - minimum: thresholds(candidate.minimum, `criticalModules[${index}].minimum`, { - requireAll: false, - }), + owner: teamId(candidate.owner, `criticalModules[${index}].owner`), + minimum: thresholds(candidate.minimum, `criticalModules[${index}].minimum`), }); }); if (new Set(criticalModules.map((entry) => entry.path)).size !== criticalModules.length) { throw new TypeError("criticalModules contains a duplicate path"); } - if (!Array.isArray(value.waivers)) { - throw new TypeError("waivers must be an array"); - } + + if (!Array.isArray(value.waivers)) throw new TypeError("waivers must be an array"); const currentTime = options.now ?? Date.now(); + if (!Number.isFinite(currentTime)) throw new TypeError("policy time must be finite"); const waivers = value.waivers.map((candidate, index) => { - if (!isRecord(candidate)) { - throw new TypeError(`waivers[${index}] must be an object`); - } + if (!isRecord(candidate)) throw new TypeError(`waivers[${index}] must be an object`); assertExactKeys(candidate, ["path", "owner", "reason", "expiresAt"], `waivers[${index}]`); const waiverPath = exactSourcePath(candidate.path, `waivers[${index}].path`); - const expiresAt = nonBlank(candidate.expiresAt, `waivers[${index}].expiresAt`); + const expiresAt = candidate.expiresAt; + if (typeof expiresAt !== "string") { + throw new TypeError(`waivers[${index}].expiresAt must be a canonical UTC ISO timestamp`); + } const expiry = Date.parse(expiresAt); - if (!Number.isFinite(expiry) || expiry <= currentTime) { - throw new TypeError(`waivers[${index}] is expired or has an invalid expiry`); + if (!Number.isFinite(expiry) || new Date(expiry).toISOString() !== expiresAt) { + throw new TypeError(`waivers[${index}].expiresAt must be a canonical UTC ISO timestamp`); + } + if (expiry <= currentTime) throw new TypeError(`waivers[${index}] is expired`); + if (expiry - currentTime > maximumWaiverDurationMs) { + throw new TypeError(`waivers[${index}] expiry must be within 90 days`); } if (!highRiskPaths.includes(waiverPath)) { throw new TypeError(`waivers[${index}] is stale because ${waiverPath} is not high-risk`); } return Object.freeze({ path: waiverPath, - owner: nonBlank(candidate.owner, `waivers[${index}].owner`), - reason: nonBlank(candidate.reason, `waivers[${index}].reason`), + owner: teamId(candidate.owner, `waivers[${index}].owner`), + reason: waiverReason(candidate.reason, `waivers[${index}].reason`), expiresAt, }); }); if (new Set(waivers.map((entry) => entry.path)).size !== waivers.length) { throw new TypeError("waivers contains a duplicate path"); } + + const criticalPaths = new Set(criticalModules.map((entry) => entry.path)); + const waiverPaths = new Set(waivers.map((entry) => entry.path)); + for (const highRiskPath of highRiskPaths) { + if (criticalPaths.has(highRiskPath) && waiverPaths.has(highRiskPath)) { + throw new TypeError( + `high-risk module cannot have both a critical owner and waiver: ${highRiskPath}`, + ); + } + if (!criticalPaths.has(highRiskPath) && !waiverPaths.has(highRiskPath)) { + throw new TypeError(`high-risk module has no owner or waiver: ${highRiskPath}`); + } + } + return Object.freeze({ schemaVersion: 2, repositoryBaseline: value.repositoryBaseline, generatedPaths, - summary: thresholds(value.summary, "summary", { requireAll: true }), + summary: thresholds(value.summary, "summary"), criticalModules: Object.freeze(criticalModules), highRiskPaths, waivers: Object.freeze(waivers), @@ -256,15 +308,15 @@ export function parseRepositoryRiskCoveragePolicy( if (!policy.highRiskPaths.includes(requiredPath)) { throw new TypeError(`required high-risk path is missing: ${requiredPath}`); } + if (policy.generatedPaths.includes(requiredPath)) { + throw new TypeError( + `required high-risk path cannot be generated-excluded: ${requiredPath}`, + ); + } } return policy; } -async function defaultAssertReadable(target: string): Promise { - const handle = await open(target, "r"); - await handle.close(); -} - function isWithin(root: string, target: string): boolean { const relative = path.relative(root, target); return ( @@ -275,7 +327,7 @@ function isWithin(root: string, target: string): boolean { ); } -function isProductionModule(relativePath: string): boolean { +export function isProductionModulePath(relativePath: string): boolean { return ( /\.tsx?$/u.test(relativePath) && !/\.d\.ts$/u.test(relativePath) && @@ -285,13 +337,15 @@ function isProductionModule(relativePath: string): boolean { export async function buildProductionModuleInventory( options: InventoryOptions = {}, -): Promise { +): Promise { const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd()); const sourceRoot = path.join(repositoryRoot, "src"); - const readDirectory = options.readDirectory ?? ((target) => readdir(target, { withFileTypes: true })); + const readDirectory = options.readDirectory ?? + ((target: string) => readdir(target, { withFileTypes: true })); const lstatPath = options.lstatPath ?? lstat; const realpathPath = options.realpathPath ?? realpath; - const assertReadable = options.assertReadable ?? defaultAssertReadable; + const openFile = options.openFile ?? + ((target: string, flags: number) => open(target, flags)); const generatedPaths = uniquePaths(options.generatedPaths ?? [], "generatedPaths"); const generated = new Set(generatedPaths); const repositoryRealpath = await realpathPath(repositoryRoot); @@ -322,7 +376,7 @@ export async function buildProductionModuleInventory( await visit(relativeTarget); continue; } - if (!isProductionModule(relativeTarget)) continue; + if (!isProductionModulePath(relativeTarget)) continue; if (!metadata.isFile()) { throw new TypeError(`production inventory path is not a regular file: ${relativeTarget}`); } @@ -330,12 +384,22 @@ export async function buildProductionModuleInventory( if (!isWithin(repositoryRealpath, resolvedTarget)) { throw new TypeError(`production inventory path is outside repository: ${relativeTarget}`); } + let handle: ReadableFileHandle | undefined; try { - await assertReadable(absoluteTarget); + handle = await openFile( + absoluteTarget, + constants.O_RDONLY | constants.O_NOFOLLOW, + ); + const openedMetadata = await handle.stat(); + if (!openedMetadata.isFile()) { + throw new TypeError("opened target is not a regular file"); + } } catch (error) { throw new Error(`production inventory file is unreadable: ${relativeTarget}`, { cause: error, }); + } finally { + await handle?.close(); } allModules.push(relativeTarget); } @@ -347,35 +411,89 @@ export async function buildProductionModuleInventory( } } const inventory = allModules.filter((file) => !generated.has(file)).sort(); - if (inventory.length === 0) { - throw new Error("production module inventory is empty"); - } + if (inventory.length === 0) throw new Error("production module inventory is empty"); if (new Set(inventory).size !== inventory.length) { throw new TypeError("production module inventory contains a duplicate path"); } - return Object.freeze(inventory); + return Object.freeze({ + files: Object.freeze(inventory), + preExclusionTotal: allModules.length, + generatedExclusions: Object.freeze([...generatedPaths].sort()), + }); } -function coveragePath(repositoryRoot: string, rawPath: string): string { - if (rawPath.includes("\\") || rawPath.includes("\0")) { +export function normalizeCoverageProducerPath(input: Readonly<{ + repositoryRoot: string; + rawPath: string; + platform?: "posix" | "win32"; +}>): string { + const platform = input.platform ?? (process.platform === "win32" ? "win32" : "posix"); + const pathApi = platform === "win32" ? path.win32 : path.posix; + const { rawPath } = input; + if (rawPath.includes("\0")) throw new TypeError("coverage path contains NUL"); + if (platform === "posix" && rawPath.includes("\\")) { throw new TypeError("coverage path must use POSIX separators"); } - if (path.isAbsolute(rawPath)) { - const relative = path.relative(repositoryRoot, rawPath).split(path.sep).join("/"); - if (!relative || relative === ".." || relative.startsWith("../")) { + if (platform === "win32" && rawPath.includes("\\") && !pathApi.isAbsolute(rawPath)) { + throw new TypeError("relative coverage path must use POSIX separators"); + } + if (pathApi.isAbsolute(rawPath)) { + const root = pathApi.resolve(input.repositoryRoot); + const relative = pathApi.relative(root, pathApi.resolve(rawPath)); + if ( + !relative || + relative === ".." || + relative.startsWith(`..${pathApi.sep}`) || + pathApi.isAbsolute(relative) + ) { throw new TypeError(`coverage path is outside repository: ${rawPath}`); } - return normalizeRepositoryRelativePath(relative, "coverage path"); + return normalizeRepositoryRelativePath( + relative.split(pathApi.sep).join("/"), + "coverage path", + ); } return normalizeRepositoryRelativePath(rawPath, "coverage path"); } +function expectedPct(total: number, covered: number): number { + return total === 0 ? 100 : Math.floor((covered / total) * 10_000) / 100; +} + +function parseCoverageCounter(value: unknown, label: string): CoverageCounter { + if (!isRecord(value)) throw new TypeError(`${label} must be an object`); + const fields = ["total", "covered", "skipped", "pct"] as const; + assertExactKeys(value, fields, label); + if (fields.some((field) => !(field in value))) { + throw new TypeError(`${label} must define total, covered, skipped, and pct`); + } + for (const count of ["total", "covered", "skipped"] as const) { + if ( + typeof value[count] !== "number" || + !Number.isSafeInteger(value[count]) || + value[count] < 0 + ) { + throw new TypeError(`${label}.${count} must be a nonnegative safe integer`); + } + } + const total = value.total as number; + const covered = value.covered as number; + const skipped = value.skipped as number; + if (covered + skipped > total) { + throw new TypeError(`${label} covered plus skipped must not exceed total`); + } + const pct = value.pct; + const calculatedPct = expectedPct(total, covered); + if (typeof pct !== "number" || !Number.isFinite(pct) || pct !== calculatedPct) { + throw new TypeError(`${label}.pct must equal ${calculatedPct}`); + } + return Object.freeze({ total, covered, skipped, pct }); +} + function parseCoverageMetrics(value: unknown, label: string): CoverageMetrics { if (!isRecord(value)) throw new TypeError(`${label} must be an object`); const unknownMetrics = Object.keys(value).filter( - (metric) => - metric !== "branchesTrue" && - !coverageMetrics.includes(metric as CoverageMetric), + (metric) => metric !== "branchesTrue" && !coverageMetrics.includes(metric as CoverageMetric), ); if (unknownMetrics.length > 0) { throw new TypeError( @@ -383,104 +501,85 @@ function parseCoverageMetrics(value: unknown, label: string): CoverageMetrics { ); } if (value.branchesTrue !== undefined) { - if (!isRecord(value.branchesTrue)) { - throw new TypeError(`${label}.branchesTrue must be an object`); - } - assertExactKeys( - value.branchesTrue, - ["total", "covered", "skipped", "pct"], - `${label}.branchesTrue`, - ); - for (const count of ["total", "covered", "skipped"] as const) { - const received = value.branchesTrue[count]; - if ( - typeof received !== "number" || - !Number.isSafeInteger(received) || - received < 0 - ) { - throw new TypeError( - `${label}.branchesTrue.${count} must be a nonnegative safe integer`, - ); - } - } - const pct = value.branchesTrue.pct; - if (typeof pct !== "number" || !Number.isFinite(pct) || pct < 0 || pct > 100) { - throw new TypeError( - `${label}.branchesTrue.pct must be a finite number from 0 to 100`, - ); - } - if ( - (value.branchesTrue.covered as number) > - (value.branchesTrue.total as number) || - (value.branchesTrue.skipped as number) > - (value.branchesTrue.total as number) - ) { - throw new TypeError(`${label}.branchesTrue counts exceed total`); - } + parseCoverageCounter(value.branchesTrue, `${label}.branchesTrue`); } - const parsed = {} as Record; + const parsed = {} as Record; for (const metric of coverageMetrics) { - const rawMetric = value[metric]; - if (!isRecord(rawMetric)) { - throw new TypeError(`${label}.${metric} must be an object`); - } - assertExactKeys( - rawMetric, - ["total", "covered", "skipped", "pct"], - `${label}.${metric}`, - ); - const suppliedCounts = ["total", "covered", "skipped"].filter( - (count) => rawMetric[count] !== undefined, - ); - if (suppliedCounts.length !== 0 && suppliedCounts.length !== 3) { - throw new TypeError( - `${label}.${metric} must define total, covered, and skipped together`, - ); - } - for (const count of suppliedCounts) { - const received = rawMetric[count]; - if ( - typeof received !== "number" || - !Number.isSafeInteger(received) || - received < 0 - ) { - throw new TypeError( - `${label}.${metric}.${count} must be a nonnegative safe integer`, - ); - } - } - if ( - suppliedCounts.length === 3 && - ((rawMetric.covered as number) > (rawMetric.total as number) || - (rawMetric.skipped as number) > (rawMetric.total as number)) - ) { - throw new TypeError(`${label}.${metric} counts exceed total`); - } - const pct = rawMetric.pct; - if (typeof pct !== "number" || !Number.isFinite(pct) || pct < 0 || pct > 100) { - throw new TypeError(`${label}.${metric}.pct must be a finite number from 0 to 100`); - } - parsed[metric] = { pct }; + parsed[metric] = parseCoverageCounter(value[metric], `${label}.${metric}`); } return Object.freeze(parsed); } +function aggregateCoverage(selected: readonly CoverageMetrics[]): CoverageMetrics { + const aggregate = {} as Record; + for (const metric of coverageMetrics) { + let total = 0; + let covered = 0; + let skipped = 0; + for (const metrics of selected) { + total += metrics[metric].total; + covered += metrics[metric].covered; + skipped += metrics[metric].skipped; + if (![total, covered, skipped].every(Number.isSafeInteger)) { + throw new TypeError(`recomputed coverage ${metric} count exceeds safe integer range`); + } + } + aggregate[metric] = Object.freeze({ + total, + covered, + skipped, + pct: expectedPct(total, covered), + }); + } + return Object.freeze(aggregate); +} + +function assertMatchingTotal( + producer: CoverageMetrics, + recomputed: CoverageMetrics, +): void { + for (const metric of coverageMetrics) { + const actual = producer[metric]; + const expected = recomputed[metric]; + if ( + actual.total !== expected.total || + actual.covered !== expected.covered || + actual.skipped !== expected.skipped || + actual.pct !== expected.pct + ) { + throw new TypeError( + `coverage total.${metric} does not match recomputed inventory total`, + ); + } + } +} + export function evaluateRiskCoverage(input: Readonly<{ repositoryRoot?: string; - inventory: readonly string[]; + inventory: ProductionModuleInventory; policy: RiskCoveragePolicy; summary: unknown; - changedFiles?: readonly string[]; - now?: number; }>): RiskCoverageResult { const repositoryRoot = path.resolve(input.repositoryRoot ?? process.cwd()); - if (input.inventory.length === 0) { - throw new TypeError("production module inventory is empty"); - } - const inventory = input.inventory.map((file) => exactSourcePath(file, "inventory path")); + const inventory = input.inventory.files.map((file) => exactSourcePath(file, "inventory path")); + if (inventory.length === 0) throw new TypeError("production module inventory is empty"); if (new Set(inventory).size !== inventory.length) { throw new TypeError("production module inventory contains a duplicate path"); } + const generatedExclusions = input.inventory.generatedExclusions.map((file) => + exactSourcePath(file, "generated exclusion"), + ); + if ( + new Set(generatedExclusions).size !== generatedExclusions.length || + input.inventory.preExclusionTotal !== inventory.length + generatedExclusions.length || + !Number.isSafeInteger(input.inventory.preExclusionTotal) + ) { + throw new TypeError("production inventory provenance is inconsistent"); + } + const policyGenerated = [...input.policy.generatedPaths].sort(); + if (generatedExclusions.join("\n") !== policyGenerated.join("\n")) { + throw new TypeError("production inventory generated exclusions do not match policy"); + } if (!isRecord(input.summary) || !("total" in input.summary)) { throw new TypeError("coverage summary must contain total metrics"); } @@ -488,13 +587,25 @@ export function evaluateRiskCoverage(input: Readonly<{ const selected = new Map(); for (const [rawPath, rawMetrics] of Object.entries(input.summary)) { if (rawPath === "total") continue; - const normalized = coveragePath(repositoryRoot, rawPath); - if (selected.has(normalized)) { - throw new TypeError(`duplicate coverage path: ${normalized}`); - } + const normalized = normalizeCoverageProducerPath({ repositoryRoot, rawPath }); + if (selected.has(normalized)) throw new TypeError(`duplicate coverage path: ${normalized}`); selected.set(normalized, parseCoverageMetrics(rawMetrics, `coverage ${normalized}`)); } + const inventorySet = new Set(inventory); + const generatedSet = new Set(generatedExclusions); + for (const selectedPath of selected.keys()) { + if (!inventorySet.has(selectedPath) && !generatedSet.has(selectedPath)) { + throw new TypeError(`unexpected coverage path outside inventory: ${selectedPath}`); + } + } + const inventoryMetrics = inventory.flatMap((file) => { + const metrics = selected.get(file); + return metrics ? [metrics] : []; + }); + assertMatchingTotal(totalMetrics, aggregateCoverage([...selected.values()])); + const recomputedInventoryMetrics = aggregateCoverage(inventoryMetrics); + const failures: string[] = []; const results: Array<{ scope: string; @@ -506,30 +617,21 @@ export function evaluateRiskCoverage(input: Readonly<{ function evaluate(scope: string, actual: CoverageMetrics, minimum: Thresholds): void { for (const metric of coverageMetrics) { const threshold = minimum[metric]; - if (threshold === undefined) continue; const received = actual[metric].pct; const passed = received >= threshold; results.push({ scope, metric, threshold, received, passed }); - if (!passed) { - failures.push(`${scope}.${metric} expected >= ${threshold}, received ${received}`); - } + if (!passed) failures.push(`${scope}.${metric} expected >= ${threshold}, received ${received}`); } } - evaluate("total", totalMetrics, input.policy.summary); - const inventorySet = new Set(inventory); + + evaluate("total", recomputedInventoryMetrics, input.policy.summary); if (inventory.length < input.policy.repositoryBaseline) { failures.push( `repository module baseline expected >= ${input.policy.repositoryBaseline}, received ${inventory.length}`, ); } const uncoveredModules = inventory.filter((file) => !selected.has(file)).sort(); - const selectedModules = inventory.filter((file) => selected.has(file)); - const ignoredCoveragePaths = [...selected.keys()] - .filter((file) => !inventorySet.has(file)) - .sort(); - failures.push( - ...uncoveredModules.map((file) => `production module missing from coverage: ${file}`), - ); + failures.push(...uncoveredModules.map((file) => `production module missing from coverage: ${file}`)); for (const modulePolicy of input.policy.criticalModules) { if (!inventorySet.has(modulePolicy.path)) { failures.push(`critical module is outside production inventory: ${modulePolicy.path}`); @@ -543,36 +645,34 @@ export function evaluateRiskCoverage(input: Readonly<{ evaluate(modulePolicy.path, actual, modulePolicy.minimum); } - const owners = new Set(input.policy.criticalModules.map((entry) => entry.path)); - const waivers = new Set(input.policy.waivers.map((entry) => entry.path)); - const highRisk = new Set(input.policy.highRiskPaths); + const criticalPaths = new Set(input.policy.criticalModules.map((entry) => entry.path)); + const waiverPaths = new Set(input.policy.waivers.map((entry) => entry.path)); for (const highRiskPath of input.policy.highRiskPaths) { - if (!owners.has(highRiskPath) && !waivers.has(highRiskPath)) { - failures.push(`high-risk module has no owner or waiver: ${highRiskPath}`); - } - } - for (const changedFile of input.changedFiles ?? []) { - const normalized = exactSourcePath(changedFile, "changed file"); - if (highRisk.has(normalized) && !owners.has(normalized) && !waivers.has(normalized)) { - failures.push(`changed high-risk module has no owner or waiver: ${normalized}`); + if (!inventorySet.has(highRiskPath)) { + failures.push(`high-risk module is outside production inventory: ${highRiskPath}`); } } for (const waiver of input.policy.waivers) { - if (!inventorySet.has(waiver.path) || !highRisk.has(waiver.path)) { - failures.push(`coverage waiver is stale: ${waiver.path}`); - } - const expiry = Date.parse(waiver.expiresAt); - if (!Number.isFinite(expiry) || expiry <= (input.now ?? Date.now())) { - failures.push(`coverage waiver is expired: ${waiver.path}`); - } + if (!inventorySet.has(waiver.path)) failures.push(`coverage waiver is stale: ${waiver.path}`); } + const ownedHighRiskPaths = input.policy.highRiskPaths + .filter((modulePath) => criticalPaths.has(modulePath)) + .sort(); + const waivedHighRiskPaths = input.policy.highRiskPaths + .filter((modulePath) => waiverPaths.has(modulePath)) + .sort(); return Object.freeze({ status: failures.length === 0 ? "PASS" : "FAIL", - selectedTotal: selectedModules.length, + selectedTotal: inventory.length - uncoveredModules.length, repositoryTotal: inventory.length, + preExclusionTotal: input.inventory.preExclusionTotal, + generatedExclusionCount: generatedExclusions.length, + generatedExclusions: Object.freeze([...generatedExclusions].sort()), + ownershipScope: "ALL_POLICY_HIGH_RISK", + ownedHighRiskPaths: Object.freeze(ownedHighRiskPaths), + waivedHighRiskPaths: Object.freeze(waivedHighRiskPaths), uncoveredModules: Object.freeze(uncoveredModules), - ignoredCoveragePaths: Object.freeze(ignoredCoveragePaths), results: Object.freeze(results), failures: Object.freeze(failures), }); diff --git a/tests/fixtures/coverage/repository-omission.json b/tests/fixtures/coverage/repository-omission.json index a2a6e6a..5a1a3e5 100644 --- a/tests/fixtures/coverage/repository-omission.json +++ b/tests/fixtures/coverage/repository-omission.json @@ -1,50 +1,50 @@ { "total": { - "lines": { "pct": 100 }, - "statements": { "pct": 100 }, - "functions": { "pct": 100 }, - "branches": { "pct": 100 } + "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 } }, "src/adapters/http/bounded-body-reader.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/adapters/http/bounded-json.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/adapters/http/http-execution-v3.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/adapters/http/request-builder.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/adapters/http/retry-policy.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/adapters/query-cache/server-state-scope-runtime.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/adapters/service-worker/service-worker-lifecycle.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/adapters/storage/browser-storage-adapter.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/adapters/telemetry/best-effort-telemetry.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/application/create-application.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/application/policies/compatibility.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/application/policies/performance-budgets.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/application/policies/promotion-readiness.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } }, "src/application/use-cases/decide-chunk-recovery.ts": { - "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + "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 } } } diff --git a/tests/unit/risk-coverage-files.test.ts b/tests/unit/risk-coverage-files.test.ts new file mode 100644 index 0000000..da65fbd --- /dev/null +++ b/tests/unit/risk-coverage-files.test.ts @@ -0,0 +1,240 @@ +import { constants } from "node:fs"; +import { + mkdir, + mkdtemp, + open, + readFile, + readdir, + rename, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { + readRiskCoverageInput, + resolveRiskCoverageArtifactPath, + writeRiskCoverageArtifactAtomic, +} from "../../scripts/lib/risk-coverage-files.ts"; + +const roots: string[] = []; + +async function fixture(): Promise { + const root = await mkdtemp(path.join(tmpdir(), "risk-coverage-files-")); + roots.push(root); + await mkdir(path.join(root, "config/testing"), { recursive: true }); + await mkdir(path.join(root, "artifacts/tests/coverage"), { recursive: true }); + await writeFile(path.join(root, "config/testing/policy.json"), "{\"policy\":true}\n"); + await writeFile(path.join(root, "artifacts/tests/coverage/summary.json"), "{\"total\":{}}\n"); + return root; +} + +afterEach(async () => { + await Promise.all( + roots.splice(0).map((root) => rm(root, { recursive: true, force: true })), + ); +}); + +describe("risk coverage CLI files", () => { + it("reads only exact contained regular input files", async () => { + const repositoryRoot = await fixture(); + await expect( + readRiskCoverageInput({ + repositoryRoot, + relativePath: "config/testing/policy.json", + label: "policy", + }), + ).resolves.toMatchObject({ + relativePath: "config/testing/policy.json", + text: "{\"policy\":true}\n", + }); + await expect( + readRiskCoverageInput({ + repositoryRoot, + relativePath: path.join(repositoryRoot, "config/testing/policy.json"), + label: "policy", + }), + ).rejects.toThrow(/repository-relative POSIX/u); + await expect( + readRiskCoverageInput({ + repositoryRoot, + relativePath: "config\\testing\\policy.json", + label: "policy", + }), + ).rejects.toThrow(/repository-relative POSIX/u); + }); + + it("rejects final and ancestor input symlinks", async () => { + const repositoryRoot = await fixture(); + const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-input-outside-")); + roots.push(outside); + await writeFile(path.join(outside, "outside.json"), "{}\n"); + await symlink( + path.join(outside, "outside.json"), + path.join(repositoryRoot, "config/testing/link.json"), + ); + await symlink(outside, path.join(repositoryRoot, "linked-config"), "dir"); + + await expect( + readRiskCoverageInput({ + repositoryRoot, + relativePath: "config/testing/link.json", + label: "policy", + }), + ).rejects.toThrow(/symlink/u); + await expect( + readRiskCoverageInput({ + repositoryRoot, + relativePath: "linked-config/outside.json", + label: "policy", + }), + ).rejects.toThrow(/outside repository|symlink/u); + }); + + it("confines artifact output and rejects input overwrite or symlink ancestors", async () => { + const repositoryRoot = await fixture(); + await expect( + resolveRiskCoverageArtifactPath({ + repositoryRoot, + relativePath: "artifacts/quality/risk-coverage.json", + inputPaths: ["config/testing/policy.json", "artifacts/tests/coverage/summary.json"], + }), + ).resolves.toBe(path.join(repositoryRoot, "artifacts/quality/risk-coverage.json")); + await expect( + resolveRiskCoverageArtifactPath({ + repositoryRoot, + relativePath: "config/testing/result.json", + inputPaths: [], + }), + ).rejects.toThrow(/artifacts\/quality/u); + await expect( + resolveRiskCoverageArtifactPath({ + repositoryRoot, + relativePath: "artifacts/quality/risk-coverage.json", + inputPaths: ["artifacts/quality/risk-coverage.json"], + }), + ).rejects.toThrow(/must not overwrite an input/u); + + const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-output-outside-")); + roots.push(outside); + await rm(path.join(repositoryRoot, "artifacts/quality"), { recursive: true, force: true }); + await symlink(outside, path.join(repositoryRoot, "artifacts/quality"), "dir"); + await expect( + resolveRiskCoverageArtifactPath({ + repositoryRoot, + relativePath: "artifacts/quality/risk-coverage.json", + inputPaths: [], + }), + ).rejects.toThrow(/symlink/u); + }); + + it("syncs an exclusive sibling temp before atomic rename", async () => { + const repositoryRoot = await fixture(); + const observed: string[] = []; + let observedFlags = 0; + await writeRiskCoverageArtifactAtomic( + { + repositoryRoot, + relativePath: "artifacts/quality/risk-coverage.json", + inputPaths: ["config/testing/policy.json", "artifacts/tests/coverage/summary.json"], + value: { schemaVersion: 2, status: "PASS" }, + }, + { + createNonce: () => "owned", + fileSystem: { + openFile: async (target, flags, mode) => { + observedFlags = flags; + const handle = await open(target, flags, mode); + return { + writeFile: async (data) => handle.writeFile(data, "utf8"), + sync: async () => { + observed.push("file-sync"); + await handle.sync(); + }, + close: async () => handle.close(), + }; + }, + openDirectory: async (target) => { + const handle = await open(target, constants.O_RDONLY); + return { + sync: async () => { + observed.push("directory-sync"); + await handle.sync(); + }, + close: async () => handle.close(), + }; + }, + rename: async (source, destination) => { + observed.push("rename"); + await rename(source, destination); + }, + rm, + }, + }, + ); + + expect(observedFlags & constants.O_EXCL).toBe(constants.O_EXCL); + expect(observedFlags & constants.O_NOFOLLOW).toBe(constants.O_NOFOLLOW); + expect(observed).toEqual(["file-sync", "rename", "directory-sync"]); + expect( + JSON.parse( + await readFile( + path.join(repositoryRoot, "artifacts/quality/risk-coverage.json"), + "utf8", + ), + ), + ).toEqual({ schemaVersion: 2, status: "PASS" }); + }); + + it("cleans its owned temp and preserves destination when publication fails", async () => { + const repositoryRoot = await fixture(); + const outputDirectory = path.join(repositoryRoot, "artifacts/quality"); + await mkdir(outputDirectory, { recursive: true }); + const destination = path.join(outputDirectory, "risk-coverage.json"); + await writeFile(destination, "previous\n"); + + await expect( + writeRiskCoverageArtifactAtomic( + { + repositoryRoot, + relativePath: "artifacts/quality/risk-coverage.json", + inputPaths: [], + value: { schemaVersion: 2 }, + }, + { + createNonce: () => "owned", + fileSystem: { + openFile: async (target, flags, mode) => { + const handle = await open(target, flags, mode); + return { + writeFile: async (data) => handle.writeFile(data, "utf8"), + sync: async () => handle.sync(), + close: async () => handle.close(), + }; + }, + openDirectory: async (target) => { + const handle = await open(target, constants.O_RDONLY); + return { sync: async () => handle.sync(), close: async () => handle.close() }; + }, + rename: async () => { + throw new Error("injected rename failure"); + }, + rm, + }, + }, + ), + ).rejects.toThrow(/injected rename failure/u); + + await expect(readFile(destination, "utf8")).resolves.toBe("previous\n"); + expect(await readdir(outputDirectory)).toEqual(["risk-coverage.json"]); + }); + + it("has no changed-files gate in the executable", async () => { + const source = await readFile("scripts/check-risk-coverage.ts", "utf8"); + expect(source).not.toMatch(/changedFiles|changed-files/u); + }); +}); diff --git a/tests/unit/risk-coverage.test.ts b/tests/unit/risk-coverage.test.ts index 003f2fb..fdfa10e 100644 --- a/tests/unit/risk-coverage.test.ts +++ b/tests/unit/risk-coverage.test.ts @@ -1,6 +1,8 @@ +import { constants } from "node:fs"; import { mkdir, mkdtemp, + open, readFile, rm, symlink, @@ -14,18 +16,45 @@ import { afterEach, describe, expect, it } from "vitest"; import { buildProductionModuleInventory, evaluateRiskCoverage, + normalizeCoverageProducerPath, parseRepositoryRiskCoveragePolicy, parseRiskCoveragePolicy, + type ProductionModuleInventory, } from "../../scripts/lib/risk-coverage.ts"; const roots: string[] = []; const now = Date.parse("2026-08-02T00:00:00.000Z"); -const fullMetrics = { - lines: { pct: 100 }, - statements: { pct: 100 }, - functions: { pct: 100 }, - branches: { pct: 100 }, -}; + +function counter(total = 1, covered = total, skipped = 0) { + return { + total, + covered, + skipped, + pct: total === 0 ? 100 : Math.floor((covered / total) * 10_000) / 100, + }; +} + +function metrics(total = 1, covered = total) { + return { + lines: counter(total, covered), + statements: counter(total, covered), + functions: counter(total, covered), + branches: counter(total, covered), + }; +} + +const fullMetrics = metrics(); + +function inventory( + files: readonly string[], + generatedExclusions: readonly string[] = [], +): ProductionModuleInventory { + return { + files, + preExclusionTotal: files.length + generatedExclusions.length, + generatedExclusions, + }; +} async function repositoryFixture(): Promise { const root = await mkdtemp(path.join(tmpdir(), "risk-coverage-")); @@ -87,17 +116,15 @@ describe("repository-aware risk coverage", () => { await readFile("tests/fixtures/coverage/repository-omission.json", "utf8"), ) as unknown; const parsedPolicy = parseRepositoryRiskCoveragePolicy(rawPolicy, { now }); - const inventory = await buildProductionModuleInventory({ + const productionInventory = await buildProductionModuleInventory({ repositoryRoot, generatedPaths: parsedPolicy.generatedPaths, }); const result = evaluateRiskCoverage({ repositoryRoot, - inventory, + inventory: productionInventory, policy: parsedPolicy, summary, - changedFiles: [], - now, }); expect(result.selectedTotal).toBe(14); @@ -108,342 +135,407 @@ describe("repository-aware risk coverage", () => { expect(result.status).toBe("FAIL"); }); - it("reports the exact selected and repository totals plus sorted omissions", async () => { + it("reports exact inventory and generated-exclusion provenance", async () => { const repositoryRoot = await repositoryFixture(); - const inventory = await buildProductionModuleInventory({ + const productionInventory = await buildProductionModuleInventory({ repositoryRoot, generatedPaths: ["src/generated.ts"], }); const result = evaluateRiskCoverage({ repositoryRoot, - inventory, + inventory: productionInventory, policy: parseRiskCoveragePolicy(policy(), { now }), summary: { total: fullMetrics, [path.join(repositoryRoot, "src/a.ts")]: fullMetrics, }, - changedFiles: [], - now, }); - expect(inventory).toEqual(["src/a.ts", "src/nested/b.tsx"]); + expect(productionInventory).toEqual({ + files: ["src/a.ts", "src/nested/b.tsx"], + preExclusionTotal: 3, + generatedExclusions: ["src/generated.ts"], + }); expect(result).toMatchObject({ status: "FAIL", selectedTotal: 1, repositoryTotal: 2, + preExclusionTotal: 3, + generatedExclusionCount: 1, + generatedExclusions: ["src/generated.ts"], uncoveredModules: ["src/nested/b.tsx"], }); - expect(result.failures).toContain( - "production module missing from coverage: src/nested/b.tsx", + }); + + it("rejects arbitrary coverage paths instead of silently allowing inflation", () => { + const parsedPolicy = parseRiskCoveragePolicy( + policy({ repositoryBaseline: 1, generatedPaths: [] }), + { now }, ); - }); - - it("maps only exact repository-relative POSIX coverage paths", async () => { - const repositoryRoot = await repositoryFixture(); - const inventory = await buildProductionModuleInventory({ - repositoryRoot, - generatedPaths: ["src/generated.ts"], - }); - const parsedPolicy = parseRiskCoveragePolicy(policy(), { now }); - - expect( + expect(() => evaluateRiskCoverage({ - repositoryRoot, - inventory, + repositoryRoot: "/repository", + inventory: inventory(["src/a.ts"]), policy: parsedPolicy, summary: { - total: fullMetrics, + total: metrics(2), "src/a.ts": fullMetrics, - "src/nested/b.tsx": fullMetrics, "tests/unit/a.test.ts": fullMetrics, - "src/generated.ts": fullMetrics, }, - changedFiles: [], - now, }), - ).toMatchObject({ - status: "PASS", - selectedTotal: 2, - ignoredCoveragePaths: ["src/generated.ts", "tests/unit/a.test.ts"], - }); - expect(() => - evaluateRiskCoverage({ - repositoryRoot, - inventory, - policy: parsedPolicy, - summary: { - total: fullMetrics, - [path.join(tmpdir(), "other/src/a.ts")]: fullMetrics, - }, - changedFiles: [], - now, - }), - ).toThrow(/outside repository/u); - expect(() => - evaluateRiskCoverage({ - repositoryRoot, - inventory, - policy: parsedPolicy, - summary: { - total: fullMetrics, - "src/a.ts": fullMetrics, - [path.join(repositoryRoot, "src/a.ts")]: fullMetrics, - }, - changedFiles: [], - now, - }), - ).toThrow(/duplicate coverage path/u); + ).toThrow(/unexpected coverage path.*tests\/unit\/a\.test\.ts/u); }); - it("accepts and validates Vitest's branchesTrue total without evaluating it", () => { - const parsedPolicy = parseRiskCoveragePolicy(policy(), { now }); - const branchesTrue = { total: 0, covered: 0, skipped: 0, pct: 100 }; + it("accepts only explicitly configured generated coverage paths", () => { + const parsedPolicy = parseRiskCoveragePolicy( + policy({ repositoryBaseline: 1 }), + { now }, + ); + expect(() => + evaluateRiskCoverage({ + repositoryRoot: "/repository", + inventory: inventory(["src/a.ts"], ["src/generated.ts"]), + policy: parsedPolicy, + summary: { + total: metrics(2, 1), + "src/a.ts": fullMetrics, + "src/generated.ts": metrics(1, 0), + }, + }), + ).not.toThrow(); + const result = evaluateRiskCoverage({ + repositoryRoot: "/repository", + inventory: inventory(["src/a.ts"], ["src/generated.ts"]), + policy: parsedPolicy, + summary: { + total: metrics(2, 1), + "src/a.ts": fullMetrics, + "src/generated.ts": metrics(1, 0), + }, + }); + expect( + result.results.find( + ({ scope, metric }) => scope === "total" && metric === "lines", + ), + ).toMatchObject({ received: 100, passed: true }); + }); + it("requires complete internally consistent Istanbul counters", () => { + const parsedPolicy = parseRiskCoveragePolicy( + policy({ repositoryBaseline: 1, generatedPaths: [] }), + { now }, + ); + const base = { + repositoryRoot: "/repository", + inventory: inventory(["src/a.ts"]), + policy: parsedPolicy, + }; + + expect(() => + evaluateRiskCoverage({ + ...base, + summary: { total: fullMetrics, "src/a.ts": { ...fullMetrics, lines: { pct: 100 } } }, + }), + ).toThrow(/lines.*total.*covered.*skipped/u); + expect(() => + evaluateRiskCoverage({ + ...base, + summary: { + total: fullMetrics, + "src/a.ts": { + ...fullMetrics, + lines: { total: 2, covered: 1, skipped: 2, pct: 50 }, + }, + }, + }), + ).toThrow(/covered plus skipped.*total/u); + expect(() => + evaluateRiskCoverage({ + ...base, + summary: { + total: fullMetrics, + "src/a.ts": { + ...fullMetrics, + lines: { total: 3, covered: 2, skipped: 0, pct: 66.67 }, + }, + }, + }), + ).toThrow(/pct.*66\.66/u); + expect(() => + evaluateRiskCoverage({ + ...base, + summary: { + total: metrics(0), + "src/a.ts": { ...fullMetrics, lines: counter(0, 0, 0) }, + }, + }), + ).toThrow(/coverage total.*does not match/u); + }); + + it("recomputes all global counters and requires an exact producer total", () => { + const parsedPolicy = parseRiskCoveragePolicy( + policy({ repositoryBaseline: 1, generatedPaths: [] }), + { now }, + ); + expect(() => + evaluateRiskCoverage({ + repositoryRoot: "/repository", + inventory: inventory(["src/a.ts"]), + policy: parsedPolicy, + summary: { + total: metrics(2), + "src/a.ts": fullMetrics, + }, + }), + ).toThrow(/coverage total\.lines does not match recomputed inventory total/u); + }); + + it("accepts and validates Vitest branchesTrue without evaluating it", () => { + const parsedPolicy = parseRiskCoveragePolicy( + policy({ repositoryBaseline: 1, generatedPaths: [] }), + { now }, + ); + const branchesTrue = counter(0); expect( evaluateRiskCoverage({ repositoryRoot: "/repository", - inventory: ["src/a.ts"], - policy: { ...parsedPolicy, repositoryBaseline: 1 }, + inventory: inventory(["src/a.ts"]), + policy: parsedPolicy, summary: { total: { ...fullMetrics, branchesTrue }, "src/a.ts": fullMetrics, }, - changedFiles: [], - now, }), - ).toMatchObject({ status: "PASS", selectedTotal: 1 }); + ).toMatchObject({ status: "PASS" }); expect(() => evaluateRiskCoverage({ repositoryRoot: "/repository", - inventory: ["src/a.ts"], - policy: { ...parsedPolicy, repositoryBaseline: 1 }, + inventory: inventory(["src/a.ts"]), + policy: parsedPolicy, summary: { total: { ...fullMetrics, - branchesTrue: { ...branchesTrue, pct: Number.NaN }, + branchesTrue: { ...branchesTrue, pct: 0 }, }, "src/a.ts": fullMetrics, }, - changedFiles: [], - now, }), - ).toThrow(/branchesTrue.*finite/u); + ).toThrow(/branchesTrue\.pct.*100/u); }); - it("fails closed on an empty, unreadable, traversing, or symlinked inventory", async () => { + it("opens regular files with O_NOFOLLOW and fails a deterministic symlink swap", async () => { + const repositoryRoot = await repositoryFixture(); + let observedFlags = 0; + await expect( + buildProductionModuleInventory({ + repositoryRoot, + openFile: async (target, flags) => { + observedFlags = flags; + if (target.endsWith("src/a.ts")) { + throw Object.assign(new Error("injected link swap"), { code: "ELOOP" }); + } + return open(target, flags); + }, + }), + ).rejects.toThrow(/unreadable.*src\/a\.ts/u); + expect(observedFlags & constants.O_NOFOLLOW).toBe(constants.O_NOFOLLOW); + }); + + it("fails closed on empty, traversing, symlinked, or stale generated inventory", async () => { const repositoryRoot = await repositoryFixture(); const emptyRoot = await mkdtemp(path.join(tmpdir(), "risk-coverage-empty-")); roots.push(emptyRoot); await mkdir(path.join(emptyRoot, "src")); + await expect(buildProductionModuleInventory({ repositoryRoot: emptyRoot })).rejects.toThrow( + /inventory is empty/u, + ); await expect( - buildProductionModuleInventory({ repositoryRoot: emptyRoot }), - ).rejects.toThrow(/inventory is empty/u); - await expect( - buildProductionModuleInventory({ - repositoryRoot, - assertReadable: async (target) => { - if (target.endsWith("src/a.ts")) throw new Error("denied"); - }, - }), - ).rejects.toThrow(/unreadable.*src\/a\.ts/u); - await expect( - buildProductionModuleInventory({ - repositoryRoot, - generatedPaths: ["../escape.ts"], - }), + buildProductionModuleInventory({ repositoryRoot, generatedPaths: ["../escape.ts"] }), ).rejects.toThrow(/repository-relative POSIX/u); + await expect( + buildProductionModuleInventory({ repositoryRoot, generatedPaths: ["src/missing.ts"] }), + ).rejects.toThrow(/stale or not a production module/u); const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-outside-")); roots.push(outside); await writeFile(path.join(outside, "linked.ts"), "export {};\n"); await symlink(path.join(outside, "linked.ts"), path.join(repositoryRoot, "src/link.ts")); - await expect( - buildProductionModuleInventory({ repositoryRoot }), - ).rejects.toThrow(/symlink.*src\/link\.ts/u); + await expect(buildProductionModuleInventory({ repositoryRoot })).rejects.toThrow( + /symlink.*src\/link\.ts/u, + ); }); - it("rejects malformed totals, duplicate paths, invalid minimums, and weak ownership", () => { - expect(() => parseRiskCoveragePolicy(null, { now })).toThrow(/policy/u); + it("normalizes native Windows absolute producer paths but rejects POSIX backslashes", () => { + expect( + normalizeCoverageProducerPath({ + repositoryRoot: "C:\\repo", + rawPath: "C:\\repo\\src\\nested\\a.ts", + platform: "win32", + }), + ).toBe("src/nested/a.ts"); expect(() => - parseRiskCoveragePolicy(policy({ repositoryBaseline: 0 }), { now }), - ).toThrow(/repositoryBaseline/u); + normalizeCoverageProducerPath({ + repositoryRoot: "/repository", + rawPath: "/repository/src\\a.ts", + platform: "posix", + }), + ).toThrow(/POSIX separators/u); + expect(() => + normalizeCoverageProducerPath({ + repositoryRoot: "C:\\repo", + rawPath: "D:\\outside\\a.ts", + platform: "win32", + }), + ).toThrow(/outside repository/u); + }); + + it("requires four positive metrics and canonical team ownership", () => { + expect(() => + parseRiskCoveragePolicy(policy({ summary: { lines: 80 } }), { now }), + ).toThrow(/summary must define all/u); + expect(() => + parseRiskCoveragePolicy(policy({ summary: { lines: 80, statements: 78, functions: 85, branches: 0 } }), { now }), + ).toThrow(/greater than 0/u); expect(() => parseRiskCoveragePolicy( policy({ criticalModules: [ - { path: "src/a.ts", owner: "team", minimum: { lines: 101 } }, + { path: "src/a.ts", owner: "Platform Runtime", minimum: policy().summary }, ], }), { now }, ), - ).toThrow(/minimum/u); + ).toThrow(/canonical team id/u); expect(() => parseRiskCoveragePolicy( policy({ criticalModules: [ - { path: "src/a.ts", owner: " ", minimum: { lines: 80 } }, + { path: "src/a.ts", owner: "platform-runtime", minimum: { lines: 80 } }, ], }), { now }, ), - ).toThrow(/owner/u); - expect(() => - parseRiskCoveragePolicy( - policy({ highRiskPaths: ["src/a.ts", "src/a.ts"] }), - { now }, - ), - ).toThrow(/duplicate/u); - - const parsedPolicy = parseRiskCoveragePolicy(policy(), { now }); - expect(() => - evaluateRiskCoverage({ - repositoryRoot: "/repository", - inventory: ["src/a.ts"], - policy: parsedPolicy, - summary: { - total: fullMetrics, - "src/a.ts": { ...fullMetrics, lines: { pct: Number.NaN } }, - }, - changedFiles: [], - now, - }), - ).toThrow(/finite/u); - expect(() => - evaluateRiskCoverage({ - repositoryRoot: "/repository", - inventory: ["src/a.ts"], - policy: parsedPolicy, - summary: { - total: fullMetrics, - "src/a.ts": { - ...fullMetrics, - lines: { - total: Number.NaN, - covered: 1, - skipped: 0, - pct: 100, - }, - }, - }, - changedFiles: [], - now, - }), - ).toThrow(/lines\.total.*nonnegative/u); - expect(() => - evaluateRiskCoverage({ - repositoryRoot: "/repository", - inventory: ["src/a.ts"], - policy: parsedPolicy, - summary: { - total: fullMetrics, - "src/a.ts": { ...fullMetrics, conditions: { pct: 100 } }, - }, - changedFiles: [], - now, - }), - ).toThrow(/unknown.*metric/u); + ).toThrow(/minimum must define all/u); }); - it("requires inventory-owned critical rows and exact owned future waivers", async () => { - const repositoryRoot = await repositoryFixture(); - const inventory = await buildProductionModuleInventory({ - repositoryRoot, - generatedPaths: ["src/generated.ts"], - }); - const summary = { - total: fullMetrics, - "src/a.ts": fullMetrics, - "src/nested/b.tsx": fullMetrics, - }; - const waiverPolicy = parseRiskCoveragePolicy( + it("enforces ALL_POLICY_HIGH_RISK ownership without changed-file input", () => { + expect(() => + parseRiskCoveragePolicy( + policy({ highRiskPaths: ["src/a.ts", "src/nested/b.tsx"] }), + { now }, + ), + ).toThrow(/high-risk module has no owner or waiver.*nested\/b/u); + + const parsed = parseRiskCoveragePolicy( policy({ highRiskPaths: ["src/a.ts", "src/nested/b.tsx"], waivers: [ { path: "src/nested/b.tsx", owner: "runtime-security", - reason: "Temporary branch instrumentation gap", + reason: "Temporary instrumentation gap", expiresAt: "2026-08-03T00:00:00.000Z", }, ], }), { now }, ); - - expect( - evaluateRiskCoverage({ - repositoryRoot, - inventory, - policy: waiverPolicy, - summary, - changedFiles: ["src/nested/b.tsx"], - now, - }), - ).toMatchObject({ status: "PASS" }); - expect( - evaluateRiskCoverage({ - repositoryRoot, - inventory, - policy: parseRiskCoveragePolicy( - policy({ highRiskPaths: ["src/a.ts", "src/nested/b.tsx"] }), - { now }, - ), - summary, - changedFiles: ["src/nested/b.tsx"], - now, - }).failures, - ).toContain("changed high-risk module has no owner or waiver: src/nested/b.tsx"); - expect( - evaluateRiskCoverage({ - repositoryRoot, - inventory, - policy: parseRiskCoveragePolicy( - policy({ highRiskPaths: ["src/a.ts", "src/nested/b.tsx"] }), - { now }, - ), - summary, - changedFiles: [], - now, - }).failures, - ).toContain("high-risk module has no owner or waiver: src/nested/b.tsx"); - expect( - evaluateRiskCoverage({ - repositoryRoot, - inventory: ["src/nested/b.tsx"], - policy: parseRiskCoveragePolicy(policy(), { now }), - summary, - changedFiles: [], - now, - }).failures, - ).toContain("critical module is outside production inventory: src/a.ts"); - }); - - it("does not let the repository policy delete a required high-risk path", async () => { - const rawPolicy = JSON.parse( - await readFile("config/testing/risk-coverage.json", "utf8"), - ) as Record; - rawPolicy.highRiskPaths = ( - rawPolicy.highRiskPaths as string[] - ).filter((modulePath) => modulePath !== "src/adapters/http/http-execution-v3.ts"); - - expect(() => parseRepositoryRiskCoveragePolicy(rawPolicy, { now })).toThrow( - /required high-risk path.*http-execution-v3/u, - ); + const result = evaluateRiskCoverage({ + repositoryRoot: "/repository", + inventory: inventory( + ["src/a.ts", "src/nested/b.tsx"], + ["src/generated.ts"], + ), + policy: parsed, + summary: { + total: metrics(2), + "src/a.ts": fullMetrics, + "src/nested/b.tsx": fullMetrics, + }, + }); + expect(result).toMatchObject({ + ownershipScope: "ALL_POLICY_HIGH_RISK", + ownedHighRiskPaths: ["src/a.ts"], + waivedHighRiskPaths: ["src/nested/b.tsx"], + status: "PASS", + }); }); it.each([ - [{ path: "src/*.ts", owner: "team", reason: "reason", expiresAt: "2026-08-03T00:00:00.000Z" }, /POSIX/u], - [{ path: "src/a.ts", owner: "", reason: "reason", expiresAt: "2026-08-03T00:00:00.000Z" }, /owner/u], - [{ path: "src/a.ts", owner: "team", reason: "", expiresAt: "2026-08-03T00:00:00.000Z" }, /reason/u], - [{ path: "src/a.ts", owner: "team", reason: "reason", expiresAt: "2026-08-01T00:00:00.000Z" }, /expired/u], - [{ path: "src/stale.ts", owner: "team", reason: "reason", expiresAt: "2026-08-03T00:00:00.000Z" }, /stale/u], - ] as const)("rejects invalid exact-path waivers %#", (waiver, message) => { + [ + { path: "src/a.ts", owner: "Runtime Team", reason: "Temporary instrumentation gap", expiresAt: "2026-08-03T00:00:00.000Z" }, + /canonical team id/u, + ], + [ + { path: "src/a.ts", owner: "runtime-team", reason: "too short", expiresAt: "2026-08-03T00:00:00.000Z" }, + /12 to 240/u, + ], + [ + { path: "src/a.ts", owner: "runtime-team", reason: "Temporary\ninstrumentation gap", expiresAt: "2026-08-03T00:00:00.000Z" }, + /control characters/u, + ], + [ + { path: "src/a.ts", owner: "runtime-team", reason: "Temporary instrumentation gap", expiresAt: "2026-08-03T00:00:00Z" }, + /canonical UTC ISO/u, + ], + [ + { path: "src/a.ts", owner: "runtime-team", reason: "Temporary instrumentation gap", expiresAt: "2026-11-01T00:00:00.000Z" }, + /90 days/u, + ], + ] as const)("rejects invalid waiver controls %#", (waiver, message) => { expect(() => parseRiskCoveragePolicy( - policy({ highRiskPaths: ["src/a.ts"], waivers: [waiver] }), + policy({ + criticalModules: [ + { + path: "src/owned.ts", + owner: "platform-runtime", + minimum: policy().summary, + }, + ], + highRiskPaths: ["src/owned.ts", "src/a.ts"], + waivers: [waiver], + }), { now }, ), ).toThrow(message); }); + + it("rejects simultaneous critical ownership and waiver", () => { + expect(() => + parseRiskCoveragePolicy( + policy({ + waivers: [ + { + path: "src/a.ts", + owner: "runtime-team", + reason: "Temporary instrumentation gap", + expiresAt: "2026-08-03T00:00:00.000Z", + }, + ], + }), + { now }, + ), + ).toThrow(/both a critical owner and waiver/u); + }); + + it("does not let repository policy omit or generate-exclude a required high-risk path", async () => { + const rawPolicy = JSON.parse( + await readFile("config/testing/risk-coverage.json", "utf8"), + ) as Record; + const missingPolicy = structuredClone(rawPolicy); + missingPolicy.highRiskPaths = (missingPolicy.highRiskPaths as string[]).filter( + (modulePath) => modulePath !== "src/adapters/http/http-execution-v3.ts", + ); + expect(() => parseRepositoryRiskCoveragePolicy(missingPolicy, { now })).toThrow( + /required high-risk path.*http-execution-v3/u, + ); + + const excludedPolicy = structuredClone(rawPolicy); + excludedPolicy.generatedPaths = ["src/adapters/http/http-execution-v3.ts"]; + expect(() => parseRepositoryRiskCoveragePolicy(excludedPolicy, { now })).toThrow( + /required high-risk path cannot be generated-excluded/u, + ); + }); });