From 5a73f7a1b5f1643982f34ec81d9f40ac97b9d07a Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Sun, 2 Aug 2026 07:52:46 +0900 Subject: [PATCH] fix: measure repository-wide risk coverage --- config/testing/risk-coverage.json | 171 +++--- package.json | 2 +- scripts/check-risk-coverage.ts | 140 ++--- scripts/lib/risk-coverage.ts | 579 ++++++++++++++++++ scripts/test-sample-removal.ts | 77 ++- .../reference-runtime-composition.test.ts | 54 ++ tests/fixtures/coverage/below-threshold.json | 6 + .../coverage/repository-omission.json | 50 ++ tests/unit/risk-coverage.test.ts | 449 ++++++++++++++ tests/unit/runtime-adapters.test.ts | 62 +- vitest.config.ts | 13 +- 11 files changed, 1361 insertions(+), 242 deletions(-) create mode 100644 scripts/lib/risk-coverage.ts create mode 100644 tests/fixtures/coverage/repository-omission.json create mode 100644 tests/unit/risk-coverage.test.ts diff --git a/config/testing/risk-coverage.json b/config/testing/risk-coverage.json index 2346fb5..2828236 100644 --- a/config/testing/risk-coverage.json +++ b/config/testing/risk-coverage.json @@ -1,119 +1,130 @@ { - "schemaVersion": 1, + "schemaVersion": 2, + "repositoryBaseline": 285, + "generatedPaths": [], "summary": { - "lines": 80, - "statements": 78, - "functions": 85, + "lines": 75, + "statements": 73, + "functions": 80, "branches": 68 }, "criticalModules": [ + { + "path": "src/adapters/http/bounded-body-reader.ts", + "owner": "http-runtime", + "minimum": { "lines": 68, "statements": 66, "functions": 55, "branches": 55 } + }, + { + "path": "src/adapters/http/bounded-json.ts", + "owner": "http-runtime", + "minimum": { "lines": 85, "statements": 84, "functions": 95, "branches": 78 } + }, + { + "path": "src/adapters/http/http-execution-v3.ts", + "owner": "http-runtime", + "minimum": { "lines": 75, "statements": 73, "functions": 70, "branches": 52 } + }, + { + "path": "src/adapters/http/request-builder.ts", + "owner": "http-runtime", + "minimum": { "lines": 85, "statements": 85, "functions": 95, "branches": 82 } + }, { "path": "src/adapters/http/retry-policy.ts", - "minimum": { - "lines": 80, - "statements": 78, - "functions": 95, - "branches": 78 - } + "owner": "http-runtime", + "minimum": { "lines": 80, "statements": 78, "functions": 95, "branches": 78 } + }, + { + "path": "src/adapters/query-cache/server-state-scope-runtime.ts", + "owner": "server-state-runtime", + "minimum": { "lines": 85, "statements": 85, "functions": 85, "branches": 75 } + }, + { + "path": "src/adapters/service-worker/service-worker-lifecycle.ts", + "owner": "service-worker-runtime", + "minimum": { "lines": 64, "statements": 60, "functions": 65, "branches": 43 } }, { "path": "src/adapters/storage/browser-storage-adapter.ts", - "minimum": { - "lines": 60, - "statements": 60, - "functions": 70, - "branches": 60 - } + "owner": "storage-runtime", + "minimum": { "lines": 60, "statements": 60, "functions": 70, "branches": 60 } }, { "path": "src/adapters/telemetry/best-effort-telemetry.ts", - "minimum": { - "lines": 85, - "statements": 85, - "functions": 70, - "branches": 75 - } + "owner": "telemetry-runtime", + "minimum": { "lines": 85, "statements": 85, "functions": 70, "branches": 75 } }, { "path": "src/application/create-application.ts", - "minimum": { - "lines": 90, - "statements": 90, - "functions": 80, - "branches": 68 - } + "owner": "application-runtime", + "minimum": { "lines": 90, "statements": 90, "functions": 80, "branches": 68 } }, { "path": "src/application/policies/compatibility.ts", - "minimum": { - "lines": 95, - "statements": 95, - "functions": 95, - "branches": 75 - } + "owner": "application-policy", + "minimum": { "lines": 95, "statements": 95, "functions": 95, "branches": 75 } }, { "path": "src/application/policies/performance-budgets.ts", - "minimum": { - "lines": 80, - "statements": 80, - "functions": 80, - "branches": 40 - } + "owner": "application-policy", + "minimum": { "lines": 80, "statements": 80, "functions": 80, "branches": 40 } }, { "path": "src/application/policies/promotion-readiness.ts", - "minimum": { - "lines": 95, - "statements": 95, - "functions": 95, - "branches": 95 - } + "owner": "release-runtime", + "minimum": { "lines": 95, "statements": 95, "functions": 95, "branches": 95 } }, { "path": "src/application/use-cases/decide-chunk-recovery.ts", - "minimum": { - "lines": 90, - "statements": 90, - "functions": 95, - "branches": 85 - } + "owner": "application-runtime", + "minimum": { "lines": 90, "statements": 90, "functions": 95, "branches": 85 } + }, + { + "path": "src/bootstrap/load-release-manifest.ts", + "owner": "release-runtime", + "minimum": { "lines": 90, "statements": 90, "functions": 90, "branches": 80 } + }, + { + "path": "src/bootstrap/read-bounded-boot-json.ts", + "owner": "bootstrap-runtime", + "minimum": { "lines": 71, "statements": 66, "functions": 48, "branches": 57 } }, { "path": "src/contracts/diagnostics.ts", - "minimum": { - "lines": 68, - "statements": 68, - "functions": 95, - "branches": 58 - } + "owner": "diagnostics-contracts", + "minimum": { "lines": 68, "statements": 68, "functions": 95, "branches": 58 } }, { "path": "src/features/reference-feature/adapters/reference-http-gateway.ts", - "minimum": { - "lines": 90, - "statements": 90, - "functions": 90, - "branches": 90 - } + "owner": "reference-feature", + "minimum": { "lines": 90, "statements": 90, "functions": 90, "branches": 90 } }, { "path": "src/presentation/adapters/query/application-query.ts", - "minimum": { - "lines": 90, - "statements": 90, - "functions": 90, - "branches": 80 - } - }, - { - "path": "scripts/lib/registry-compatibility.ts", - "minimum": { - "lines": 80, - "statements": 80, - "functions": 85, - "branches": 60 - } + "owner": "presentation-runtime", + "minimum": { "lines": 90, "statements": 90, "functions": 90, "branches": 80 } } - ] + ], + "highRiskPaths": [ + "src/adapters/http/bounded-body-reader.ts", + "src/adapters/http/bounded-json.ts", + "src/adapters/http/http-execution-v3.ts", + "src/adapters/http/request-builder.ts", + "src/adapters/http/retry-policy.ts", + "src/adapters/query-cache/server-state-scope-runtime.ts", + "src/adapters/service-worker/service-worker-lifecycle.ts", + "src/adapters/storage/browser-storage-adapter.ts", + "src/adapters/telemetry/best-effort-telemetry.ts", + "src/application/create-application.ts", + "src/application/policies/compatibility.ts", + "src/application/policies/performance-budgets.ts", + "src/application/policies/promotion-readiness.ts", + "src/application/use-cases/decide-chunk-recovery.ts", + "src/bootstrap/load-release-manifest.ts", + "src/bootstrap/read-bounded-boot-json.ts", + "src/contracts/diagnostics.ts", + "src/features/reference-feature/adapters/reference-http-gateway.ts", + "src/presentation/adapters/query/application-query.ts" + ], + "waivers": [] } diff --git a/package.json b/package.json index 44cf6c6..ae1f067 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "test:browser-file-storage-removal": "node scripts/test-browser-file-storage-runtime-removal.ts", "test:realtime-removal": "node scripts/test-realtime-runtime-removal.ts", "test:reference-feature": "vitest run tests/features/reference-feature --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/reference-feature.xml --passWithNoTests", - "test:coverage": "vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature --coverage --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts", + "test:coverage": "vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts", "check:coverage:fixture": "node scripts/check-risk-coverage.ts --summary tests/fixtures/coverage/below-threshold.json --artifact artifacts/quality/risk-coverage-fixture.json", "test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm test:recipes", "verify:lockfile": "corepack pnpm install --frozen-lockfile", diff --git a/scripts/check-risk-coverage.ts b/scripts/check-risk-coverage.ts index ce1581f..910b1fc 100644 --- a/scripts/check-risk-coverage.ts +++ b/scripts/check-risk-coverage.ts @@ -1,100 +1,90 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -type CoverageMetrics = Record; -type CoveragePolicy = Readonly<{ - summary: Record; - criticalModules: readonly Readonly<{ - path: string; - minimum: Record; - }>[]; -}>; -type CoverageSummary = Record; -type CoverageResult = Readonly<{ - scope: string; - metric: string; - threshold: number; - received: number | undefined; - passed: boolean; -}>; +import { + buildProductionModuleInventory, + evaluateRiskCoverage, + parseRepositoryRiskCoveragePolicy, +} from "./lib/risk-coverage.ts"; -function argumentValue(name: string, fallback: string): string { +function argumentValue(name: string, fallback?: string): string | undefined { const index = process.argv.indexOf(name); - return index >= 0 && process.argv[index + 1] - ? process.argv[index + 1] - : fallback; + return index >= 0 ? process.argv[index + 1] : fallback; } -const policyPath = argumentValue( +function requiredArgument(name: string, fallback: string): string { + const value = argumentValue(name, fallback); + if (!value) throw new TypeError(`${name} requires a path`); + 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 = argumentValue( +const summaryPath = requiredArgument( "--summary", "artifacts/tests/coverage/coverage-summary.json", ); -const artifactPath = argumentValue( +const artifactPath = requiredArgument( "--artifact", "artifacts/quality/risk-coverage.json", ); -const policy = JSON.parse( - await readFile(policyPath, "utf8"), -) as CoveragePolicy; -const summary = JSON.parse( - await readFile(summaryPath, "utf8"), -) as CoverageSummary; -const failures: string[] = []; -const results: CoverageResult[] = []; - -function evaluate( - scope: string, - actual: CoverageMetrics, - minimum: Record, -): void { - for (const [metric, threshold] of Object.entries(minimum)) { - const received = actual?.[metric]?.pct; - const passed = - typeof received === "number" && - Number.isFinite(received) && - received >= threshold; - results.push({ scope, metric, threshold, received, passed }); - if (!passed) { - failures.push( - `${scope}.${metric} expected >= ${threshold}, received ${String(received)}`, - ); - } - } -} - -evaluate("total", summary.total, policy.summary); -for (const modulePolicy of policy.criticalModules) { - const key = Object.keys(summary).find( - (candidate) => - candidate !== "total" && - candidate.replaceAll("\\", "/").endsWith(`/${modulePolicy.path}`), - ); - if (!key) { - failures.push(`critical module missing from coverage: ${modulePolicy.path}`); - continue; - } - evaluate(modulePolicy.path, summary[key], modulePolicy.minimum); -} - +const changedFilesPath = argumentValue("--changed-files"); +const policy = parseRepositoryRiskCoveragePolicy( + JSON.parse(await readFile(path.resolve(repositoryRoot, policyPath), "utf8")) 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, +}); const artifact = { - schemaVersion: 1, + schemaVersion: 2, policy: policyPath, summary: summaryPath, - status: failures.length === 0 ? "PASS" : "FAIL", - results, - failures, + changedFiles: changedFilesPath ?? null, + ...result, }; -await mkdir(path.dirname(artifactPath), { recursive: true }); -await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`); +const resolvedArtifactPath = path.resolve(repositoryRoot, artifactPath); +await mkdir(path.dirname(resolvedArtifactPath), { recursive: true }); +await writeFile(resolvedArtifactPath, `${JSON.stringify(artifact, null, 2)}\n`); -if (failures.length > 0) { - process.stderr.write(`Risk coverage failed:\n- ${failures.join("\n- ")}\n`); +if (result.failures.length > 0) { + process.stderr.write( + `Risk coverage failed:\n- ${result.failures.join("\n- ")}\n`, + ); process.exit(1); } process.stdout.write( - `Risk coverage: PASS (${results.length} scoped thresholds)\n`, + `Risk coverage: PASS (${result.selectedTotal}/${result.repositoryTotal} production modules, ${result.results.length} thresholds)\n`, ); diff --git a/scripts/lib/risk-coverage.ts b/scripts/lib/risk-coverage.ts new file mode 100644 index 0000000..ca103e7 --- /dev/null +++ b/scripts/lib/risk-coverage.ts @@ -0,0 +1,579 @@ +import { open, readdir, realpath, lstat } from "node:fs/promises"; +import type { Dirent, Stats } from "node:fs"; +import path from "node:path"; + +import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts"; + +const coverageMetrics = [ + "lines", + "statements", + "functions", + "branches", +] as const; + +export const REQUIRED_HIGH_RISK_PATHS = Object.freeze([ + "src/adapters/http/http-execution-v3.ts", + "src/adapters/http/request-builder.ts", + "src/adapters/http/bounded-body-reader.ts", + "src/adapters/http/bounded-json.ts", + "src/bootstrap/read-bounded-boot-json.ts", + "src/adapters/service-worker/service-worker-lifecycle.ts", + "src/adapters/query-cache/server-state-scope-runtime.ts", + "src/bootstrap/load-release-manifest.ts", +] as const); + +type CoverageMetric = (typeof coverageMetrics)[number]; +type Thresholds = Readonly>>; +type CoverageMetrics = Readonly< + Record> +>; + +export type RiskCoveragePolicy = Readonly<{ + schemaVersion: 2; + repositoryBaseline: number; + generatedPaths: readonly string[]; + summary: Thresholds; + criticalModules: readonly Readonly<{ + path: string; + owner: string; + minimum: Thresholds; + }>[]; + highRiskPaths: readonly string[]; + waivers: readonly Readonly<{ + path: string; + owner: string; + reason: string; + expiresAt: string; + }>[]; +}>; + +export type RiskCoverageResult = Readonly<{ + status: "PASS" | "FAIL"; + selectedTotal: number; + repositoryTotal: number; + uncoveredModules: readonly string[]; + ignoredCoveragePaths: readonly string[]; + results: readonly Readonly<{ + scope: string; + metric: CoverageMetric; + threshold: number; + received: number; + passed: boolean; + }>[]; + failures: readonly string[]; +}>; + +type InventoryOptions = Readonly<{ + repositoryRoot?: string; + generatedPaths?: readonly string[]; + readDirectory?: (target: string) => Promise; + lstatPath?: (target: string) => Promise; + realpathPath?: (target: string) => Promise; + assertReadable?: (target: string) => Promise; +}>; + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function assertExactKeys( + value: Record, + allowed: readonly string[], + label: string, +): void { + const unknown = Object.keys(value).filter((key) => !allowed.includes(key)); + if (unknown.length > 0) { + throw new TypeError(`${label} has unknown fields: ${unknown.sort().join(", ")}`); + } +} + +function exactSourcePath(value: unknown, label: string): string { + if ( + typeof value !== "string" || + ["*", "?", "[", "]", "{", "}"].some((character) => + value.includes(character), + ) + ) { + throw new TypeError(`${label} must be an exact repository-relative POSIX path`); + } + const normalized = normalizeRepositoryRelativePath(value, label); + if (!normalized.startsWith("src/") || !/\.tsx?$/u.test(normalized)) { + throw new TypeError(`${label} must identify a TypeScript module below src`); + } + 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"}`); + } + const paths = value.map((entry) => exactSourcePath(entry, `${label} entry`)); + if (new Set(paths).size !== paths.length) { + throw new TypeError(`${label} contains a duplicate path`); + } + 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`); + } + 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`); + } + const parsed: Partial> = {}; + for (const [metric, threshold] of Object.entries(value)) { + if ( + typeof threshold !== "number" || + !Number.isFinite(threshold) || + threshold < 0 || + threshold > 100 + ) { + throw new TypeError(`${label}.${metric} minimum must be a finite number from 0 to 100`); + } + parsed[metric as CoverageMetric] = threshold; + } + return Object.freeze(parsed); +} + +export function parseRiskCoveragePolicy( + value: unknown, + options: Readonly<{ now?: number }> = {}, +): RiskCoveragePolicy { + if (!isRecord(value)) { + throw new TypeError("risk coverage policy must be an object"); + } + assertExactKeys( + value, + [ + "schemaVersion", + "repositoryBaseline", + "generatedPaths", + "summary", + "criticalModules", + "highRiskPaths", + "waivers", + ], + "risk coverage policy", + ); + if (value.schemaVersion !== 2) { + throw new TypeError("risk coverage policy schemaVersion must be 2"); + } + if ( + typeof value.repositoryBaseline !== "number" || + !Number.isInteger(value.repositoryBaseline) || + value.repositoryBaseline <= 0 + ) { + throw new TypeError("repositoryBaseline must be a positive integer"); + } + const generatedPaths = uniquePaths(value.generatedPaths, "generatedPaths"); + const highRiskPaths = uniquePaths(value.highRiskPaths, "highRiskPaths", { + allowEmpty: false, + }); + if (!Array.isArray(value.criticalModules) || value.criticalModules.length === 0) { + throw new TypeError("criticalModules must be a non-empty array"); + } + const criticalModules = value.criticalModules.map((candidate, index) => { + if (!isRecord(candidate)) { + throw new TypeError(`criticalModules[${index}] must be an object`); + } + 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, + }), + }); + }); + 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"); + } + const currentTime = options.now ?? Date.now(); + const waivers = value.waivers.map((candidate, index) => { + 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 expiry = Date.parse(expiresAt); + if (!Number.isFinite(expiry) || expiry <= currentTime) { + throw new TypeError(`waivers[${index}] is expired or has an invalid expiry`); + } + 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`), + expiresAt, + }); + }); + if (new Set(waivers.map((entry) => entry.path)).size !== waivers.length) { + throw new TypeError("waivers contains a duplicate path"); + } + return Object.freeze({ + schemaVersion: 2, + repositoryBaseline: value.repositoryBaseline, + generatedPaths, + summary: thresholds(value.summary, "summary", { requireAll: true }), + criticalModules: Object.freeze(criticalModules), + highRiskPaths, + waivers: Object.freeze(waivers), + }); +} + +export function parseRepositoryRiskCoveragePolicy( + value: unknown, + options: Readonly<{ now?: number }> = {}, +): RiskCoveragePolicy { + const policy = parseRiskCoveragePolicy(value, options); + for (const requiredPath of REQUIRED_HIGH_RISK_PATHS) { + if (!policy.highRiskPaths.includes(requiredPath)) { + throw new TypeError(`required high-risk path is missing: ${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 ( + relative === "" || + (relative !== ".." && + !relative.startsWith(`..${path.sep}`) && + !path.isAbsolute(relative)) + ); +} + +function isProductionModule(relativePath: string): boolean { + return ( + /\.tsx?$/u.test(relativePath) && + !/\.d\.ts$/u.test(relativePath) && + !/\.stories\.tsx?$/u.test(relativePath) + ); +} + +export async function buildProductionModuleInventory( + options: InventoryOptions = {}, +): 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 lstatPath = options.lstatPath ?? lstat; + const realpathPath = options.realpathPath ?? realpath; + const assertReadable = options.assertReadable ?? defaultAssertReadable; + const generatedPaths = uniquePaths(options.generatedPaths ?? [], "generatedPaths"); + const generated = new Set(generatedPaths); + const repositoryRealpath = await realpathPath(repositoryRoot); + const sourceMetadata = await lstatPath(sourceRoot); + if (!sourceMetadata.isDirectory() || sourceMetadata.isSymbolicLink()) { + throw new TypeError("production source root is not a regular directory: src"); + } + const sourceRealpath = await realpathPath(sourceRoot); + if (!isWithin(repositoryRealpath, sourceRealpath)) { + throw new TypeError("production source root is outside repository"); + } + + const allModules: string[] = []; + async function visit(relativeDirectory: string): Promise { + const absoluteDirectory = path.join(repositoryRoot, relativeDirectory); + const entries = await readDirectory(absoluteDirectory); + for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { + const relativeTarget = normalizeRepositoryRelativePath( + `${relativeDirectory}/${entry.name}`, + "production inventory path", + ); + const absoluteTarget = path.join(repositoryRoot, relativeTarget); + const metadata = await lstatPath(absoluteTarget); + if (metadata.isSymbolicLink() || entry.isSymbolicLink()) { + throw new TypeError(`production inventory path is a symlink: ${relativeTarget}`); + } + if (metadata.isDirectory()) { + await visit(relativeTarget); + continue; + } + if (!isProductionModule(relativeTarget)) continue; + if (!metadata.isFile()) { + throw new TypeError(`production inventory path is not a regular file: ${relativeTarget}`); + } + const resolvedTarget = await realpathPath(absoluteTarget); + if (!isWithin(repositoryRealpath, resolvedTarget)) { + throw new TypeError(`production inventory path is outside repository: ${relativeTarget}`); + } + try { + await assertReadable(absoluteTarget); + } catch (error) { + throw new Error(`production inventory file is unreadable: ${relativeTarget}`, { + cause: error, + }); + } + allModules.push(relativeTarget); + } + } + await visit("src"); + for (const generatedPath of generatedPaths) { + if (!allModules.includes(generatedPath)) { + throw new TypeError(`generated path is stale or not a production module: ${generatedPath}`); + } + } + const inventory = allModules.filter((file) => !generated.has(file)).sort(); + 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); +} + +function coveragePath(repositoryRoot: string, rawPath: string): string { + if (rawPath.includes("\\") || rawPath.includes("\0")) { + 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("../")) { + throw new TypeError(`coverage path is outside repository: ${rawPath}`); + } + return normalizeRepositoryRelativePath(relative, "coverage path"); + } + return normalizeRepositoryRelativePath(rawPath, "coverage path"); +} + +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), + ); + if (unknownMetrics.length > 0) { + throw new TypeError( + `${label} has unknown coverage metric keys: ${unknownMetrics.sort().join(", ")}`, + ); + } + 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`); + } + } + 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 }; + } + return Object.freeze(parsed); +} + +export function evaluateRiskCoverage(input: Readonly<{ + repositoryRoot?: string; + inventory: readonly string[]; + 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")); + if (new Set(inventory).size !== inventory.length) { + throw new TypeError("production module inventory contains a duplicate path"); + } + if (!isRecord(input.summary) || !("total" in input.summary)) { + throw new TypeError("coverage summary must contain total metrics"); + } + const totalMetrics = parseCoverageMetrics(input.summary.total, "coverage total"); + 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}`); + } + selected.set(normalized, parseCoverageMetrics(rawMetrics, `coverage ${normalized}`)); + } + + const failures: string[] = []; + const results: Array<{ + scope: string; + metric: CoverageMetric; + threshold: number; + received: number; + passed: boolean; + }> = []; + 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}`); + } + } + } + evaluate("total", totalMetrics, input.policy.summary); + const inventorySet = new Set(inventory); + 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}`), + ); + for (const modulePolicy of input.policy.criticalModules) { + if (!inventorySet.has(modulePolicy.path)) { + failures.push(`critical module is outside production inventory: ${modulePolicy.path}`); + continue; + } + const actual = selected.get(modulePolicy.path); + if (!actual) { + failures.push(`critical module missing from coverage: ${modulePolicy.path}`); + continue; + } + 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); + 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}`); + } + } + 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}`); + } + } + + return Object.freeze({ + status: failures.length === 0 ? "PASS" : "FAIL", + selectedTotal: selectedModules.length, + repositoryTotal: inventory.length, + uncoveredModules: Object.freeze(uncoveredModules), + ignoredCoveragePaths: Object.freeze(ignoredCoveragePaths), + results: Object.freeze(results), + failures: Object.freeze(failures), + }); +} diff --git a/scripts/test-sample-removal.ts b/scripts/test-sample-removal.ts index 717925c..933b349 100644 --- a/scripts/test-sample-removal.ts +++ b/scripts/test-sample-removal.ts @@ -3,6 +3,7 @@ import { access, cp, mkdir, + mkdtemp, readFile, readdir, rm, @@ -11,7 +12,11 @@ import { } from "node:fs/promises"; import path from "node:path"; -const fixtureRoot = path.resolve(".tmp/reference-feature-removal"); +const fixtureParent = path.resolve(".tmp"); +await mkdir(fixtureParent, { recursive: true }); +const fixtureRoot = await mkdtemp( + path.join(fixtureParent, "reference-feature-removal-"), +); const pnpmCli = requireEnvironment("npm_execpath"); const featureSource = "src/features/reference-feature"; const featureTests = "tests/features/reference-feature"; @@ -130,7 +135,14 @@ const emptyMessages = `export const INSTALLED_MESSAGE_CATALOGS = Object.freeze({ `; type CoveragePolicy = { - criticalModules: Array<{ path?: string }>; + repositoryBaseline: number; + generatedPaths: string[]; + criticalModules: Array<{ + path?: string; + minimum?: Record; + }>; + highRiskPaths: string[]; + waivers: Array<{ path?: string }>; }; type EvidenceContribution = Readonly<{ owner?: string }>; @@ -170,13 +182,20 @@ function runPnpm(script: string, extra: string[] = []): boolean { return result.status === 0; } -await rm(fixtureRoot, { recursive: true, force: true }); -await mkdir(fixtureRoot, { recursive: true }); for (const target of copyTargets) { await cp(target, path.join(fixtureRoot, target), { recursive: true }); } await symlink(path.resolve("node_modules"), path.join(fixtureRoot, "node_modules"), "dir"); +const removedProductionModuleCount = ( + await filesBelow(path.join(fixtureRoot, featureSource)) +).filter( + (file) => + /\.tsx?$/u.test(file) && + !/\.d\.ts$/u.test(file) && + !/\.stories\.tsx?$/u.test(file), +).length; + for (const ownedPath of featureOwnedPaths) { await rm(path.join(fixtureRoot, ownedPath), { recursive: true, @@ -204,18 +223,6 @@ await writeFile( emptyContractContributions, ); -const vitestConfigFile = path.join(fixtureRoot, "vitest.config.ts"); -const vitestConfig = await readFile(vitestConfigFile, "utf8"); -const featureCoverageInclude = - ` "${featureSource}/adapters/reference-http-gateway.ts",\n`; -if (!vitestConfig.includes(featureCoverageInclude)) { - throw new Error("Reference feature coverage include is not registered"); -} -await writeFile( - vitestConfigFile, - vitestConfig.replace(featureCoverageInclude, ""), -); - const coveragePolicyFile = path.join( fixtureRoot, "config/testing/risk-coverage.json", @@ -232,6 +239,44 @@ if ( throw new Error("Reference feature coverage policy is not registered"); } coveragePolicy.criticalModules = retainedCriticalModules; +// The removable reference feature exercises shared request-body failure branches. +// Keep the production policy unchanged while preserving an audited floor for the +// intentionally smaller executable universe in this isolated removal proof. +const removalSpecificBoundedBodyFloor = { + lines: 65, + statements: 63, + functions: 55, + branches: 45, +}; +const boundedBodyPolicy = coveragePolicy.criticalModules.find( + (modulePolicy) => + modulePolicy.path === "src/adapters/http/bounded-body-reader.ts", +); +if (!boundedBodyPolicy?.minimum) { + throw new Error("Shared bounded-body coverage policy is not registered"); +} +for (const [metric, floor] of Object.entries(removalSpecificBoundedBodyFloor)) { + const productionFloor = boundedBodyPolicy.minimum[metric]; + if (typeof productionFloor !== "number" || productionFloor < floor) { + throw new Error( + `Production bounded-body ${metric} floor must remain at least ${floor}`, + ); + } +} +boundedBodyPolicy.minimum = removalSpecificBoundedBodyFloor; +coveragePolicy.highRiskPaths = coveragePolicy.highRiskPaths.filter( + (modulePath) => !modulePath.startsWith(`${featureSource}/`), +); +coveragePolicy.waivers = coveragePolicy.waivers.filter( + (waiver) => !waiver.path?.startsWith(`${featureSource}/`), +); +coveragePolicy.generatedPaths = coveragePolicy.generatedPaths.filter( + (modulePath) => !modulePath.startsWith(`${featureSource}/`), +); +coveragePolicy.repositoryBaseline -= removedProductionModuleCount; +if (coveragePolicy.repositoryBaseline <= 0) { + throw new Error("Reference feature removal produced an invalid coverage baseline"); +} await writeFile( coveragePolicyFile, `${JSON.stringify(coveragePolicy, null, 2)}\n`, diff --git a/tests/features/reference-feature/reference-runtime-composition.test.ts b/tests/features/reference-feature/reference-runtime-composition.test.ts index e069ed1..36e2d93 100644 --- a/tests/features/reference-feature/reference-runtime-composition.test.ts +++ b/tests/features/reference-feature/reference-runtime-composition.test.ts @@ -138,4 +138,58 @@ describe("reference feature runtime composition", () => { ); adapters.infrastructure.dispose(); }); + + it("keeps private command intent identity out of URLs and diagnostics", async () => { + const requests: Array> = []; + const fetcher = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + requests.push({ + url: String(input), + headers: new Headers(init?.headers), + }); + return Response.json( + { id: "resource-1", name: "Created resource" }, + { status: 201 }, + ); + }); + const adapters = await createRuntimeAdapters({ + runtime, + release, + host: {}, + fetcher, + }); + await adapters.outputPorts.session.beginSignIn(); + await vi.waitFor(() => + expect(adapters.infrastructure.serverStateScope.getPhase()).toBe("READY"), + ); + const intent = Object.freeze({ + intentId: "private-intent-id", + operationId: "CREATE_REFERENCE_RESOURCE", + canonicalInputIdentity: "private-canonical-input", + idempotencyKey: "private-idempotency-key", + createdAtMonotonicMs: 42, + }); + + await expect( + adapters.featureInputs[REFERENCE_FEATURE_ID].createResource( + { name: "Created resource" }, + { intent }, + ), + ).resolves.toMatchObject({ ok: true }); + + expect(requests).toHaveLength(1); + expect(requests[0]?.headers.get("Idempotency-Key")).toBe( + "private-idempotency-key", + ); + expect(requests[0]?.url).toBe( + "http://localhost:8080/api/reference-resources", + ); + const safeEvidence = JSON.stringify({ + requests: requests.map((request) => request.url), + diagnostics: adapters.outputPorts.diagnostics.entries(), + }); + expect(safeEvidence).not.toContain("private-intent-id"); + expect(safeEvidence).not.toContain("private-canonical-input"); + expect(safeEvidence).not.toContain("private-idempotency-key"); + adapters.infrastructure.dispose(); + }); }); diff --git a/tests/fixtures/coverage/below-threshold.json b/tests/fixtures/coverage/below-threshold.json index 9b766e5..1df4f7e 100644 --- a/tests/fixtures/coverage/below-threshold.json +++ b/tests/fixtures/coverage/below-threshold.json @@ -4,5 +4,11 @@ "statements": { "pct": 0 }, "functions": { "pct": 0 }, "branches": { "pct": 0 } + }, + "src/adapters/http/http-execution-v3.ts": { + "lines": { "pct": 0 }, + "statements": { "pct": 0 }, + "functions": { "pct": 0 }, + "branches": { "pct": 0 } } } diff --git a/tests/fixtures/coverage/repository-omission.json b/tests/fixtures/coverage/repository-omission.json new file mode 100644 index 0000000..a2a6e6a --- /dev/null +++ b/tests/fixtures/coverage/repository-omission.json @@ -0,0 +1,50 @@ +{ + "total": { + "lines": { "pct": 100 }, + "statements": { "pct": 100 }, + "functions": { "pct": 100 }, + "branches": { "pct": 100 } + }, + "src/adapters/http/bounded-body-reader.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/adapters/http/bounded-json.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/adapters/http/http-execution-v3.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/adapters/http/request-builder.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/adapters/http/retry-policy.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/adapters/query-cache/server-state-scope-runtime.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/adapters/service-worker/service-worker-lifecycle.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/adapters/storage/browser-storage-adapter.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/adapters/telemetry/best-effort-telemetry.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/application/create-application.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/application/policies/compatibility.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/application/policies/performance-budgets.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/application/policies/promotion-readiness.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + }, + "src/application/use-cases/decide-chunk-recovery.ts": { + "lines": { "pct": 100 }, "statements": { "pct": 100 }, "functions": { "pct": 100 }, "branches": { "pct": 100 } + } +} diff --git a/tests/unit/risk-coverage.test.ts b/tests/unit/risk-coverage.test.ts new file mode 100644 index 0000000..003f2fb --- /dev/null +++ b/tests/unit/risk-coverage.test.ts @@ -0,0 +1,449 @@ +import { + mkdir, + mkdtemp, + readFile, + 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 { + buildProductionModuleInventory, + evaluateRiskCoverage, + parseRepositoryRiskCoveragePolicy, + parseRiskCoveragePolicy, +} 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 }, +}; + +async function repositoryFixture(): Promise { + const root = await mkdtemp(path.join(tmpdir(), "risk-coverage-")); + roots.push(root); + await mkdir(path.join(root, "src/nested"), { recursive: true }); + await Promise.all([ + writeFile(path.join(root, "src/a.ts"), "export const a = 1;\n"), + writeFile(path.join(root, "src/nested/b.tsx"), "export const b = 2;\n"), + writeFile(path.join(root, "src/types.d.ts"), "declare const value: 1;\n"), + writeFile(path.join(root, "src/example.stories.tsx"), "export {};\n"), + writeFile(path.join(root, "src/generated.ts"), "export const generated = true;\n"), + writeFile(path.join(root, "src/ignored.js"), "export const ignored = true;\n"), + ]); + return root; +} + +function policy(overrides: Record = {}) { + return { + schemaVersion: 2, + repositoryBaseline: 2, + generatedPaths: ["src/generated.ts"], + summary: { + lines: 80, + statements: 78, + functions: 85, + branches: 68, + }, + criticalModules: [ + { + path: "src/a.ts", + owner: "platform-runtime", + minimum: { + lines: 80, + statements: 78, + functions: 85, + branches: 68, + }, + }, + ], + highRiskPaths: ["src/a.ts"], + waivers: [], + ...overrides, + }; +} + +afterEach(async () => { + await Promise.all( + roots.splice(0).map((root) => rm(root, { recursive: true, force: true })), + ); +}); + +describe("repository-aware risk coverage", () => { + it("rejects the historical 14-file summary against the full repository", async () => { + const repositoryRoot = process.cwd(); + const rawPolicy = JSON.parse( + await readFile("config/testing/risk-coverage.json", "utf8"), + ) as unknown; + const summary = JSON.parse( + await readFile("tests/fixtures/coverage/repository-omission.json", "utf8"), + ) as unknown; + const parsedPolicy = parseRepositoryRiskCoveragePolicy(rawPolicy, { now }); + const inventory = await buildProductionModuleInventory({ + repositoryRoot, + generatedPaths: parsedPolicy.generatedPaths, + }); + const result = evaluateRiskCoverage({ + repositoryRoot, + inventory, + policy: parsedPolicy, + summary, + changedFiles: [], + now, + }); + + expect(result.selectedTotal).toBe(14); + expect(result.repositoryTotal).toBeGreaterThan(result.selectedTotal); + expect(result.uncoveredModules).toHaveLength( + result.repositoryTotal - result.selectedTotal, + ); + expect(result.status).toBe("FAIL"); + }); + + it("reports the exact selected and repository totals plus sorted omissions", async () => { + const repositoryRoot = await repositoryFixture(); + const inventory = await buildProductionModuleInventory({ + repositoryRoot, + generatedPaths: ["src/generated.ts"], + }); + const result = evaluateRiskCoverage({ + repositoryRoot, + inventory, + 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(result).toMatchObject({ + status: "FAIL", + selectedTotal: 1, + repositoryTotal: 2, + uncoveredModules: ["src/nested/b.tsx"], + }); + expect(result.failures).toContain( + "production module missing from coverage: src/nested/b.tsx", + ); + }); + + 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( + evaluateRiskCoverage({ + repositoryRoot, + inventory, + policy: parsedPolicy, + summary: { + total: fullMetrics, + "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); + }); + + 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 }; + + expect( + evaluateRiskCoverage({ + repositoryRoot: "/repository", + inventory: ["src/a.ts"], + policy: { ...parsedPolicy, repositoryBaseline: 1 }, + summary: { + total: { ...fullMetrics, branchesTrue }, + "src/a.ts": fullMetrics, + }, + changedFiles: [], + now, + }), + ).toMatchObject({ status: "PASS", selectedTotal: 1 }); + expect(() => + evaluateRiskCoverage({ + repositoryRoot: "/repository", + inventory: ["src/a.ts"], + policy: { ...parsedPolicy, repositoryBaseline: 1 }, + summary: { + total: { + ...fullMetrics, + branchesTrue: { ...branchesTrue, pct: Number.NaN }, + }, + "src/a.ts": fullMetrics, + }, + changedFiles: [], + now, + }), + ).toThrow(/branchesTrue.*finite/u); + }); + + it("fails closed on an empty, unreadable, traversing, or symlinked 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, + 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"], + }), + ).rejects.toThrow(/repository-relative POSIX/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); + }); + + it("rejects malformed totals, duplicate paths, invalid minimums, and weak ownership", () => { + expect(() => parseRiskCoveragePolicy(null, { now })).toThrow(/policy/u); + expect(() => + parseRiskCoveragePolicy(policy({ repositoryBaseline: 0 }), { now }), + ).toThrow(/repositoryBaseline/u); + expect(() => + parseRiskCoveragePolicy( + policy({ + criticalModules: [ + { path: "src/a.ts", owner: "team", minimum: { lines: 101 } }, + ], + }), + { now }, + ), + ).toThrow(/minimum/u); + expect(() => + parseRiskCoveragePolicy( + policy({ + criticalModules: [ + { path: "src/a.ts", owner: " ", 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); + }); + + 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( + policy({ + highRiskPaths: ["src/a.ts", "src/nested/b.tsx"], + waivers: [ + { + path: "src/nested/b.tsx", + owner: "runtime-security", + reason: "Temporary branch 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, + ); + }); + + 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) => { + expect(() => + parseRiskCoveragePolicy( + policy({ highRiskPaths: ["src/a.ts"], waivers: [waiver] }), + { now }, + ), + ).toThrow(message); + }); +}); diff --git a/tests/unit/runtime-adapters.test.ts b/tests/unit/runtime-adapters.test.ts index a756bc9..5b06df1 100644 --- a/tests/unit/runtime-adapters.test.ts +++ b/tests/unit/runtime-adapters.test.ts @@ -65,14 +65,14 @@ describe("runtime adapter composition", () => { }); const intent = factory.create({ - operationId: "CREATE_REFERENCE_RESOURCE", + operationId: "CREATE_ENTITY", canonicalInputIdentity: "opaque-canonical-input", requiresIdempotencyKey: true, }); expect(intent).toEqual({ intentId: "intent-uuid", - operationId: "CREATE_REFERENCE_RESOURCE", + operationId: "CREATE_ENTITY", canonicalInputIdentity: "opaque-canonical-input", idempotencyKey: "idempotency-uuid", createdAtMonotonicMs: 12.5, @@ -96,7 +96,7 @@ describe("runtime adapter composition", () => { ).toThrow(TypeError); expect(() => factory.create({ - operationId: "CREATE_REFERENCE_RESOURCE", + operationId: "CREATE_ENTITY", canonicalInputIdentity: "x".repeat(16_385), requiresIdempotencyKey: false, }), @@ -106,7 +106,7 @@ describe("runtime adapter composition", () => { randomUUID: () => "opaque-runtime-identifier", monotonicNow: () => -1, }).create({ - operationId: "CREATE_REFERENCE_RESOURCE", + operationId: "CREATE_ENTITY", canonicalInputIdentity: "valid-identity", requiresIdempotencyKey: false, }), @@ -165,60 +165,6 @@ describe("runtime adapter composition", () => { adapters.infrastructure.dispose(); }); - it("passes the supplied command intent unchanged and keeps private identity out of URLs and diagnostics", async () => { - const requests: Array> = []; - const fetcher = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { - requests.push({ - url: String(input), - headers: new Headers(init?.headers), - }); - return Response.json( - { id: "resource-1", name: "Created resource" }, - { status: 201 }, - ); - }); - const adapters = await createRuntimeAdapters({ - runtime, - release, - host: {}, - fetcher, - }); - await adapters.outputPorts.session.beginSignIn(); - await vi.waitFor(() => - expect(adapters.infrastructure.serverStateScope.getPhase()).toBe("READY"), - ); - const intent = Object.freeze({ - intentId: "private-intent-id", - operationId: "CREATE_REFERENCE_RESOURCE", - canonicalInputIdentity: "private-canonical-input", - idempotencyKey: "private-idempotency-key", - createdAtMonotonicMs: 42, - }); - - await expect( - adapters.featureInputs["reference-feature"].createResource( - { name: "Created resource" }, - { intent }, - ), - ).resolves.toMatchObject({ ok: true }); - - expect(requests).toHaveLength(1); - expect(requests[0]?.headers.get("Idempotency-Key")).toBe( - "private-idempotency-key", - ); - expect(requests[0]?.url).toBe( - "http://localhost:8080/api/reference-resources", - ); - const safeEvidence = JSON.stringify({ - requests: requests.map((request) => request.url), - diagnostics: adapters.outputPorts.diagnostics.entries(), - }); - expect(safeEvidence).not.toContain("private-intent-id"); - expect(safeEvidence).not.toContain("private-canonical-input"); - expect(safeEvidence).not.toContain("private-idempotency-key"); - adapters.infrastructure.dispose(); - }); - it("does not fail boot when Web Storage capability getters throw", async () => { const host: Record = {}; Object.defineProperties(host, { diff --git a/vitest.config.ts b/vitest.config.ts index adb8dad..3f4e639 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,18 +13,7 @@ export default defineConfig({ provider: "v8", reportsDirectory: "artifacts/tests/coverage", reporter: ["text", "json-summary", "lcov"], - include: [ - "src/application/create-application.ts", - "src/application/policies/**/*.ts", - "src/application/use-cases/**/*.ts", - "src/contracts/diagnostics.ts", - "src/adapters/http/retry-policy.ts", - "src/adapters/storage/browser-storage-adapter.ts", - "src/adapters/telemetry/best-effort-telemetry.ts", - "src/features/reference-feature/adapters/reference-http-gateway.ts", - "src/presentation/adapters/query/application-query.ts", - "scripts/lib/registry-compatibility.ts", - ], + include: ["src/**/*.{ts,tsx}"], exclude: ["**/*.d.ts", "**/*.stories.*"], }, },