The release-evidence test asserted that every artifact a candidate is assembled from survives the copy, which quietly assumed the checkout had already run the release chain. It holds in this repository and fails in a product repository that has not, where `artifacts/performance/bundle.json` simply does not exist yet — a fact about the checkout, not about the copier. It now asserts that whatever release evidence is present is preserved, and that at least one thing was, so it cannot pass by finding nothing to check. Vitest also walked `.worktrees/`. A git worktree inside the repository is a different checkout of a different branch; running its tests against this checkout's config produces failures that belong to neither and cost real time to attribute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
135 lines
5.9 KiB
TypeScript
135 lines
5.9 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 release artifact that exists here has to survive the copy; a
|
|
// fixture missing one cannot build a candidate at all, and every provider
|
|
// suite then fails while constructing its own fixture.
|
|
//
|
|
// Which ones exist depends on what this checkout has generated — a product
|
|
// repository that has not run the release chain has fewer than the template
|
|
// does — so the subject is preservation, not the presence of a full chain.
|
|
// Requiring at least one keeps that from quietly asserting nothing.
|
|
let preserved = 0;
|
|
for (const evidence of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
|
|
if (!evidence.startsWith("artifacts/")) continue;
|
|
try {
|
|
await access(evidence);
|
|
} catch {
|
|
continue;
|
|
}
|
|
await expect(access(nodePath.join(root, evidence)), evidence).resolves.toBeUndefined();
|
|
preserved += 1;
|
|
}
|
|
expect(preserved).toBeGreaterThan(0);
|
|
// 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([]);
|
|
}
|
|
});
|
|
});
|