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([]); } }); });