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:
co-authored by
Claude Opus 5
parent
b75c9d0956
commit
4090c8681d
@@ -333,6 +333,11 @@
|
|||||||
"script": "check:tech-log-contract",
|
"script": "check:tech-log-contract",
|
||||||
"expect": "pass"
|
"expect": "pass"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "check-dev-release-manifest",
|
||||||
|
"script": "check:dev-release-manifest",
|
||||||
|
"expect": "pass"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "build",
|
"id": "build",
|
||||||
"script": "build",
|
"script": "build",
|
||||||
@@ -1972,6 +1977,7 @@
|
|||||||
"check-registries-fixture",
|
"check-registries-fixture",
|
||||||
"check-routes-fixture",
|
"check-routes-fixture",
|
||||||
"check-tech-log-contract",
|
"check-tech-log-contract",
|
||||||
|
"check-dev-release-manifest",
|
||||||
"check-ci"
|
"check-ci"
|
||||||
],
|
],
|
||||||
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
|
"logArtifactId": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
|
||||||
|
|||||||
@@ -456,7 +456,10 @@ const CANONICAL_GATE_SHAPE_SHA256 =
|
|||||||
// `test-tech-log` and its junit evidence, by the same method — the previous
|
// `test-tech-log` and its junit evidence, by the same method — the previous
|
||||||
// constant f3cc9075… was reproduced from the previous gates.json first, so
|
// constant f3cc9075… was reproduced from the previous gates.json first, so
|
||||||
// the transcription that produced this value is known to be the real one.
|
// 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 {
|
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
|
||||||
const normalized = gates.map(
|
const normalized = gates.map(
|
||||||
@@ -497,9 +500,13 @@ function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[]
|
|||||||
// already ran inside `test:coverage`'s combined vitest invocation, so a
|
// already ran inside `test:coverage`'s combined vitest invocation, so a
|
||||||
// TechLog failure was reported as a coverage-gate failure with no junit of
|
// TechLog failure was reported as a coverage-gate failure with no junit of
|
||||||
// its own to name it.
|
// 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(
|
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.
|
// Template merge. 126 product artifacts plus the two the template added.
|
||||||
|
|||||||
@@ -191,8 +191,10 @@ describe("CI gate contract", () => {
|
|||||||
expect(contract.jobs).toHaveLength(9);
|
expect(contract.jobs).toHaveLength(9);
|
||||||
// Final fix wave item 1: FE-GATE-010 gained `check-tech-log-contract`.
|
// 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`.
|
// Alignment follow-up item 2: FE-GATE-007 gained `test-tech-log`.
|
||||||
expect(contract.commands).toHaveLength(84);
|
// Dev release manifest drift fix item 2: FE-GATE-010 gained
|
||||||
expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(96);
|
// `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);
|
expect(contract.commands.filter(({ expect }) => expect === "fail")).toHaveLength(23);
|
||||||
// Template merge. Recounted from the merged config/ci/gates.json rather
|
// Template merge. Recounted from the merged config/ci/gates.json rather
|
||||||
// than taking either side's number.
|
// 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.
|
// accessibility evidence, bringing the total to 129.
|
||||||
// Final fix wave item 1 added `check-tech-log-contract` to FE-GATE-010.
|
// 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
|
// 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.gates).toHaveLength(27);
|
||||||
expect(canonical.commands).toHaveLength(84);
|
expect(canonical.commands).toHaveLength(85);
|
||||||
expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(96);
|
expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(97);
|
||||||
expect(canonical.artifacts).toHaveLength(130);
|
expect(canonical.artifacts).toHaveLength(130);
|
||||||
expect(canonical.stages).toHaveLength(5);
|
expect(canonical.stages).toHaveLength(5);
|
||||||
expect(canonical.retention.classes).toHaveLength(5);
|
expect(canonical.retention.classes).toHaveLength(5);
|
||||||
|
|||||||
Reference in New Issue
Block a user