fix: close coverage evidence races
This commit is contained in:
@@ -35,6 +35,18 @@ type WriterDependencies = Readonly<{
|
||||
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({
|
||||
openFile: async (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 {
|
||||
if (metadata.isSymbolicLink()) {
|
||||
throw new TypeError(`artifact ancestor is a symlink: ${relativePath}`);
|
||||
@@ -87,27 +138,41 @@ export async function readRiskCoverageInput(input: Readonly<{
|
||||
repositoryRoot: string;
|
||||
relativePath: 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(
|
||||
input.relativePath,
|
||||
`${input.label} path`,
|
||||
);
|
||||
const repositoryRoot = path.resolve(input.repositoryRoot);
|
||||
const repositoryRealpath = await realpath(repositoryRoot);
|
||||
const repositoryRealpath = await realpathPath(repositoryRoot);
|
||||
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()) {
|
||||
throw new TypeError(`${input.label} path is a symlink: ${relativePath}`);
|
||||
}
|
||||
if (!metadata.isFile()) {
|
||||
throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`);
|
||||
}
|
||||
const resolvedPath = await realpath(absolutePath);
|
||||
const resolvedPath = await realpathPath(absolutePath);
|
||||
if (!isWithin(repositoryRealpath, resolvedPath)) {
|
||||
throw new TypeError(`${input.label} path is outside repository: ${relativePath}`);
|
||||
}
|
||||
|
||||
const handle = await open(
|
||||
const handle = await openFile(
|
||||
absolutePath,
|
||||
constants.O_RDONLY | constants.O_NOFOLLOW,
|
||||
);
|
||||
@@ -116,6 +181,9 @@ export async function readRiskCoverageInput(input: Readonly<{
|
||||
if (!openedMetadata.isFile()) {
|
||||
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");
|
||||
return Object.freeze({ relativePath, absolutePath, text });
|
||||
} finally {
|
||||
@@ -170,17 +238,35 @@ export async function resolveRiskCoverageArtifactPath(input: Readonly<{
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(repositoryRoot, relativePath);
|
||||
let destinationMetadata: Stats | undefined;
|
||||
try {
|
||||
const metadata = await lstat(absolutePath);
|
||||
if (metadata.isSymbolicLink()) {
|
||||
destinationMetadata = await lstat(absolutePath);
|
||||
if (destinationMetadata.isSymbolicLink()) {
|
||||
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}`);
|
||||
}
|
||||
} catch (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;
|
||||
}
|
||||
|
||||
@@ -232,7 +318,14 @@ export async function writeRiskCoverageArtifactAtomic(
|
||||
ownsTemporaryFile = false;
|
||||
const directoryHandle = await fileSystem.openDirectory(path.dirname(destination));
|
||||
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 {
|
||||
await directoryHandle.close();
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ type InventoryOptions = Readonly<{
|
||||
openFile?: (target: string, flags: number) => Promise<ReadableFileHandle>;
|
||||
}>;
|
||||
|
||||
class FileIdentityChangedError extends Error {}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
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 {
|
||||
return (
|
||||
/\.tsx?$/u.test(relativePath) &&
|
||||
@@ -394,7 +412,16 @@ export async function buildProductionModuleInventory(
|
||||
if (!openedMetadata.isFile()) {
|
||||
throw new TypeError("opened target is not a regular file");
|
||||
}
|
||||
if (!sameFileIdentity(metadata, openedMetadata)) {
|
||||
throw new FileIdentityChangedError("opened file identity changed");
|
||||
}
|
||||
} 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}`, {
|
||||
cause: error,
|
||||
});
|
||||
|
||||
+232
-250
@@ -12,6 +12,8 @@ import {
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { isProductionModulePath } from "./lib/risk-coverage.ts";
|
||||
|
||||
const fixtureParent = path.resolve(".tmp");
|
||||
await mkdir(fixtureParent, { recursive: true });
|
||||
const fixtureRoot = await mkdtemp(
|
||||
@@ -182,265 +184,245 @@ function runPnpm(script: string, extra: string[] = []): boolean {
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
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}`,
|
||||
);
|
||||
try {
|
||||
for (const target of copyTargets) {
|
||||
await cp(target, path.join(fixtureRoot, target), { recursive: true });
|
||||
}
|
||||
}
|
||||
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`,
|
||||
);
|
||||
await symlink(path.resolve("node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
|
||||
|
||||
const evidencePolicyFile = path.join(
|
||||
fixtureRoot,
|
||||
"config/testing/test-evidence.json",
|
||||
);
|
||||
const evidencePolicy = JSON.parse(
|
||||
await readFile(evidencePolicyFile, "utf8"),
|
||||
) as EvidencePolicy;
|
||||
let removedEvidenceContributions = 0;
|
||||
for (const policyKey of ["scenarioCatalogs", "sourceContracts"] as const) {
|
||||
const contributions = evidencePolicy[policyKey];
|
||||
if (!Array.isArray(contributions)) {
|
||||
throw new Error(`Test evidence policy is missing ${policyKey}`);
|
||||
const coveragePolicyFile = path.join(
|
||||
fixtureRoot,
|
||||
"config/testing/risk-coverage.json",
|
||||
);
|
||||
const coveragePolicy = JSON.parse(
|
||||
await readFile(coveragePolicyFile, "utf8"),
|
||||
) as CoveragePolicy;
|
||||
const generatedProductionModules = new Set(coveragePolicy.generatedPaths);
|
||||
|
||||
const removedProductionModuleCount = (
|
||||
await filesBelow(path.join(fixtureRoot, featureSource))
|
||||
)
|
||||
.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) => {
|
||||
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`,
|
||||
);
|
||||
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 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 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;
|
||||
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 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 evidencePolicyFile = path.join(
|
||||
fixtureRoot,
|
||||
"config/testing/test-evidence.json",
|
||||
);
|
||||
const evidencePolicy = JSON.parse(
|
||||
await readFile(evidencePolicyFile, "utf8"),
|
||||
) as EvidencePolicy;
|
||||
let removedEvidenceContributions = 0;
|
||||
for (const policyKey of ["scenarioCatalogs", "sourceContracts"] as const) {
|
||||
const contributions = evidencePolicy[policyKey];
|
||||
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]> = [
|
||||
[
|
||||
"common-test-evidence",
|
||||
(
|
||||
await Promise.all(
|
||||
commonTestPaths.map(async (testPath) => {
|
||||
try {
|
||||
await access(path.join(fixtureRoot, testPath));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).every(Boolean),
|
||||
],
|
||||
["typecheck", runPnpm("check:types")],
|
||||
["architecture", runPnpm("check:architecture")],
|
||||
["registry-structure", runPnpm("check:registries:structure")],
|
||||
["unit-integration", runPnpm("test:all")],
|
||||
["coverage", runPnpm("test:coverage")],
|
||||
["test-evidence-source", runPnpm("check:test-evidence:source")],
|
||||
[
|
||||
"home-smoke",
|
||||
runPnpm("exec", [
|
||||
"vitest",
|
||||
"run",
|
||||
"tests/component/router.test.tsx",
|
||||
"--reporter=default",
|
||||
]),
|
||||
],
|
||||
["build", runPnpm("build")],
|
||||
];
|
||||
const builtResidue: string[] = [];
|
||||
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
|
||||
if (!/\.(?:js|css|html|json)$/.test(file)) continue;
|
||||
const content = await readFile(file, "utf8");
|
||||
if (
|
||||
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(content)
|
||||
) {
|
||||
builtResidue.push(path.relative(fixtureRoot, file));
|
||||
const checks: Array<[string, boolean]> = [
|
||||
[
|
||||
"common-test-evidence",
|
||||
(
|
||||
await Promise.all(
|
||||
commonTestPaths.map(async (testPath) => {
|
||||
try {
|
||||
await access(path.join(fixtureRoot, testPath));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).every(Boolean),
|
||||
],
|
||||
["typecheck", runPnpm("check:types")],
|
||||
["architecture", runPnpm("check:architecture")],
|
||||
["registry-structure", runPnpm("check:registries:structure")],
|
||||
["unit-integration", runPnpm("test:all")],
|
||||
["coverage", runPnpm("test:coverage")],
|
||||
["test-evidence-source", runPnpm("check:test-evidence:source")],
|
||||
[
|
||||
"home-smoke",
|
||||
runPnpm("exec", [
|
||||
"vitest",
|
||||
"run",
|
||||
"tests/component/router.test.tsx",
|
||||
"--reporter=default",
|
||||
]),
|
||||
],
|
||||
["build", runPnpm("build")],
|
||||
];
|
||||
const builtResidue: string[] = [];
|
||||
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
|
||||
if (!/\.(?:js|css|html|json)$/.test(file)) continue;
|
||||
const content = await readFile(file, "utf8");
|
||||
if (
|
||||
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(content)
|
||||
) {
|
||||
builtResidue.push(path.relative(fixtureRoot, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
const routeCatalog = await import(
|
||||
`${new URL(
|
||||
"../src/features/installed-feature-contracts.ts",
|
||||
`file://${fixtureRoot}/scripts/`,
|
||||
).href}?removed=${Date.now()}`
|
||||
) as { ROUTE_REGISTRY: Readonly<Record<string, unknown>> };
|
||||
const routeIds = Object.keys(routeCatalog.ROUTE_REGISTRY);
|
||||
const routeAbsent = routeIds.every((routeId) => !routeId.startsWith("REFERENCE_"));
|
||||
checks.push(["route-absent", routeAbsent]);
|
||||
checks.push(["fixture-id-residue", residue.length === 0]);
|
||||
checks.push(["built-fixture-id-residue", builtResidue.length === 0]);
|
||||
const routeCatalog = await import(
|
||||
`${new URL(
|
||||
"../src/features/installed-feature-contracts.ts",
|
||||
`file://${fixtureRoot}/scripts/`,
|
||||
).href}?removed=${Date.now()}`
|
||||
) as { ROUTE_REGISTRY: Readonly<Record<string, unknown>> };
|
||||
const routeIds = Object.keys(routeCatalog.ROUTE_REGISTRY);
|
||||
const routeAbsent = routeIds.every((routeId) => !routeId.startsWith("REFERENCE_"));
|
||||
checks.push(["route-absent", routeAbsent]);
|
||||
checks.push(["fixture-id-residue", residue.length === 0]);
|
||||
checks.push(["built-fixture-id-residue", builtResidue.length === 0]);
|
||||
|
||||
const passed = checks.every(([, result]) => result);
|
||||
await mkdir("artifacts/tests", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/tests/sample-removal.xml",
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||
`<testsuite name="reference-feature-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
|
||||
checks
|
||||
.map(
|
||||
([name, result]) =>
|
||||
`<testcase name="${name}">${result ? "" : `<failure>${[...residue, ...builtResidue].join(", ")}</failure>`}</testcase>`,
|
||||
)
|
||||
.join("") +
|
||||
`</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`,
|
||||
const passed = checks.every(([, result]) => result);
|
||||
await mkdir("artifacts/tests", { recursive: true });
|
||||
await writeFile(
|
||||
"artifacts/tests/sample-removal.xml",
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||
`<testsuite name="reference-feature-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
|
||||
checks
|
||||
.map(
|
||||
([name, result]) =>
|
||||
`<testcase name="${name}">${result ? "" : `<failure>${[...residue, ...builtResidue].join(", ")}</failure>`}</testcase>`,
|
||||
)
|
||||
.join("") +
|
||||
`</testsuite>\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`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user