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) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-19 14:07:40 +09:00
co-authored by Claude Opus 5
parent b75c9d0956
commit 4090c8681d
5 changed files with 106 additions and 8 deletions
+4 -2
View File
@@ -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.
@@ -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);
});
});
@@ -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);