From 4090c8681d07c9c0d4d834b45a4bb603d4201d5f Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Wed, 19 Aug 2026 14:07:40 +0900 Subject: [PATCH] test: gate the dev release manifest against the compiled contract set Nothing in the gate set read `public/*.json`. Every static gate passed and the whole suite passed while `pnpm dev` rendered the boot-error screen instead of the app, which is the only reason the drift survived two contract changes. `check:dev-release-manifest` compares the fixture's `setAlgorithm`, `setDigest` and package set against what `generate-contract-set.ts` composes, and is registered on FE-GATE-010 next to `check-tech-log-contract` so CI executes it. Unlike the refresh wired into contract generation, this observes the composed set directly, so it also catches a contribution added to or removed from `installed-contract-contributions.ts`. `CANONICAL_GATE_SHAPE_SHA256` recomputed by hand, as always: the committed constant 98d19911... was first reproduced from the committed `gates.json` with an independent transcription of `canonicalGateShapeSha256`, and only then was b4096244... hashed from the new one. Command counts move 84/96 -> 85/97; artifacts stay at 130 because the gate publishes no evidence file, matching `check-tech-log-contract`. Co-Authored-By: Claude Opus 5 (1M context) --- config/ci/gates.json | 6 ++ scripts/contracts/ci-gates.ts | 13 ++- tests/unit/ci-workflow-generation.test.ts | 6 +- .../dev-release-manifest-contract-set.test.ts | 82 +++++++++++++++++++ .../unit/task3-selective-integration.test.ts | 7 +- 5 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 tests/unit/dev-release-manifest-contract-set.test.ts 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);