FE-GATE-020 proves a capability can be removed by rebuilding the whole project without it. The fixture it built could not get that far, and the failures all came from the fixture rather than from anything about removability. It was not a repository. The supply-chain inventory is defined as the tracked file set, so it asks `git ls-files` what the project contains; with no repository to ask, generation failed and took every provider suite down with it. It is now initialised on preparation and committed after the removal — not before, or the index would still list the files the removal deleted. It had no `.gitignore`, so once it did have a repository, every generated artifact and every linked module landed in the index and the inventory refused the fixture for tracked and generated paths colliding. It carries the ignore rules now, and therefore records the same tracked set as the repository it was copied from. Each removal script kept its own copy-target list and they had drifted: the reference-feature fixture omitted `playwright.capabilities.config.ts`, which the inventory requires. There is one list now. It also gained the install and workspace identity — `.npmrc`, the lockfile, the workspace file — without which the fixture is a different project, and the release evidence a candidate is assembled from, without which no candidate can be built at all. A tracked root the removal deletes is no longer required of the result: the optional-recipe fixture deletes `recipes/`, and the inventory policy demanded it back. Roots that are gone are pruned from the fixture's policy. Two smaller causes. A platform integration file asserted the reference feature's own route ids, so removing the feature left it importing a deleted module — typecheck, the test run, coverage and the residue scan all failed on that one misplaced assertion, which now lives in the feature's test tree. And the canonical exact-count authority was re-imposed on a contract the fixture deliberately reduces, failing the fixture for the reduction it exists to prove; `CI_CONTRACT_MODE` already marked those runs and is now honoured by default. The reference-feature fixture goes from failing before its first assertion to 1,612 passing with one failure, and that one is the live process-tree observation test already red on the main tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
122 lines
5.4 KiB
TypeScript
122 lines
5.4 KiB
TypeScript
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { loadCiGateContract } from "../../scripts/contracts/ci-gates.ts";
|
|
import {
|
|
assertNoRuntimeImports,
|
|
pruneRemovalFixtureCiContract,
|
|
removeRuntimeDependentTests,
|
|
runtimeImportGraph,
|
|
} from "../../scripts/lib/removal-fixture.ts";
|
|
|
|
const temporaryRoots: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
it("rejects an empty scanned module universe", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "removal-fixture-empty-modules-"));
|
|
temporaryRoots.push(root);
|
|
|
|
await expect(runtimeImportGraph(root, ["src/runtime"])).rejects.toThrow(
|
|
/scanned module universe is empty/i,
|
|
);
|
|
});
|
|
|
|
it("rejects a runtime removal scan that discovers no dependent tests", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "removal-fixture-zero-dependent-tests-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "src/runtime"), { recursive: true });
|
|
await mkdir(path.join(root, "tests"), { recursive: true });
|
|
await writeFile(path.join(root, "src/runtime/index.ts"), "export const runtime = true;\n");
|
|
await writeFile(path.join(root, "tests/unrelated.test.ts"), "export const unrelated = true;\n");
|
|
|
|
await expect(removeRuntimeDependentTests(root, ["src/runtime"])).rejects.toThrow(
|
|
/no runtime-dependent tests discovered/i,
|
|
);
|
|
});
|
|
|
|
it("allows zero runtime imports after the runtime has been removed", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "removal-fixture-post-removal-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "tests"), { recursive: true });
|
|
await writeFile(path.join(root, "tests/unrelated.test.ts"), "export const unrelated = true;\n");
|
|
|
|
await expect(
|
|
assertNoRuntimeImports(root, ["src/runtime"], "fixture runtime"),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("prunes removed command and evidence references from a reduced CI contract", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "removal-fixture-contract-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
|
await writeFile(path.join(root, "config/ci/gates.json"), await readFile("config/ci/gates.json"));
|
|
await writeFile(path.join(root, "package.json"), await readFile("package.json"));
|
|
|
|
await pruneRemovalFixtureCiContract({
|
|
root,
|
|
removedScripts: new Set(["test:reference-feature"]),
|
|
removedEvidencePathFragments: ["reference-feature.xml"],
|
|
});
|
|
|
|
const contract = await loadCiGateContract(root, { mode: "removal-fixture" });
|
|
expect(contract.commands.some(({ script }) => script === "test:reference-feature")).toBe(false);
|
|
expect(contract.artifacts.some(({ path }) => path.includes("reference-feature.xml"))).toBe(false);
|
|
expect(contract.gates.some(({ commandIds }) => commandIds.includes("test-reference-feature"))).toBe(false);
|
|
expect(contract.gates.some(({ evidenceArtifactIds }) =>
|
|
evidenceArtifactIds.includes("artifact-artifacts-tests-reference-feature-xml")
|
|
)).toBe(false);
|
|
});
|
|
|
|
it("rejects pruning that leaves a reduced gate without commands", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "removal-fixture-empty-gate-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
|
await writeFile(path.join(root, "config/ci/gates.json"), await readFile("config/ci/gates.json"));
|
|
await writeFile(path.join(root, "package.json"), await readFile("package.json"));
|
|
|
|
await expect(pruneRemovalFixtureCiContract({
|
|
root,
|
|
removedScripts: new Set(["test:runtime-schema"]),
|
|
removedEvidencePathFragments: ["runtime-schema.xml"],
|
|
})).rejects.toThrow(/commandIds|too small|at least 1/i);
|
|
});
|
|
|
|
describe("release evidence fixture copy", () => {
|
|
it("keeps the release evidence and leaves the regenerated trees behind", async () => {
|
|
const { copyReleaseEvidenceTree, RELEASE_EVIDENCE_REGENERATED_TREES } =
|
|
await import("../../scripts/lib/removal-fixture.ts");
|
|
const { RELEASE_CANDIDATE_EVIDENCE_PATHS } = await import(
|
|
"../../scripts/lib/release-candidate.ts"
|
|
);
|
|
const { mkdtemp, access, readdir } = await import("node:fs/promises");
|
|
const { tmpdir } = await import("node:os");
|
|
const nodePath = (await import("node:path")).default;
|
|
|
|
const root = await mkdtemp(nodePath.join(tmpdir(), "release-evidence-"));
|
|
await copyReleaseEvidenceTree(process.cwd(), root);
|
|
|
|
// Every artifact a candidate archive is assembled from has to survive; a
|
|
// fixture missing one of these cannot build a candidate at all, and every
|
|
// provider suite then fails while constructing its own fixture.
|
|
for (const evidence of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
|
|
if (!evidence.startsWith("artifacts/")) continue;
|
|
await expect(access(nodePath.join(root, evidence)), evidence).resolves.toBeUndefined();
|
|
}
|
|
// The regenerated trees are why this is a filter and not a plain copy: they
|
|
// are tens of megabytes of traces and coverage HTML. They still exist,
|
|
// because the repository inventory expects the directories.
|
|
for (const tree of RELEASE_EVIDENCE_REGENERATED_TREES) {
|
|
await expect(access(nodePath.join(root, tree)), tree).resolves.toBeUndefined();
|
|
await expect(readdir(nodePath.join(root, tree)), tree).resolves.toEqual([]);
|
|
}
|
|
});
|
|
});
|