diff --git a/config/ci/gates.json b/config/ci/gates.json index c3b8032..6bf78f8 100644 --- a/config/ci/gates.json +++ b/config/ci/gates.json @@ -333,6 +333,11 @@ "script": "check:tech-log-contract", "expect": "pass" }, + { + "id": "check-dev-release-manifest", + "script": "check:dev-release-manifest", + "expect": "pass" + }, { "id": "build", "script": "build", @@ -1972,6 +1977,7 @@ "check-registries-fixture", "check-routes-fixture", "check-tech-log-contract", + "check-dev-release-manifest", "check-ci" ], "logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt", diff --git a/scripts/contracts/ci-gates.ts b/scripts/contracts/ci-gates.ts index 9d11ae2..2b21f31 100644 --- a/scripts/contracts/ci-gates.ts +++ b/scripts/contracts/ci-gates.ts @@ -456,7 +456,10 @@ const CANONICAL_GATE_SHAPE_SHA256 = // `test-tech-log` and its junit evidence, by the same method — the previous // constant f3cc9075… was reproduced from the previous gates.json first, so // the transcription that produced this value is known to be the real one. - "98d19911c37a18f579fe51b3e3b8164ff0515579557512bc4040b407d4399be9"; + // Dev release manifest drift fix, item 2: recomputed again after FE-GATE-010 + // gained `check-dev-release-manifest`. Same method — 98d19911… was first + // reproduced from the previous gates.json before this value was hashed. + "b40962448e617883060e09eb7183837cfea6833519f435f11713efd083210dbe"; function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string { const normalized = gates.map( @@ -497,9 +500,13 @@ function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[] // already ran inside `test:coverage`'s combined vitest invocation, so a // TechLog failure was reported as a coverage-gate failure with no junit of // its own to name it. - if (contract.commands.length !== 84 || commandReferenceCount !== 96) { + // Dev release manifest drift fix, item 2: FE-GATE-010 gained + // `check-dev-release-manifest`. No gate read `public/*.json` at all, so a + // fixture that did not declare the compiled contract set broke `pnpm dev` + // outright while every static gate stayed green. + if (contract.commands.length !== 85 || commandReferenceCount !== 97) { failures.push( - `command authority baseline must contain exactly 84 definitions and 96 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`, + `command authority baseline must contain exactly 85 definitions and 97 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`, ); } // Template merge. 126 product artifacts plus the two the template added. diff --git a/tests/unit/ci-workflow-generation.test.ts b/tests/unit/ci-workflow-generation.test.ts index 0f81f8d..88e41c5 100644 --- a/tests/unit/ci-workflow-generation.test.ts +++ b/tests/unit/ci-workflow-generation.test.ts @@ -191,8 +191,10 @@ describe("CI gate contract", () => { expect(contract.jobs).toHaveLength(9); // Final fix wave item 1: FE-GATE-010 gained `check-tech-log-contract`. // Alignment follow-up item 2: FE-GATE-007 gained `test-tech-log`. - expect(contract.commands).toHaveLength(84); - expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(96); + // Dev release manifest drift fix item 2: FE-GATE-010 gained + // `check-dev-release-manifest`. + expect(contract.commands).toHaveLength(85); + expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(97); expect(contract.commands.filter(({ expect }) => expect === "fail")).toHaveLength(23); // Template merge. Recounted from the merged config/ci/gates.json rather // than taking either side's number. diff --git a/tests/unit/dev-release-manifest-contract-set.test.ts b/tests/unit/dev-release-manifest-contract-set.test.ts new file mode 100644 index 0000000..ddc3b89 --- /dev/null +++ b/tests/unit/dev-release-manifest-contract-set.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; + +import { + DEV_RELEASE_MANIFEST_PATH, + checkDevReleaseManifestContractSet, + compareDevReleaseManifestContractSet, + expectedDevReleaseManifestContractSet, + readDevReleaseManifest, +} from "../../scripts/lib/dev-release-manifest.ts"; + +/** + * `corepack pnpm dev` serves `public/release-manifest.json` verbatim and + * `verifyContractSet` runs unconditionally at boot, so a fixture that does not + * declare the compiled contract set is a hard `pnpm dev` boot failure in the + * default `MOCK` mode. Nothing else in the gate set reads `public/*.json`. + */ +describe("dev release manifest contract set", () => { + it("declares exactly the contract set the build compiles", async () => { + await expect(checkDevReleaseManifestContractSet()).resolves.toEqual([]); + }); + + it("keeps the fixture algorithm and digest identical to the compiled set", async () => { + const document = await readDevReleaseManifest(); + const expected = expectedDevReleaseManifestContractSet(); + expect(document["contractSet"]).toEqual(expected); + expect(expected.packages.length).toBeGreaterThan(0); + }); + + it("reports the missing package that fails boot closed", () => { + const expected = expectedDevReleaseManifestContractSet(); + const failures = compareDevReleaseManifestContractSet( + { ...expected, packages: [] }, + expected, + ); + expect(failures.join("\n")).toMatch(/CONTRACT_SET_PACKAGE_MISSING/u); + }); + + it("reports a stale set digest", () => { + const expected = expectedDevReleaseManifestContractSet(); + const failures = compareDevReleaseManifestContractSet( + { ...expected, setDigest: `sha256:${"a".repeat(64)}` }, + expected, + ); + expect(failures.join("\n")).toMatch(/setDigest drift/u); + }); + + it("reports a package the manifest declares but the build never compiled", () => { + const expected = expectedDevReleaseManifestContractSet(); + const stranger = { + packageId: "@tech-log/not-installed", + version: "1.0.0", + digest: `sha256:${"0".repeat(64)}` as const, + runtimeProtocolVersion: 1 as const, + sourceRevision: "abcdef0", + }; + const failures = compareDevReleaseManifestContractSet( + { ...expected, packages: [...expected.packages, stranger] }, + expected, + ); + expect(failures.join("\n")).toMatch(/CONTRACT_SET_PACKAGE_UNEXPECTED/u); + }); + + it("reports a stale version left behind by a regenerated contract", () => { + const expected = expectedDevReleaseManifestContractSet(); + const [first] = expected.packages; + expect(first).toBeDefined(); + const failures = compareDevReleaseManifestContractSet( + { ...expected, packages: [{ ...first!, version: "999.0.0" }] }, + expected, + ); + expect(failures.join("\n")).toMatch(/package drift for/u); + }); + + it("rejects a fixture whose contractSet block is absent altogether", () => { + const failures = compareDevReleaseManifestContractSet( + undefined, + expectedDevReleaseManifestContractSet(), + ); + expect(failures).toHaveLength(1); + expect(failures[0]).toContain(DEV_RELEASE_MANIFEST_PATH); + }); +}); diff --git a/tests/unit/task3-selective-integration.test.ts b/tests/unit/task3-selective-integration.test.ts index 1877493..fb11579 100644 --- a/tests/unit/task3-selective-integration.test.ts +++ b/tests/unit/task3-selective-integration.test.ts @@ -187,10 +187,11 @@ describe("selective Task 3 contract closure", () => { // accessibility evidence, bringing the total to 129. // Final fix wave item 1 added `check-tech-log-contract` to FE-GATE-010. // Alignment follow-up item 2 added `test-tech-log` and its junit report to - // FE-GATE-007, bringing the totals to 84/96/130. + // FE-GATE-007, bringing the totals to 84/96/130. The dev release manifest + // drift fix added `check-dev-release-manifest` to FE-GATE-010: 85/97/130. expect(canonical.gates).toHaveLength(27); - expect(canonical.commands).toHaveLength(84); - expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(96); + expect(canonical.commands).toHaveLength(85); + expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(97); expect(canonical.artifacts).toHaveLength(130); expect(canonical.stages).toHaveLength(5); expect(canonical.retention.classes).toHaveLength(5);