Compare commits
3
Commits
d84b57bb3f
...
68c5dbdaa3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68c5dbdaa3 | ||
|
|
4090c8681d | ||
|
|
b75c9d0956 |
@@ -78,13 +78,36 @@ The switch is a field on the versioned runtime config document
|
|||||||
`public/config.json` (and `public/release-manifest.json`) verbatim as dev
|
`public/config.json` (and `public/release-manifest.json`) verbatim as dev
|
||||||
fixtures — editing `config/runtime/local.json` alone has no effect on
|
fixtures — editing `config/runtime/local.json` alone has no effect on
|
||||||
`pnpm dev`. To exercise `HTTP` mode under `pnpm dev`, set
|
`pnpm dev`. To exercise `HTTP` mode under `pnpm dev`, set
|
||||||
`TECH_LOG_STUDIO_SOURCE` in `public/config.json` directly. Switching to
|
`TECH_LOG_STUDIO_SOURCE` in `public/config.json` directly.
|
||||||
`HTTP` also requires `public/release-manifest.json`'s `contractSet` to
|
|
||||||
declare the `@tech-log/studio-contract` package the build compiled in
|
### The dev release manifest must declare the compiled contract set
|
||||||
(`.generated/frontend-runtime/contract-set.ts` after a build), or boot fails
|
|
||||||
closed earlier, at contract-set verification (`CONTRACT_SET_PACKAGE_MISSING`)
|
Contract-set verification runs unconditionally at boot, before any adapter is
|
||||||
— itself a graceful, non-blank error screen, just not the one this switch is
|
selected. It is not an `HTTP`-mode caveat: if `public/release-manifest.json`'s
|
||||||
usually used to exercise.
|
`contractSet` does not match the set the build compiled, `corepack pnpm dev`
|
||||||
|
does not start the app at all — it renders the fail-closed boot screen
|
||||||
|
(`CONTRACT_SET_MISMATCH` / `CONTRACT_SET_PACKAGE_MISSING`) in the **default
|
||||||
|
`MOCK` mode** too. A developer who runs `pnpm dev` and gets a boot error has a
|
||||||
|
broken dev server, however tidy the screen looks; treat it as a defect in the
|
||||||
|
fixture, never as expected behaviour.
|
||||||
|
|
||||||
|
The expectation comes from `EXPECTED_CONTRACT_SET_PACKAGES`
|
||||||
|
(`src/features/installed-contract-contributions.ts`), and a real build writes it
|
||||||
|
into `dist/release-manifest.json` from `scripts/generate-contract-set.ts`, so
|
||||||
|
production builds are always self-consistent. Only the hand-maintained dev
|
||||||
|
fixture can drift, and it drifts whenever either half moves — a regenerated
|
||||||
|
contract (new package digest or version) or a contribution added to or removed
|
||||||
|
from `installed-contract-contributions.ts`. Two things keep it honest:
|
||||||
|
|
||||||
|
- `corepack pnpm generate:tech-log-contract` refreshes the fixture's
|
||||||
|
`contractSet` block as its last step, so regenerating the contract can never
|
||||||
|
leave the two out of step. `corepack pnpm generate:dev-release-manifest`
|
||||||
|
refreshes the same block on its own, for the contribution-list case that does
|
||||||
|
not go through contract generation.
|
||||||
|
- `corepack pnpm check:dev-release-manifest` is the gate. It compares the
|
||||||
|
fixture's `setAlgorithm`, `setDigest` and package set against the compiled
|
||||||
|
set and fails on any difference. It runs in CI as part of FE-GATE-010, which
|
||||||
|
is what catches the changes the generation step cannot see.
|
||||||
|
|
||||||
### TechLog contract generation
|
### TechLog contract generation
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -87,6 +87,8 @@
|
|||||||
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
|
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
|
||||||
"generate:tech-log-contract": "node scripts/generate-tech-log-contract.ts",
|
"generate:tech-log-contract": "node scripts/generate-tech-log-contract.ts",
|
||||||
"check:tech-log-contract": "node scripts/generate-tech-log-contract.ts --check",
|
"check:tech-log-contract": "node scripts/generate-tech-log-contract.ts --check",
|
||||||
|
"generate:dev-release-manifest": "node scripts/check-dev-release-manifest.ts --write",
|
||||||
|
"check:dev-release-manifest": "node scripts/check-dev-release-manifest.ts",
|
||||||
"generate:supply-chain": "node scripts/generate-supply-chain.ts",
|
"generate:supply-chain": "node scripts/generate-supply-chain.ts",
|
||||||
"verify:local-evidence": "node scripts/verify-release-candidate.ts && node scripts/verify-release.ts && node scripts/verify-supply-chain-artifacts.ts && node scripts/verify-archived-local-evidence.ts && node scripts/verify-release-candidate.ts",
|
"verify:local-evidence": "node scripts/verify-release-candidate.ts && node scripts/verify-release.ts && node scripts/verify-supply-chain-artifacts.ts && node scripts/verify-archived-local-evidence.ts && node scripts/verify-release-candidate.ts",
|
||||||
"verify:promotion": "node scripts/verify-exact-promotion-bundle.ts",
|
"verify:promotion": "node scripts/verify-exact-promotion-bundle.ts",
|
||||||
|
|||||||
@@ -38,7 +38,15 @@
|
|||||||
},
|
},
|
||||||
"contractSet": {
|
"contractSet": {
|
||||||
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
"setAlgorithm": "CA_CONTRACT_SET_V1",
|
||||||
"setDigest": "sha256:ad6aab71fea6a9ff87cbd170b984b339965afc90d85bb57f87801c9e0c020da2",
|
"setDigest": "sha256:e0da77655f51592ece583826d5fc6b092f57dd2bf63307e45e7e77283e6bf437",
|
||||||
"packages": []
|
"packages": [
|
||||||
|
{
|
||||||
|
"packageId": "@tech-log/studio-contract",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"digest": "sha256:99f54f56ea0c582eafdbdf9be5653e3384bef0a1b08bff67f3147ee0292019ea",
|
||||||
|
"runtimeProtocolVersion": 1,
|
||||||
|
"sourceRevision": "ce2e748"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Gate: the hand-maintained dev fixture `public/release-manifest.json` must
|
||||||
|
* declare the same `contractSet` the build compiles.
|
||||||
|
*
|
||||||
|
* `corepack pnpm dev` serves that file verbatim, and `verifyContractSet` runs
|
||||||
|
* unconditionally at boot, so a stale fixture is a hard boot failure of
|
||||||
|
* `pnpm dev` in the default `MOCK` mode — not an `HTTP`-mode caveat. Nothing
|
||||||
|
* else in the gate set reads `public/*.json`, which is how a completely broken
|
||||||
|
* `pnpm dev` shipped with every static check green.
|
||||||
|
*
|
||||||
|
* `--write` refreshes the block instead of failing; that is what
|
||||||
|
* `corepack pnpm generate:tech-log-contract` calls.
|
||||||
|
*/
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEV_RELEASE_MANIFEST_PATH,
|
||||||
|
checkDevReleaseManifestContractSet,
|
||||||
|
refreshDevReleaseManifestContractSet,
|
||||||
|
} from "./lib/dev-release-manifest.ts";
|
||||||
|
|
||||||
|
const write = process.argv.includes("--write");
|
||||||
|
|
||||||
|
if (write) {
|
||||||
|
const { changed, contractSet } = await refreshDevReleaseManifestContractSet();
|
||||||
|
process.stdout.write(
|
||||||
|
`${changed ? "Updated" : "Already in sync"}: ${DEV_RELEASE_MANIFEST_PATH} contractSet ` +
|
||||||
|
`(${contractSet.packages.length} package(s), ${contractSet.setDigest})\n`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const failures = await checkDevReleaseManifestContractSet();
|
||||||
|
if (failures.length > 0) {
|
||||||
|
process.stderr.write(
|
||||||
|
`dev release manifest drift:\n- ${failures.join("\n- ")}\n` +
|
||||||
|
`Run: corepack pnpm generate:dev-release-manifest\n`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.stdout.write(
|
||||||
|
`${DEV_RELEASE_MANIFEST_PATH} contractSet matches the compiled contract set.\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -121,3 +121,21 @@ writeFileSync(SOURCE_RECORD, `${JSON.stringify(record, null, 2)}\n`);
|
|||||||
console.log(
|
console.log(
|
||||||
`Generated from ${record.packageId}@${record.version} (${record.sourceRevision}), ${record.operationIds.length} operations.`,
|
`Generated from ${record.packageId}@${record.version} (${record.sourceRevision}), ${record.operationIds.length} operations.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 재생성은 매번 package digest를 바꾼다. `pnpm dev`가 그대로 서빙하는
|
||||||
|
// `public/release-manifest.json`은 build가 컴파일한 contract set을 그대로
|
||||||
|
// 선언해야 하고(§5.5, `verifyContractSet`는 MOCK 모드에서도 무조건 돈다),
|
||||||
|
// 그러지 않으면 dev 부팅이 CONTRACT_SET_PACKAGE_MISSING으로 닫힌다.
|
||||||
|
// 방금 쓴 canonical-source.json을 읽어야 하므로 정적 import가 아닌 동적
|
||||||
|
// import로 불러온다.
|
||||||
|
const { refreshDevReleaseManifestContractSet, DEV_RELEASE_MANIFEST_PATH } =
|
||||||
|
await import("./lib/dev-release-manifest.ts");
|
||||||
|
const refreshed = await refreshDevReleaseManifestContractSet();
|
||||||
|
console.log(
|
||||||
|
`${refreshed.changed ? "Updated" : "Already in sync"}: ${DEV_RELEASE_MANIFEST_PATH} contractSet ` +
|
||||||
|
`(${refreshed.contractSet.packages.length} package(s), ${refreshed.contractSet.setDigest})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// contract를 다시 만들지 않아도 구성된 set은 바뀔 수 있다
|
||||||
|
// (`installed-contract-contributions.ts`에 기여가 추가/제거되는 경우).
|
||||||
|
// 그 경로는 여기서 못 잡으므로 `check:dev-release-manifest` 게이트가 잡는다.
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { buildContractSet } from "../generate-contract-set.ts";
|
||||||
|
import { contractSetSchema, type ContractSet } from "../../src/contracts/contract-set.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `corepack pnpm dev` never runs the build pipeline: plain `vite` serves
|
||||||
|
* `public/release-manifest.json` verbatim. That fixture therefore has to carry
|
||||||
|
* the same `contractSet` block `scripts/generate-build-manifest.ts` writes into
|
||||||
|
* `dist/release-manifest.json`, because `verifyContractSet` runs unconditionally
|
||||||
|
* at boot — in the default `MOCK` mode as much as in `HTTP` mode. When the two
|
||||||
|
* disagree the app renders the fail-closed boot screen instead of the shell.
|
||||||
|
*
|
||||||
|
* Both halves of the answer live here so they cannot disagree: the generation
|
||||||
|
* step rewrites the block, and the CI gate compares it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DEV_RELEASE_MANIFEST_PATH = "public/release-manifest.json";
|
||||||
|
|
||||||
|
export type DevReleaseManifestContractSet = ContractSet;
|
||||||
|
|
||||||
|
function resolveManifestPath(root: string): string {
|
||||||
|
return path.join(root, DEV_RELEASE_MANIFEST_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contract set the build would compile, derived from
|
||||||
|
* `EXPECTED_CONTRACT_SET_PACKAGES` exactly as `generate-build-manifest.ts`
|
||||||
|
* derives it.
|
||||||
|
*/
|
||||||
|
export function expectedDevReleaseManifestContractSet(): DevReleaseManifestContractSet {
|
||||||
|
return contractSetSchema.parse(
|
||||||
|
JSON.parse(JSON.stringify(buildContractSet())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares one manifest `contractSet` block against the compiled expectation.
|
||||||
|
* Returns one human-readable line per disagreement; an empty array means the
|
||||||
|
* fixture would boot.
|
||||||
|
*/
|
||||||
|
export function compareDevReleaseManifestContractSet(
|
||||||
|
actual: unknown,
|
||||||
|
expected: DevReleaseManifestContractSet,
|
||||||
|
): string[] {
|
||||||
|
const parsed = contractSetSchema.safeParse(actual);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return [
|
||||||
|
`${DEV_RELEASE_MANIFEST_PATH} contractSet is missing or not a valid contract set: ${parsed.error.issues
|
||||||
|
.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`)
|
||||||
|
.join("; ")}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const manifest = parsed.data;
|
||||||
|
const failures: string[] = [];
|
||||||
|
if (manifest.setAlgorithm !== expected.setAlgorithm) {
|
||||||
|
failures.push(
|
||||||
|
`setAlgorithm drift: manifest ${manifest.setAlgorithm}, build ${expected.setAlgorithm}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (manifest.setDigest !== expected.setDigest) {
|
||||||
|
failures.push(
|
||||||
|
`setDigest drift: manifest ${manifest.setDigest}, build ${expected.setDigest}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const manifestById = new Map(
|
||||||
|
manifest.packages.map((entry) => [entry.packageId, entry] as const),
|
||||||
|
);
|
||||||
|
const expectedById = new Map(
|
||||||
|
expected.packages.map((entry) => [entry.packageId, entry] as const),
|
||||||
|
);
|
||||||
|
for (const [packageId, entry] of expectedById) {
|
||||||
|
const declared = manifestById.get(packageId);
|
||||||
|
if (!declared) {
|
||||||
|
failures.push(
|
||||||
|
`package missing from the manifest (boot fails with CONTRACT_SET_PACKAGE_MISSING): ${packageId}@${entry.version}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (JSON.stringify(declared) !== JSON.stringify(entry)) {
|
||||||
|
failures.push(
|
||||||
|
`package drift for ${packageId}: manifest ${JSON.stringify(declared)}, build ${JSON.stringify(entry)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const packageId of manifestById.keys()) {
|
||||||
|
if (!expectedById.has(packageId)) {
|
||||||
|
failures.push(
|
||||||
|
`package declared by the manifest but not compiled into the build (boot fails with CONTRACT_SET_PACKAGE_UNEXPECTED): ${packageId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readDevReleaseManifest(
|
||||||
|
root: string = process.cwd(),
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const raw = await readFile(resolveManifestPath(root), "utf8");
|
||||||
|
const document: unknown = JSON.parse(raw);
|
||||||
|
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
||||||
|
throw new TypeError(`${DEV_RELEASE_MANIFEST_PATH} must be a JSON object`);
|
||||||
|
}
|
||||||
|
return document as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkDevReleaseManifestContractSet(
|
||||||
|
root: string = process.cwd(),
|
||||||
|
): Promise<readonly string[]> {
|
||||||
|
const document = await readDevReleaseManifest(root);
|
||||||
|
return compareDevReleaseManifestContractSet(
|
||||||
|
document["contractSet"],
|
||||||
|
expectedDevReleaseManifestContractSet(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrites only the `contractSet` block. Every other field of the hand
|
||||||
|
* maintained fixture is preserved byte for byte by round-tripping the same
|
||||||
|
* two-space JSON encoding the file already uses.
|
||||||
|
*/
|
||||||
|
export async function refreshDevReleaseManifestContractSet(
|
||||||
|
root: string = process.cwd(),
|
||||||
|
): Promise<Readonly<{ changed: boolean; contractSet: DevReleaseManifestContractSet }>> {
|
||||||
|
const target = resolveManifestPath(root);
|
||||||
|
const document = await readDevReleaseManifest(root);
|
||||||
|
const contractSet = expectedDevReleaseManifestContractSet();
|
||||||
|
const previous = JSON.stringify(document["contractSet"]);
|
||||||
|
document["contractSet"] = contractSet;
|
||||||
|
const next = `${JSON.stringify(document, null, 2)}\n`;
|
||||||
|
const changed = previous !== JSON.stringify(contractSet);
|
||||||
|
if (changed) await writeFile(target, next, "utf8");
|
||||||
|
return Object.freeze({ changed, contractSet });
|
||||||
|
}
|
||||||
@@ -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