fix: close coverage evidence races
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
{
|
{
|
||||||
"path": "src/adapters/http/bounded-body-reader.ts",
|
"path": "src/adapters/http/bounded-body-reader.ts",
|
||||||
"owner": "http-runtime",
|
"owner": "http-runtime",
|
||||||
"minimum": { "lines": 68, "statements": 66, "functions": 55, "branches": 55 }
|
"minimum": { "lines": 95, "statements": 95, "functions": 95, "branches": 90 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "src/adapters/http/bounded-json.ts",
|
"path": "src/adapters/http/bounded-json.ts",
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ type WriterDependencies = Readonly<{
|
|||||||
fileSystem?: RiskCoverageArtifactFileSystem;
|
fileSystem?: RiskCoverageArtifactFileSystem;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type ReadableHandle = Readonly<{
|
||||||
|
stat(): Promise<Stats>;
|
||||||
|
readFile(encoding: "utf8"): Promise<string>;
|
||||||
|
close(): Promise<unknown>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type InputDependencies = Readonly<{
|
||||||
|
lstatPath?: (target: string) => Promise<Stats>;
|
||||||
|
realpathPath?: (target: string) => Promise<string>;
|
||||||
|
openFile?: (target: string, flags: number) => Promise<ReadableHandle>;
|
||||||
|
}>;
|
||||||
|
|
||||||
const defaultFileSystem: RiskCoverageArtifactFileSystem = Object.freeze({
|
const defaultFileSystem: RiskCoverageArtifactFileSystem = Object.freeze({
|
||||||
openFile: async (target, flags, mode) => {
|
openFile: async (target, flags, mode) => {
|
||||||
const handle = await open(target, flags, mode);
|
const handle = await open(target, flags, mode);
|
||||||
@@ -74,6 +86,45 @@ function isWithin(root: string, target: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasStableIdentity(metadata: Stats): boolean {
|
||||||
|
return (
|
||||||
|
Number.isSafeInteger(metadata.dev) &&
|
||||||
|
Number.isSafeInteger(metadata.ino) &&
|
||||||
|
(metadata.dev !== 0 || metadata.ino !== 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameFileIdentity(before: Stats, after: Stats): boolean {
|
||||||
|
return (
|
||||||
|
!hasStableIdentity(before) ||
|
||||||
|
!hasStableIdentity(after) ||
|
||||||
|
(before.dev === after.dev && before.ino === after.ino)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rejectSymlinkAncestors(
|
||||||
|
repositoryRoot: string,
|
||||||
|
relativePath: string,
|
||||||
|
label: string,
|
||||||
|
lstatPath: (target: string) => Promise<Stats>,
|
||||||
|
): Promise<void> {
|
||||||
|
let current = repositoryRoot;
|
||||||
|
let currentRelative = "";
|
||||||
|
const directory = path.posix.dirname(relativePath);
|
||||||
|
if (directory === ".") return;
|
||||||
|
for (const segment of directory.split("/")) {
|
||||||
|
current = path.join(current, segment);
|
||||||
|
currentRelative = currentRelative ? `${currentRelative}/${segment}` : segment;
|
||||||
|
const metadata = await lstatPath(current);
|
||||||
|
if (metadata.isSymbolicLink()) {
|
||||||
|
throw new TypeError(`${label} ancestor is a symlink: ${currentRelative}`);
|
||||||
|
}
|
||||||
|
if (!metadata.isDirectory()) {
|
||||||
|
throw new TypeError(`${label} ancestor is not a directory: ${currentRelative}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function assertDirectory(metadata: Stats, relativePath: string): void {
|
function assertDirectory(metadata: Stats, relativePath: string): void {
|
||||||
if (metadata.isSymbolicLink()) {
|
if (metadata.isSymbolicLink()) {
|
||||||
throw new TypeError(`artifact ancestor is a symlink: ${relativePath}`);
|
throw new TypeError(`artifact ancestor is a symlink: ${relativePath}`);
|
||||||
@@ -87,27 +138,41 @@ export async function readRiskCoverageInput(input: Readonly<{
|
|||||||
repositoryRoot: string;
|
repositoryRoot: string;
|
||||||
relativePath: string;
|
relativePath: string;
|
||||||
label: string;
|
label: string;
|
||||||
}>): Promise<Readonly<{ relativePath: string; absolutePath: string; text: string }>> {
|
}>, dependencies: InputDependencies = {}): Promise<Readonly<{
|
||||||
|
relativePath: string;
|
||||||
|
absolutePath: string;
|
||||||
|
text: string;
|
||||||
|
}>> {
|
||||||
|
const lstatPath = dependencies.lstatPath ?? lstat;
|
||||||
|
const realpathPath = dependencies.realpathPath ?? realpath;
|
||||||
|
const openFile = dependencies.openFile ??
|
||||||
|
((target: string, flags: number) => open(target, flags));
|
||||||
const relativePath = normalizeRepositoryRelativePath(
|
const relativePath = normalizeRepositoryRelativePath(
|
||||||
input.relativePath,
|
input.relativePath,
|
||||||
`${input.label} path`,
|
`${input.label} path`,
|
||||||
);
|
);
|
||||||
const repositoryRoot = path.resolve(input.repositoryRoot);
|
const repositoryRoot = path.resolve(input.repositoryRoot);
|
||||||
const repositoryRealpath = await realpath(repositoryRoot);
|
const repositoryRealpath = await realpathPath(repositoryRoot);
|
||||||
const absolutePath = path.resolve(repositoryRoot, relativePath);
|
const absolutePath = path.resolve(repositoryRoot, relativePath);
|
||||||
const metadata = await lstat(absolutePath);
|
await rejectSymlinkAncestors(
|
||||||
|
repositoryRoot,
|
||||||
|
relativePath,
|
||||||
|
input.label,
|
||||||
|
lstatPath,
|
||||||
|
);
|
||||||
|
const metadata = await lstatPath(absolutePath);
|
||||||
if (metadata.isSymbolicLink()) {
|
if (metadata.isSymbolicLink()) {
|
||||||
throw new TypeError(`${input.label} path is a symlink: ${relativePath}`);
|
throw new TypeError(`${input.label} path is a symlink: ${relativePath}`);
|
||||||
}
|
}
|
||||||
if (!metadata.isFile()) {
|
if (!metadata.isFile()) {
|
||||||
throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`);
|
throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`);
|
||||||
}
|
}
|
||||||
const resolvedPath = await realpath(absolutePath);
|
const resolvedPath = await realpathPath(absolutePath);
|
||||||
if (!isWithin(repositoryRealpath, resolvedPath)) {
|
if (!isWithin(repositoryRealpath, resolvedPath)) {
|
||||||
throw new TypeError(`${input.label} path is outside repository: ${relativePath}`);
|
throw new TypeError(`${input.label} path is outside repository: ${relativePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handle = await open(
|
const handle = await openFile(
|
||||||
absolutePath,
|
absolutePath,
|
||||||
constants.O_RDONLY | constants.O_NOFOLLOW,
|
constants.O_RDONLY | constants.O_NOFOLLOW,
|
||||||
);
|
);
|
||||||
@@ -116,6 +181,9 @@ export async function readRiskCoverageInput(input: Readonly<{
|
|||||||
if (!openedMetadata.isFile()) {
|
if (!openedMetadata.isFile()) {
|
||||||
throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`);
|
throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`);
|
||||||
}
|
}
|
||||||
|
if (!sameFileIdentity(metadata, openedMetadata)) {
|
||||||
|
throw new TypeError(`${input.label} path changed during validation: ${relativePath}`);
|
||||||
|
}
|
||||||
const text = await handle.readFile("utf8");
|
const text = await handle.readFile("utf8");
|
||||||
return Object.freeze({ relativePath, absolutePath, text });
|
return Object.freeze({ relativePath, absolutePath, text });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -170,17 +238,35 @@ export async function resolveRiskCoverageArtifactPath(input: Readonly<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const absolutePath = path.resolve(repositoryRoot, relativePath);
|
const absolutePath = path.resolve(repositoryRoot, relativePath);
|
||||||
|
let destinationMetadata: Stats | undefined;
|
||||||
try {
|
try {
|
||||||
const metadata = await lstat(absolutePath);
|
destinationMetadata = await lstat(absolutePath);
|
||||||
if (metadata.isSymbolicLink()) {
|
if (destinationMetadata.isSymbolicLink()) {
|
||||||
throw new TypeError(`artifact path is a symlink: ${relativePath}`);
|
throw new TypeError(`artifact path is a symlink: ${relativePath}`);
|
||||||
}
|
}
|
||||||
if (!metadata.isFile()) {
|
if (!destinationMetadata.isFile()) {
|
||||||
throw new TypeError(`artifact path is not a regular file: ${relativePath}`);
|
throw new TypeError(`artifact path is not a regular file: ${relativePath}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!hasErrorCode(error, "ENOENT")) throw error;
|
if (!hasErrorCode(error, "ENOENT")) throw error;
|
||||||
}
|
}
|
||||||
|
if (destinationMetadata) {
|
||||||
|
const destinationRealpath = await realpath(absolutePath);
|
||||||
|
for (const inputPath of normalizedInputs) {
|
||||||
|
const inputAbsolutePath = path.resolve(repositoryRoot, inputPath);
|
||||||
|
const inputRealpath = await realpath(inputAbsolutePath);
|
||||||
|
const inputMetadata = await lstat(inputAbsolutePath);
|
||||||
|
if (
|
||||||
|
inputRealpath === destinationRealpath ||
|
||||||
|
(hasStableIdentity(inputMetadata) &&
|
||||||
|
hasStableIdentity(destinationMetadata) &&
|
||||||
|
inputMetadata.dev === destinationMetadata.dev &&
|
||||||
|
inputMetadata.ino === destinationMetadata.ino)
|
||||||
|
) {
|
||||||
|
throw new TypeError(`artifact path is the same file as an input: ${inputPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return absolutePath;
|
return absolutePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +318,14 @@ export async function writeRiskCoverageArtifactAtomic(
|
|||||||
ownsTemporaryFile = false;
|
ownsTemporaryFile = false;
|
||||||
const directoryHandle = await fileSystem.openDirectory(path.dirname(destination));
|
const directoryHandle = await fileSystem.openDirectory(path.dirname(destination));
|
||||||
try {
|
try {
|
||||||
await directoryHandle.sync();
|
try {
|
||||||
|
await directoryHandle.sync();
|
||||||
|
} catch (error) {
|
||||||
|
// Windows and some filesystems do not support fsync on directory handles.
|
||||||
|
if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await directoryHandle.close();
|
await directoryHandle.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ type InventoryOptions = Readonly<{
|
|||||||
openFile?: (target: string, flags: number) => Promise<ReadableFileHandle>;
|
openFile?: (target: string, flags: number) => Promise<ReadableFileHandle>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
class FileIdentityChangedError extends Error {}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
@@ -327,6 +329,22 @@ function isWithin(root: string, target: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasStableIdentity(metadata: Stats): boolean {
|
||||||
|
return (
|
||||||
|
Number.isSafeInteger(metadata.dev) &&
|
||||||
|
Number.isSafeInteger(metadata.ino) &&
|
||||||
|
(metadata.dev !== 0 || metadata.ino !== 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameFileIdentity(before: Stats, after: Stats): boolean {
|
||||||
|
return (
|
||||||
|
!hasStableIdentity(before) ||
|
||||||
|
!hasStableIdentity(after) ||
|
||||||
|
(before.dev === after.dev && before.ino === after.ino)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isProductionModulePath(relativePath: string): boolean {
|
export function isProductionModulePath(relativePath: string): boolean {
|
||||||
return (
|
return (
|
||||||
/\.tsx?$/u.test(relativePath) &&
|
/\.tsx?$/u.test(relativePath) &&
|
||||||
@@ -394,7 +412,16 @@ export async function buildProductionModuleInventory(
|
|||||||
if (!openedMetadata.isFile()) {
|
if (!openedMetadata.isFile()) {
|
||||||
throw new TypeError("opened target is not a regular file");
|
throw new TypeError("opened target is not a regular file");
|
||||||
}
|
}
|
||||||
|
if (!sameFileIdentity(metadata, openedMetadata)) {
|
||||||
|
throw new FileIdentityChangedError("opened file identity changed");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof FileIdentityChangedError) {
|
||||||
|
throw new Error(
|
||||||
|
`production inventory file changed during validation: ${relativeTarget}`,
|
||||||
|
{ cause: error },
|
||||||
|
);
|
||||||
|
}
|
||||||
throw new Error(`production inventory file is unreadable: ${relativeTarget}`, {
|
throw new Error(`production inventory file is unreadable: ${relativeTarget}`, {
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
|
|||||||
+232
-250
@@ -12,6 +12,8 @@ import {
|
|||||||
} from "node:fs/promises";
|
} from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { isProductionModulePath } from "./lib/risk-coverage.ts";
|
||||||
|
|
||||||
const fixtureParent = path.resolve(".tmp");
|
const fixtureParent = path.resolve(".tmp");
|
||||||
await mkdir(fixtureParent, { recursive: true });
|
await mkdir(fixtureParent, { recursive: true });
|
||||||
const fixtureRoot = await mkdtemp(
|
const fixtureRoot = await mkdtemp(
|
||||||
@@ -182,265 +184,245 @@ function runPnpm(script: string, extra: string[] = []): boolean {
|
|||||||
return result.status === 0;
|
return result.status === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const target of copyTargets) {
|
try {
|
||||||
await cp(target, path.join(fixtureRoot, target), { 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,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await writeFile(
|
|
||||||
path.join(fixtureRoot, "src/features/installed-feature-contracts.ts"),
|
|
||||||
emptyContracts,
|
|
||||||
);
|
|
||||||
await writeFile(
|
|
||||||
path.join(fixtureRoot, "src/features/installed-feature-runtimes.tsx"),
|
|
||||||
emptyRuntimes,
|
|
||||||
);
|
|
||||||
await writeFile(
|
|
||||||
path.join(fixtureRoot, "src/features/installed-feature-adapters.ts"),
|
|
||||||
emptyAdapters,
|
|
||||||
);
|
|
||||||
await writeFile(
|
|
||||||
path.join(fixtureRoot, "src/features/installed-feature-messages.ts"),
|
|
||||||
emptyMessages,
|
|
||||||
);
|
|
||||||
await writeFile(
|
|
||||||
path.join(fixtureRoot, "src/features/installed-contract-contributions.ts"),
|
|
||||||
emptyContractContributions,
|
|
||||||
);
|
|
||||||
|
|
||||||
const coveragePolicyFile = path.join(
|
|
||||||
fixtureRoot,
|
|
||||||
"config/testing/risk-coverage.json",
|
|
||||||
);
|
|
||||||
const coveragePolicy = JSON.parse(
|
|
||||||
await readFile(coveragePolicyFile, "utf8"),
|
|
||||||
) as CoveragePolicy;
|
|
||||||
const retainedCriticalModules = coveragePolicy.criticalModules.filter(
|
|
||||||
(modulePolicy) => !modulePolicy.path?.startsWith(`${featureSource}/`),
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
retainedCriticalModules.length === coveragePolicy.criticalModules.length
|
|
||||||
) {
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
await symlink(path.resolve("node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
|
||||||
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`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const evidencePolicyFile = path.join(
|
const coveragePolicyFile = path.join(
|
||||||
fixtureRoot,
|
fixtureRoot,
|
||||||
"config/testing/test-evidence.json",
|
"config/testing/risk-coverage.json",
|
||||||
);
|
);
|
||||||
const evidencePolicy = JSON.parse(
|
const coveragePolicy = JSON.parse(
|
||||||
await readFile(evidencePolicyFile, "utf8"),
|
await readFile(coveragePolicyFile, "utf8"),
|
||||||
) as EvidencePolicy;
|
) as CoveragePolicy;
|
||||||
let removedEvidenceContributions = 0;
|
const generatedProductionModules = new Set(coveragePolicy.generatedPaths);
|
||||||
for (const policyKey of ["scenarioCatalogs", "sourceContracts"] as const) {
|
|
||||||
const contributions = evidencePolicy[policyKey];
|
const removedProductionModuleCount = (
|
||||||
if (!Array.isArray(contributions)) {
|
await filesBelow(path.join(fixtureRoot, featureSource))
|
||||||
throw new Error(`Test evidence policy is missing ${policyKey}`);
|
)
|
||||||
|
.map((file) => path.relative(fixtureRoot, file).split(path.sep).join("/"))
|
||||||
|
.filter(
|
||||||
|
(file) =>
|
||||||
|
isProductionModulePath(file) && !generatedProductionModules.has(file),
|
||||||
|
).length;
|
||||||
|
|
||||||
|
for (const ownedPath of featureOwnedPaths) {
|
||||||
|
await rm(path.join(fixtureRoot, ownedPath), {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
evidencePolicy[policyKey] = contributions.filter((candidate: unknown) => {
|
await writeFile(
|
||||||
const contribution = candidate as EvidenceContribution;
|
path.join(fixtureRoot, "src/features/installed-feature-contracts.ts"),
|
||||||
const retained = contribution.owner !== "reference-feature";
|
emptyContracts,
|
||||||
if (!retained) removedEvidenceContributions += 1;
|
);
|
||||||
return retained;
|
await writeFile(
|
||||||
});
|
path.join(fixtureRoot, "src/features/installed-feature-runtimes.tsx"),
|
||||||
}
|
emptyRuntimes,
|
||||||
if (removedEvidenceContributions === 0) {
|
);
|
||||||
throw new Error("Reference feature test evidence policy is not registered");
|
await writeFile(
|
||||||
}
|
path.join(fixtureRoot, "src/features/installed-feature-adapters.ts"),
|
||||||
await writeFile(
|
emptyAdapters,
|
||||||
evidencePolicyFile,
|
);
|
||||||
`${JSON.stringify(evidencePolicy, null, 2)}\n`,
|
await writeFile(
|
||||||
);
|
path.join(fixtureRoot, "src/features/installed-feature-messages.ts"),
|
||||||
|
emptyMessages,
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
path.join(fixtureRoot, "src/features/installed-contract-contributions.ts"),
|
||||||
|
emptyContractContributions,
|
||||||
|
);
|
||||||
|
|
||||||
const governanceFile = path.join(
|
const retainedCriticalModules = coveragePolicy.criticalModules.filter(
|
||||||
fixtureRoot,
|
(modulePolicy) => !modulePolicy.path?.startsWith(`${featureSource}/`),
|
||||||
"config/contracts/registry-governance.json",
|
);
|
||||||
);
|
if (
|
||||||
const removalGovernance = JSON.parse(
|
retainedCriticalModules.length === coveragePolicy.criticalModules.length
|
||||||
await readFile(governanceFile, "utf8"),
|
) {
|
||||||
) as RemovalGovernance;
|
throw new Error("Reference feature coverage policy is not registered");
|
||||||
removalGovernance.registries = removalGovernance.registries.map(
|
}
|
||||||
(registry) => ({
|
coveragePolicy.criticalModules = retainedCriticalModules;
|
||||||
...registry,
|
coveragePolicy.highRiskPaths = coveragePolicy.highRiskPaths.filter(
|
||||||
...(Array.isArray(registry.consumers)
|
(modulePath) => !modulePath.startsWith(`${featureSource}/`),
|
||||||
? {
|
);
|
||||||
consumers: registry.consumers.filter(
|
coveragePolicy.waivers = coveragePolicy.waivers.filter(
|
||||||
(candidate: unknown) => {
|
(waiver) => !waiver.path?.startsWith(`${featureSource}/`),
|
||||||
const consumer = candidate as GovernanceConsumer;
|
);
|
||||||
return !consumer.path?.includes("features/reference-feature");
|
coveragePolicy.generatedPaths = coveragePolicy.generatedPaths.filter(
|
||||||
},
|
(modulePath) => !modulePath.startsWith(`${featureSource}/`),
|
||||||
),
|
);
|
||||||
}
|
coveragePolicy.repositoryBaseline -= removedProductionModuleCount;
|
||||||
: {}),
|
if (coveragePolicy.repositoryBaseline <= 0) {
|
||||||
...(Array.isArray(registry.consumerDirectories)
|
throw new Error("Reference feature removal produced an invalid coverage baseline");
|
||||||
? {
|
}
|
||||||
consumerDirectories: registry.consumerDirectories.filter(
|
await writeFile(
|
||||||
(directory: unknown) =>
|
coveragePolicyFile,
|
||||||
typeof directory !== "string" ||
|
`${JSON.stringify(coveragePolicy, null, 2)}\n`,
|
||||||
!directory.includes("features/reference-feature"),
|
);
|
||||||
),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await writeFile(
|
|
||||||
governanceFile,
|
|
||||||
`${JSON.stringify(removalGovernance, null, 2)}\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const residue: string[] = [];
|
const evidencePolicyFile = path.join(
|
||||||
for (const root of ["src", "tests"]) {
|
fixtureRoot,
|
||||||
for (const file of await filesBelow(path.join(fixtureRoot, root))) {
|
"config/testing/test-evidence.json",
|
||||||
const relative = path.relative(fixtureRoot, file);
|
);
|
||||||
const content = await readFile(file, "utf8");
|
const evidencePolicy = JSON.parse(
|
||||||
if (
|
await readFile(evidencePolicyFile, "utf8"),
|
||||||
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(
|
) as EvidencePolicy;
|
||||||
`${relative}\n${content}`,
|
let removedEvidenceContributions = 0;
|
||||||
)
|
for (const policyKey of ["scenarioCatalogs", "sourceContracts"] as const) {
|
||||||
) {
|
const contributions = evidencePolicy[policyKey];
|
||||||
residue.push(relative);
|
if (!Array.isArray(contributions)) {
|
||||||
|
throw new Error(`Test evidence policy is missing ${policyKey}`);
|
||||||
|
}
|
||||||
|
evidencePolicy[policyKey] = contributions.filter((candidate: unknown) => {
|
||||||
|
const contribution = candidate as EvidenceContribution;
|
||||||
|
const retained = contribution.owner !== "reference-feature";
|
||||||
|
if (!retained) removedEvidenceContributions += 1;
|
||||||
|
return retained;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (removedEvidenceContributions === 0) {
|
||||||
|
throw new Error("Reference feature test evidence policy is not registered");
|
||||||
|
}
|
||||||
|
await writeFile(
|
||||||
|
evidencePolicyFile,
|
||||||
|
`${JSON.stringify(evidencePolicy, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const governanceFile = path.join(
|
||||||
|
fixtureRoot,
|
||||||
|
"config/contracts/registry-governance.json",
|
||||||
|
);
|
||||||
|
const removalGovernance = JSON.parse(
|
||||||
|
await readFile(governanceFile, "utf8"),
|
||||||
|
) as RemovalGovernance;
|
||||||
|
removalGovernance.registries = removalGovernance.registries.map(
|
||||||
|
(registry) => ({
|
||||||
|
...registry,
|
||||||
|
...(Array.isArray(registry.consumers)
|
||||||
|
? {
|
||||||
|
consumers: registry.consumers.filter(
|
||||||
|
(candidate: unknown) => {
|
||||||
|
const consumer = candidate as GovernanceConsumer;
|
||||||
|
return !consumer.path?.includes("features/reference-feature");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(Array.isArray(registry.consumerDirectories)
|
||||||
|
? {
|
||||||
|
consumerDirectories: registry.consumerDirectories.filter(
|
||||||
|
(directory: unknown) =>
|
||||||
|
typeof directory !== "string" ||
|
||||||
|
!directory.includes("features/reference-feature"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
governanceFile,
|
||||||
|
`${JSON.stringify(removalGovernance, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const residue: string[] = [];
|
||||||
|
for (const root of ["src", "tests"]) {
|
||||||
|
for (const file of await filesBelow(path.join(fixtureRoot, root))) {
|
||||||
|
const relative = path.relative(fixtureRoot, file);
|
||||||
|
const content = await readFile(file, "utf8");
|
||||||
|
if (
|
||||||
|
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(
|
||||||
|
`${relative}\n${content}`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
residue.push(relative);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const checks: Array<[string, boolean]> = [
|
const checks: Array<[string, boolean]> = [
|
||||||
[
|
[
|
||||||
"common-test-evidence",
|
"common-test-evidence",
|
||||||
(
|
(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
commonTestPaths.map(async (testPath) => {
|
commonTestPaths.map(async (testPath) => {
|
||||||
try {
|
try {
|
||||||
await access(path.join(fixtureRoot, testPath));
|
await access(path.join(fixtureRoot, testPath));
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
).every(Boolean),
|
).every(Boolean),
|
||||||
],
|
],
|
||||||
["typecheck", runPnpm("check:types")],
|
["typecheck", runPnpm("check:types")],
|
||||||
["architecture", runPnpm("check:architecture")],
|
["architecture", runPnpm("check:architecture")],
|
||||||
["registry-structure", runPnpm("check:registries:structure")],
|
["registry-structure", runPnpm("check:registries:structure")],
|
||||||
["unit-integration", runPnpm("test:all")],
|
["unit-integration", runPnpm("test:all")],
|
||||||
["coverage", runPnpm("test:coverage")],
|
["coverage", runPnpm("test:coverage")],
|
||||||
["test-evidence-source", runPnpm("check:test-evidence:source")],
|
["test-evidence-source", runPnpm("check:test-evidence:source")],
|
||||||
[
|
[
|
||||||
"home-smoke",
|
"home-smoke",
|
||||||
runPnpm("exec", [
|
runPnpm("exec", [
|
||||||
"vitest",
|
"vitest",
|
||||||
"run",
|
"run",
|
||||||
"tests/component/router.test.tsx",
|
"tests/component/router.test.tsx",
|
||||||
"--reporter=default",
|
"--reporter=default",
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
["build", runPnpm("build")],
|
["build", runPnpm("build")],
|
||||||
];
|
];
|
||||||
const builtResidue: string[] = [];
|
const builtResidue: string[] = [];
|
||||||
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
|
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
|
||||||
if (!/\.(?:js|css|html|json)$/.test(file)) continue;
|
if (!/\.(?:js|css|html|json)$/.test(file)) continue;
|
||||||
const content = await readFile(file, "utf8");
|
const content = await readFile(file, "utf8");
|
||||||
if (
|
if (
|
||||||
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(content)
|
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(content)
|
||||||
) {
|
) {
|
||||||
builtResidue.push(path.relative(fixtureRoot, file));
|
builtResidue.push(path.relative(fixtureRoot, file));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
const routeCatalog = await import(
|
||||||
const routeCatalog = await import(
|
`${new URL(
|
||||||
`${new URL(
|
"../src/features/installed-feature-contracts.ts",
|
||||||
"../src/features/installed-feature-contracts.ts",
|
`file://${fixtureRoot}/scripts/`,
|
||||||
`file://${fixtureRoot}/scripts/`,
|
).href}?removed=${Date.now()}`
|
||||||
).href}?removed=${Date.now()}`
|
) as { ROUTE_REGISTRY: Readonly<Record<string, unknown>> };
|
||||||
) as { ROUTE_REGISTRY: Readonly<Record<string, unknown>> };
|
const routeIds = Object.keys(routeCatalog.ROUTE_REGISTRY);
|
||||||
const routeIds = Object.keys(routeCatalog.ROUTE_REGISTRY);
|
const routeAbsent = routeIds.every((routeId) => !routeId.startsWith("REFERENCE_"));
|
||||||
const routeAbsent = routeIds.every((routeId) => !routeId.startsWith("REFERENCE_"));
|
checks.push(["route-absent", routeAbsent]);
|
||||||
checks.push(["route-absent", routeAbsent]);
|
checks.push(["fixture-id-residue", residue.length === 0]);
|
||||||
checks.push(["fixture-id-residue", residue.length === 0]);
|
checks.push(["built-fixture-id-residue", builtResidue.length === 0]);
|
||||||
checks.push(["built-fixture-id-residue", builtResidue.length === 0]);
|
|
||||||
|
|
||||||
const passed = checks.every(([, result]) => result);
|
const passed = checks.every(([, result]) => result);
|
||||||
await mkdir("artifacts/tests", { recursive: true });
|
await mkdir("artifacts/tests", { recursive: true });
|
||||||
await writeFile(
|
await writeFile(
|
||||||
"artifacts/tests/sample-removal.xml",
|
"artifacts/tests/sample-removal.xml",
|
||||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||||
`<testsuite name="reference-feature-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
|
`<testsuite name="reference-feature-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
|
||||||
checks
|
checks
|
||||||
.map(
|
.map(
|
||||||
([name, result]) =>
|
([name, result]) =>
|
||||||
`<testcase name="${name}">${result ? "" : `<failure>${[...residue, ...builtResidue].join(", ")}</failure>`}</testcase>`,
|
`<testcase name="${name}">${result ? "" : `<failure>${[...residue, ...builtResidue].join(", ")}</failure>`}</testcase>`,
|
||||||
)
|
)
|
||||||
.join("") +
|
.join("") +
|
||||||
`</testsuite>\n`,
|
`</testsuite>\n`,
|
||||||
);
|
|
||||||
await rm(fixtureRoot, { recursive: true, force: true });
|
|
||||||
|
|
||||||
if (!passed) {
|
|
||||||
const failures = checks
|
|
||||||
.filter(([, result]) => !result)
|
|
||||||
.map(([name]) => name);
|
|
||||||
process.stderr.write(
|
|
||||||
`Reference feature removal failed: ${failures.join(", ")}; residue: ${[...residue, ...builtResidue].join(", ")}\n`,
|
|
||||||
);
|
);
|
||||||
process.exit(1);
|
if (!passed) {
|
||||||
|
const failures = checks
|
||||||
|
.filter(([, result]) => !result)
|
||||||
|
.map(([name]) => name);
|
||||||
|
process.stderr.write(
|
||||||
|
`Reference feature removal failed: ${failures.join(", ")}; residue: ${[...residue, ...builtResidue].join(", ")}\n`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
} else {
|
||||||
|
process.stdout.write(
|
||||||
|
`Reference feature removal: PASS (${checks.length} checks, no fixture IDs)\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await rm(fixtureRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
process.stdout.write(
|
|
||||||
`Reference feature removal: PASS (${checks.length} checks, no fixture IDs)\n`,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
declaredContentLength,
|
||||||
|
decodeJsonBytes,
|
||||||
|
isEffectivelyEmpty,
|
||||||
|
isJsonMediaType,
|
||||||
|
probeForbiddenBody,
|
||||||
|
readBoundedBytes,
|
||||||
|
} from "../../src/adapters/http/bounded-body-reader.ts";
|
||||||
|
|
||||||
|
function responseWithBody(
|
||||||
|
body: ReadableStream<Uint8Array> | null,
|
||||||
|
contentLength?: string,
|
||||||
|
): Response {
|
||||||
|
return new Response(body, {
|
||||||
|
headers:
|
||||||
|
contentLength === undefined ? undefined : { "content-length": contentLength },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("bounded body reader", () => {
|
||||||
|
it.each([
|
||||||
|
[null, false],
|
||||||
|
["", false],
|
||||||
|
[" application/json ; charset=utf-8 ", true],
|
||||||
|
["APPLICATION/PROBLEM+JSON", true],
|
||||||
|
["text/json", false],
|
||||||
|
["application/jsonp", false],
|
||||||
|
] as const)("classifies JSON media type %j", (header, expected) => {
|
||||||
|
expect(isJsonMediaType(header)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[undefined, null],
|
||||||
|
["0", 0],
|
||||||
|
["12", 12],
|
||||||
|
["-1", null],
|
||||||
|
["NaN", null],
|
||||||
|
["Infinity", null],
|
||||||
|
] as const)("parses declared content length %s", (header, expected) => {
|
||||||
|
expect(declaredContentLength(responseWithBody(null, header))).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an oversized declared body and tolerates cancellation failure", async () => {
|
||||||
|
const cancel = vi.fn(async () => {
|
||||||
|
throw new Error("already settled");
|
||||||
|
});
|
||||||
|
const response = {
|
||||||
|
headers: new Headers({ "content-length": "9" }),
|
||||||
|
body: { cancel },
|
||||||
|
} as unknown as Response;
|
||||||
|
|
||||||
|
await expect(readBoundedBytes(response, 8)).resolves.toEqual({
|
||||||
|
ok: false,
|
||||||
|
code: "RESPONSE_TOO_LARGE",
|
||||||
|
});
|
||||||
|
expect(cancel).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty bytes when a successful response has no body", async () => {
|
||||||
|
const result = await readBoundedBytes(responseWithBody(null), 8);
|
||||||
|
expect(result).toEqual({ ok: true, bytes: new Uint8Array(0) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("joins chunks without retaining empty chunks", async () => {
|
||||||
|
const body = new ReadableStream<Uint8Array>({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue(new Uint8Array(0));
|
||||||
|
controller.enqueue(Uint8Array.of(1, 2));
|
||||||
|
controller.enqueue(Uint8Array.of(3));
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(readBoundedBytes(responseWithBody(body), 3)).resolves.toEqual({
|
||||||
|
ok: true,
|
||||||
|
bytes: Uint8Array.of(1, 2, 3),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels streaming input as soon as the accumulated limit is exceeded", async () => {
|
||||||
|
const cancel = vi.fn().mockRejectedValue(new Error("cancel failed"));
|
||||||
|
const reader = {
|
||||||
|
read: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ done: false, value: Uint8Array.of(1, 2) })
|
||||||
|
.mockResolvedValueOnce({ done: false, value: Uint8Array.of(3, 4) }),
|
||||||
|
cancel,
|
||||||
|
releaseLock: vi.fn(),
|
||||||
|
};
|
||||||
|
const response = {
|
||||||
|
headers: new Headers(),
|
||||||
|
body: { getReader: () => reader },
|
||||||
|
} as unknown as Response;
|
||||||
|
|
||||||
|
await expect(readBoundedBytes(response, 3)).resolves.toEqual({
|
||||||
|
ok: false,
|
||||||
|
code: "RESPONSE_TOO_LARGE",
|
||||||
|
});
|
||||||
|
expect(cancel).toHaveBeenCalledOnce();
|
||||||
|
expect(reader.releaseLock).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps reader failure and cancellation failure to a stream failure", async () => {
|
||||||
|
const reader = {
|
||||||
|
read: vi.fn().mockRejectedValue(new Error("stream failed")),
|
||||||
|
cancel: vi.fn().mockRejectedValue(new Error("cancel failed")),
|
||||||
|
releaseLock: vi.fn(() => {
|
||||||
|
throw new Error("already released");
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const response = {
|
||||||
|
headers: new Headers(),
|
||||||
|
body: { getReader: () => reader },
|
||||||
|
} as unknown as Response;
|
||||||
|
|
||||||
|
await expect(readBoundedBytes(response, 3)).resolves.toEqual({
|
||||||
|
ok: false,
|
||||||
|
code: "RESPONSE_STREAM_FAILURE",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects a forbidden body from declared length without reading it", async () => {
|
||||||
|
const cancel = vi.fn();
|
||||||
|
const response = {
|
||||||
|
headers: new Headers({ "content-length": "1" }),
|
||||||
|
body: { cancel },
|
||||||
|
} as unknown as Response;
|
||||||
|
|
||||||
|
await expect(probeForbiddenBody(response)).resolves.toEqual({
|
||||||
|
ok: true,
|
||||||
|
present: true,
|
||||||
|
});
|
||||||
|
expect(cancel).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts an absent, completed, or zero-byte forbidden body", async () => {
|
||||||
|
await expect(probeForbiddenBody(responseWithBody(null))).resolves.toEqual({
|
||||||
|
ok: true,
|
||||||
|
present: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const next of [
|
||||||
|
{ done: true, value: undefined },
|
||||||
|
{ done: false, value: new Uint8Array(0) },
|
||||||
|
]) {
|
||||||
|
const reader = {
|
||||||
|
read: vi.fn().mockResolvedValue(next),
|
||||||
|
cancel: vi.fn(),
|
||||||
|
releaseLock: vi.fn(),
|
||||||
|
};
|
||||||
|
const response = {
|
||||||
|
headers: new Headers(),
|
||||||
|
body: { getReader: () => reader },
|
||||||
|
} as unknown as Response;
|
||||||
|
await expect(probeForbiddenBody(response)).resolves.toEqual({
|
||||||
|
ok: true,
|
||||||
|
present: false,
|
||||||
|
});
|
||||||
|
expect(reader.cancel).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("probes only one present byte and cancels the remaining body", async () => {
|
||||||
|
const reader = {
|
||||||
|
read: vi.fn().mockResolvedValue({ done: false, value: Uint8Array.of(1) }),
|
||||||
|
cancel: vi.fn().mockRejectedValue(new Error("cancel failed")),
|
||||||
|
releaseLock: vi.fn(),
|
||||||
|
};
|
||||||
|
const response = {
|
||||||
|
headers: new Headers(),
|
||||||
|
body: { getReader: () => reader },
|
||||||
|
} as unknown as Response;
|
||||||
|
|
||||||
|
await expect(probeForbiddenBody(response)).resolves.toEqual({
|
||||||
|
ok: true,
|
||||||
|
present: true,
|
||||||
|
});
|
||||||
|
expect(reader.read).toHaveBeenCalledOnce();
|
||||||
|
expect(reader.cancel).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps a forbidden-body probe error even when cleanup also fails", async () => {
|
||||||
|
const reader = {
|
||||||
|
read: vi.fn().mockRejectedValue(new Error("probe failed")),
|
||||||
|
cancel: vi.fn().mockRejectedValue(new Error("cancel failed")),
|
||||||
|
releaseLock: vi.fn(() => {
|
||||||
|
throw new Error("released");
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const response = {
|
||||||
|
headers: new Headers(),
|
||||||
|
body: { getReader: () => reader },
|
||||||
|
} as unknown as Response;
|
||||||
|
|
||||||
|
await expect(probeForbiddenBody(response)).resolves.toEqual({
|
||||||
|
ok: false,
|
||||||
|
code: "RESPONSE_STREAM_FAILURE",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes valid JSON and distinguishes UTF-8 from JSON failures", () => {
|
||||||
|
expect(decodeJsonBytes(new TextEncoder().encode('{"ok":true}'))).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { ok: true },
|
||||||
|
});
|
||||||
|
expect(decodeJsonBytes(Uint8Array.of(0xc3, 0x28))).toEqual({
|
||||||
|
ok: false,
|
||||||
|
code: "UTF8_INVALID",
|
||||||
|
});
|
||||||
|
expect(decodeJsonBytes(new TextEncoder().encode("{"))).toEqual({
|
||||||
|
ok: false,
|
||||||
|
code: "JSON_INVALID",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[new Uint8Array(0), true],
|
||||||
|
[Uint8Array.of(0x20, 0x09, 0x0a, 0x0d), true],
|
||||||
|
[Uint8Array.of(0x20, 0x00), false],
|
||||||
|
])("classifies effective emptiness %#", (bytes, expected) => {
|
||||||
|
expect(isEffectivelyEmpty(bytes)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { constants } from "node:fs";
|
import { constants } from "node:fs";
|
||||||
import {
|
import {
|
||||||
mkdir,
|
mkdir,
|
||||||
|
link,
|
||||||
mkdtemp,
|
mkdtemp,
|
||||||
open,
|
open,
|
||||||
readFile,
|
readFile,
|
||||||
@@ -78,6 +79,13 @@ describe("risk coverage CLI files", () => {
|
|||||||
path.join(repositoryRoot, "config/testing/link.json"),
|
path.join(repositoryRoot, "config/testing/link.json"),
|
||||||
);
|
);
|
||||||
await symlink(outside, path.join(repositoryRoot, "linked-config"), "dir");
|
await symlink(outside, path.join(repositoryRoot, "linked-config"), "dir");
|
||||||
|
await mkdir(path.join(repositoryRoot, "real-config"));
|
||||||
|
await writeFile(path.join(repositoryRoot, "real-config/inside.json"), "{}\n");
|
||||||
|
await symlink(
|
||||||
|
path.join(repositoryRoot, "real-config"),
|
||||||
|
path.join(repositoryRoot, "inside-alias"),
|
||||||
|
"dir",
|
||||||
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
readRiskCoverageInput({
|
readRiskCoverageInput({
|
||||||
@@ -93,6 +101,34 @@ describe("risk coverage CLI files", () => {
|
|||||||
label: "policy",
|
label: "policy",
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(/outside repository|symlink/u);
|
).rejects.toThrow(/outside repository|symlink/u);
|
||||||
|
await expect(
|
||||||
|
readRiskCoverageInput({
|
||||||
|
repositoryRoot,
|
||||||
|
relativePath: "inside-alias/inside.json",
|
||||||
|
label: "policy",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/ancestor is a symlink/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an input identity swap between lstat and open", async () => {
|
||||||
|
const repositoryRoot = await fixture();
|
||||||
|
const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-input-race-"));
|
||||||
|
roots.push(outside);
|
||||||
|
const replacement = path.join(outside, "replacement.json");
|
||||||
|
await writeFile(replacement, "{\"replacement\":true}\n");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
readRiskCoverageInput(
|
||||||
|
{
|
||||||
|
repositoryRoot,
|
||||||
|
relativePath: "config/testing/policy.json",
|
||||||
|
label: "policy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
openFile: async (_target, flags) => open(replacement, flags),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).rejects.toThrow(/changed during validation/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("confines artifact output and rejects input overwrite or symlink ancestors", async () => {
|
it("confines artifact output and rejects input overwrite or symlink ancestors", async () => {
|
||||||
@@ -132,6 +168,24 @@ describe("risk coverage CLI files", () => {
|
|||||||
).rejects.toThrow(/symlink/u);
|
).rejects.toThrow(/symlink/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects input overwrite through a realpath or hard-link alias", 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, "{}\n");
|
||||||
|
const hardLinkInput = path.join(repositoryRoot, "config/testing/output-alias.json");
|
||||||
|
await link(destination, hardLinkInput);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveRiskCoverageArtifactPath({
|
||||||
|
repositoryRoot,
|
||||||
|
relativePath: "artifacts/quality/risk-coverage.json",
|
||||||
|
inputPaths: ["config/testing/output-alias.json"],
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/same file as an input/u);
|
||||||
|
});
|
||||||
|
|
||||||
it("syncs an exclusive sibling temp before atomic rename", async () => {
|
it("syncs an exclusive sibling temp before atomic rename", async () => {
|
||||||
const repositoryRoot = await fixture();
|
const repositoryRoot = await fixture();
|
||||||
const observed: string[] = [];
|
const observed: string[] = [];
|
||||||
@@ -233,6 +287,54 @@ describe("risk coverage CLI files", () => {
|
|||||||
expect(await readdir(outputDirectory)).toEqual(["risk-coverage.json"]);
|
expect(await readdir(outputDirectory)).toEqual(["risk-coverage.json"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["EINVAL", true],
|
||||||
|
["ENOTSUP", true],
|
||||||
|
["EIO", false],
|
||||||
|
] as const)(
|
||||||
|
"handles directory sync error %s with an explicit portability fallback",
|
||||||
|
async (code, accepted) => {
|
||||||
|
const repositoryRoot = await fixture();
|
||||||
|
const operation = writeRiskCoverageArtifactAtomic(
|
||||||
|
{
|
||||||
|
repositoryRoot,
|
||||||
|
relativePath: `artifacts/quality/sync-${code}.json`,
|
||||||
|
inputPaths: [],
|
||||||
|
value: { schemaVersion: 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createNonce: () => code,
|
||||||
|
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 () => {
|
||||||
|
throw Object.assign(new Error(`sync ${code}`), { code });
|
||||||
|
},
|
||||||
|
close: async () => handle.close(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rename,
|
||||||
|
rm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (accepted) {
|
||||||
|
await expect(operation).resolves.toBeUndefined();
|
||||||
|
} else {
|
||||||
|
await expect(operation).rejects.toMatchObject({ code });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("has no changed-files gate in the executable", async () => {
|
it("has no changed-files gate in the executable", async () => {
|
||||||
const source = await readFile("scripts/check-risk-coverage.ts", "utf8");
|
const source = await readFile("scripts/check-risk-coverage.ts", "utf8");
|
||||||
expect(source).not.toMatch(/changedFiles|changed-files/u);
|
expect(source).not.toMatch(/changedFiles|changed-files/u);
|
||||||
|
|||||||
@@ -186,6 +186,34 @@ describe("repository-aware risk coverage", () => {
|
|||||||
).toThrow(/unexpected coverage path.*tests\/unit\/a\.test\.ts/u);
|
).toThrow(/unexpected coverage path.*tests\/unit\/a\.test\.ts/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects outside and duplicate normalized producer paths", () => {
|
||||||
|
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, "/outside/src/a.ts": fullMetrics },
|
||||||
|
}),
|
||||||
|
).toThrow(/outside repository/u);
|
||||||
|
expect(() =>
|
||||||
|
evaluateRiskCoverage({
|
||||||
|
...base,
|
||||||
|
summary: {
|
||||||
|
total: metrics(2),
|
||||||
|
"src/a.ts": fullMetrics,
|
||||||
|
"/repository/src/a.ts": fullMetrics,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrow(/duplicate coverage path/u);
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts only explicitly configured generated coverage paths", () => {
|
it("accepts only explicitly configured generated coverage paths", () => {
|
||||||
const parsedPolicy = parseRiskCoveragePolicy(
|
const parsedPolicy = parseRiskCoveragePolicy(
|
||||||
policy({ repositoryBaseline: 1 }),
|
policy({ repositoryBaseline: 1 }),
|
||||||
@@ -341,6 +369,22 @@ describe("repository-aware risk coverage", () => {
|
|||||||
expect(observedFlags & constants.O_NOFOLLOW).toBe(constants.O_NOFOLLOW);
|
expect(observedFlags & constants.O_NOFOLLOW).toBe(constants.O_NOFOLLOW);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects a post-lstat file identity swap even without relying on O_NOFOLLOW", async () => {
|
||||||
|
const repositoryRoot = await repositoryFixture();
|
||||||
|
const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-race-"));
|
||||||
|
roots.push(outside);
|
||||||
|
const outsideFile = path.join(outside, "replacement.ts");
|
||||||
|
await writeFile(outsideFile, "export const replacement = true;\n");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
buildProductionModuleInventory({
|
||||||
|
repositoryRoot,
|
||||||
|
openFile: async (target, flags) =>
|
||||||
|
open(target.endsWith("src/a.ts") ? outsideFile : target, flags),
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/changed during validation/u);
|
||||||
|
});
|
||||||
|
|
||||||
it("fails closed on empty, traversing, symlinked, or stale generated inventory", async () => {
|
it("fails closed on empty, traversing, symlinked, or stale generated inventory", async () => {
|
||||||
const repositoryRoot = await repositoryFixture();
|
const repositoryRoot = await repositoryFixture();
|
||||||
const emptyRoot = await mkdtemp(path.join(tmpdir(), "risk-coverage-empty-"));
|
const emptyRoot = await mkdtemp(path.join(tmpdir(), "risk-coverage-empty-"));
|
||||||
|
|||||||
Reference in New Issue
Block a user