`PublicContentQueries` returned arrays, not promises. That signature is only implementable by something already in memory, so the port could hold exactly one adapter — the bundled fixture — and no amount of configuration could put the public site on the backend. Turning it async is the change that makes a second adapter possible; the adapter itself follows. The markup is untouched. Every page reads a value and hands it to a presentational component, so the shape those components receive is mapped at the adapter boundary and nothing below the page changes. Screens load through one query, not one per read. Several pages read in a loop — the home timeline walks every project for its activity, the explore filter walks search results to resolve titles — and a hook per read would mean a variable number of hooks per render, which React forbids. `usePublicContent` takes the whole screen's reads as one loader, where a loop is a loop and `Promise.all` is available; the loops that used to be N sequential lookups now issue together. Two places deliberately do not show the loading surface. The explore filter sits inside a page that already renders one, so a second skeleton would move the layout under it — it keeps its structure and fills its options in when they arrive. The search dialog is a type-ahead: re-querying per keystroke would replace the results with a skeleton on every key, so it loads the catalog once and applies the same predicate locally. `usePublicContent` requires an object because `undefined` is how the query layer says "no result yet". A loader returning the record itself would make a missing slug indistinguishable from a request in flight, and the page would sit on a skeleton instead of rendering its not-found route. Studio's `resolvePublishedLabel` stays synchronous. It is called from inside the public renderer, so making it async would push awaits through the render tree; the shell loads the catalog once and the callback remains a lookup. The component tests now assemble the query providers the running app assembles. Without them the render throws "No QueryClient set" — not a harness quirk, but the same failure the app would produce if it were mounted without its query layer.
374 lines
11 KiB
TypeScript
374 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { verifyRollbackReleaseCoherence } from "../../scripts/drill-runbook.ts";
|
|
import { verifyReleaseArtifactsCoherence } from "../../scripts/verify-release.ts";
|
|
import type { InstalledContractPackageIdentity } from "../../src/contracts/external-contract-runtime.ts";
|
|
import { computeContractSetDigest } from "../../src/contracts/contract-set-canonical.ts";
|
|
import type {
|
|
ReleaseArtifact,
|
|
RuntimeConfigArtifact,
|
|
} from "../../src/contracts/release-artifacts.ts";
|
|
import {
|
|
compareReleaseToRuntime,
|
|
RELEASE_TOKEN_REGISTRY,
|
|
} from "../../src/contracts/release-tokens.ts";
|
|
|
|
const contractPackages = [
|
|
{
|
|
packageId: "@example/accounts",
|
|
version: "1.2.3",
|
|
digest: `sha256:${"1".repeat(64)}`,
|
|
runtimeProtocolVersion: 1,
|
|
sourceRevision: "a".repeat(40),
|
|
},
|
|
{
|
|
packageId: "@example/billing",
|
|
version: "2.3.4",
|
|
digest: `sha256:${"2".repeat(64)}`,
|
|
runtimeProtocolVersion: 1,
|
|
sourceRevision: "b".repeat(40),
|
|
},
|
|
] as const satisfies readonly InstalledContractPackageIdentity[];
|
|
|
|
const releaseV1 = {
|
|
schemaVersion: 1,
|
|
appVersion: "1.0.0",
|
|
buildId: "build-a",
|
|
commitSha: "abc1234",
|
|
configSchemaVersion: "1",
|
|
apiContractVersion: "1.4.0",
|
|
assetManifestHash: "assets-a",
|
|
releaseId: "release-a",
|
|
builtAt: "2026-08-01T00:00:00.000Z",
|
|
routeChunks: {},
|
|
} as const satisfies ReleaseArtifact;
|
|
|
|
const runtimeV1 = {
|
|
APP_ENV: "local",
|
|
API_BASE_URL: "http://localhost:8080/",
|
|
REQUEST_TIMEOUT_MS: 10_000,
|
|
MAX_RETRY_ATTEMPTS: 2,
|
|
TELEMETRY_ENABLED: false,
|
|
AUTH_MODE: "external",
|
|
CONFIG_SCHEMA_VERSION: "1",
|
|
API_CONTRACT_VERSION: "1.6.0",
|
|
RELEASE_MANIFEST_URL: "/release-manifest.json",
|
|
BUILD_ID: "build-a",
|
|
RELEASE_ID: "release-a",
|
|
} as const satisfies RuntimeConfigArtifact;
|
|
|
|
const runtimeV2 = {
|
|
APP_ENV: "local",
|
|
API_BASE_URL: "http://localhost:8080/",
|
|
REQUEST_TIMEOUT_MS: 10_000,
|
|
MAX_RETRY_ATTEMPTS: 2,
|
|
TELEMETRY_ENABLED: false,
|
|
AUTH_MODE: "external",
|
|
CONFIG_SCHEMA_VERSION: "2.0",
|
|
RELEASE_MANIFEST_URL: "/release-manifest.json",
|
|
BUILD_ID: "build-a",
|
|
RELEASE_ID: "release-a",
|
|
CAPABILITY_OVERRIDES: {
|
|
REALTIME: "DEFAULT",
|
|
WEB_WORKER: "DEFAULT",
|
|
SERVICE_WORKER: "DEFAULT",
|
|
OFFLINE_COMMANDS: "DEFAULT",
|
|
},
|
|
FEATURE_OVERRIDES: {},
|
|
TECH_LOG_STUDIO_SOURCE: "MOCK",
|
|
TECH_LOG_PUBLIC_SOURCE: "MOCK",
|
|
} as const satisfies RuntimeConfigArtifact;
|
|
|
|
async function releaseV2With(
|
|
packages: readonly InstalledContractPackageIdentity[],
|
|
setDigest?: `sha256:${string}`,
|
|
): Promise<ReleaseArtifact> {
|
|
return {
|
|
schemaVersion: 2,
|
|
appVersion: "1.0.0",
|
|
buildId: "build-a",
|
|
commitSha: "abc1234",
|
|
configSchemaVersion: "2.0",
|
|
assetManifestHash: "assets-a",
|
|
releaseId: "release-a",
|
|
builtAt: "2026-08-01T00:00:00.000Z",
|
|
routeChunks: {},
|
|
contractSet: {
|
|
setAlgorithm: "CA_CONTRACT_SET_V1",
|
|
setDigest: setDigest ?? (await computeContractSetDigest(packages)),
|
|
packages: [...packages],
|
|
},
|
|
};
|
|
}
|
|
|
|
function artifactReader(
|
|
entries: Readonly<Record<string, unknown | Error>>,
|
|
reads: string[] = [],
|
|
): (path: string) => Promise<unknown> {
|
|
return async (path) => {
|
|
reads.push(path);
|
|
const value = entries[path];
|
|
if (value === undefined) {
|
|
throw Object.assign(new Error(`missing ${path}`), { code: "ENOENT" });
|
|
}
|
|
if (value instanceof Error) throw value;
|
|
return value;
|
|
};
|
|
}
|
|
|
|
describe("release coherence", () => {
|
|
it("owns all nine release tokens and keeps builtAt diagnostic-only", () => {
|
|
// §5.2 adds contractSetDigest beside the legacy apiContractVersion scalar.
|
|
expect(Object.keys(RELEASE_TOKEN_REGISTRY)).toHaveLength(9);
|
|
expect(RELEASE_TOKEN_REGISTRY.contractSetDigest.compatibilityRole).toContain(
|
|
"multi-package",
|
|
);
|
|
expect(RELEASE_TOKEN_REGISTRY.builtAt.compatibilityRole).toContain(
|
|
"never cache identity",
|
|
);
|
|
});
|
|
|
|
it("compares the runtime config to the release structurally", () => {
|
|
const release = {
|
|
buildId: "build-a",
|
|
configSchemaVersion: "1.0",
|
|
apiContractVersion: "1.0",
|
|
assetManifestHash: "assets-a",
|
|
releaseId: "release-a",
|
|
};
|
|
expect(
|
|
compareReleaseToRuntime(release, {
|
|
BUILD_ID: "build-a",
|
|
CONFIG_SCHEMA_VERSION: "1.2",
|
|
API_CONTRACT_VERSION: "1.1",
|
|
RELEASE_ID: "release-a",
|
|
}),
|
|
).toMatchObject({ compatible: true, mismatches: [] });
|
|
});
|
|
|
|
it("rejects HTML-only rollback against a newer runtime config", () => {
|
|
const oldRelease = {
|
|
buildId: "build-old",
|
|
configSchemaVersion: "1.0",
|
|
apiContractVersion: "1.0",
|
|
assetManifestHash: "assets-old",
|
|
releaseId: "release-old",
|
|
};
|
|
expect(
|
|
compareReleaseToRuntime(oldRelease, {
|
|
BUILD_ID: "build-new",
|
|
CONFIG_SCHEMA_VERSION: "2.0",
|
|
API_CONTRACT_VERSION: "2.0",
|
|
RELEASE_ID: "release-new",
|
|
}),
|
|
).toMatchObject({
|
|
compatible: false,
|
|
mismatches: ["buildId", "configSchemaVersion", "apiContractVersion"],
|
|
});
|
|
});
|
|
|
|
it("gives release verification and rollback drills the same V1/V2 tamper verdicts", async () => {
|
|
const packageAdded = [
|
|
...contractPackages,
|
|
{
|
|
packageId: "@example/notifications",
|
|
version: "3.0.0",
|
|
digest: `sha256:${"3".repeat(64)}`,
|
|
runtimeProtocolVersion: 1,
|
|
sourceRevision: "c".repeat(40),
|
|
},
|
|
] as const satisfies readonly InstalledContractPackageIdentity[];
|
|
const packageRemoved = contractPackages.slice(0, 1);
|
|
const versionChanged = [
|
|
{ ...contractPackages[0], version: "1.2.4" },
|
|
contractPackages[1],
|
|
] as const satisfies readonly InstalledContractPackageIdentity[];
|
|
const packageDigestChanged = [
|
|
{
|
|
...contractPackages[0],
|
|
digest: `sha256:${"f".repeat(64)}`,
|
|
},
|
|
contractPackages[1],
|
|
] as const satisfies readonly InstalledContractPackageIdentity[];
|
|
const exactV2 = await releaseV2With(contractPackages);
|
|
const matrix = [
|
|
{
|
|
name: "V1 scalar success",
|
|
release: releaseV1,
|
|
runtime: runtimeV1,
|
|
expectedCompatible: true,
|
|
},
|
|
{
|
|
name: "V1 scalar mismatch",
|
|
release: releaseV1,
|
|
runtime: { ...runtimeV1, API_CONTRACT_VERSION: "2.0.0" },
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 exact package set",
|
|
release: exactV2,
|
|
runtime: runtimeV2,
|
|
expectedCompatible: true,
|
|
},
|
|
{
|
|
name: "V2 exact package set in non-canonical manifest order",
|
|
release: await releaseV2With([...contractPackages].reverse()),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: true,
|
|
},
|
|
{
|
|
name: "V2 package added",
|
|
release: await releaseV2With(packageAdded),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 package removed",
|
|
release: await releaseV2With(packageRemoved),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 package version tampered",
|
|
release: await releaseV2With(versionChanged),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 package digest tampered",
|
|
release: await releaseV2With(packageDigestChanged),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
{
|
|
name: "V2 set digest tampered",
|
|
release: await releaseV2With(
|
|
contractPackages,
|
|
`sha256:${"0".repeat(64)}`,
|
|
),
|
|
runtime: runtimeV2,
|
|
expectedCompatible: false,
|
|
},
|
|
];
|
|
|
|
for (const fixture of matrix) {
|
|
const input = { release: fixture.release, runtime: fixture.runtime };
|
|
const verifierVerdict = (
|
|
await verifyReleaseArtifactsCoherence({
|
|
readArtifact: artifactReader({
|
|
"dist/release-manifest.json": input.release,
|
|
"dist/config.json": input.runtime,
|
|
}),
|
|
contractPackages,
|
|
})
|
|
).coherence.compatible;
|
|
const rollbackDrillVerdict = (
|
|
await verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader({
|
|
"dist/release-manifest.json": fixture.release,
|
|
"dist/config.json": fixture.runtime,
|
|
}),
|
|
contractPackages,
|
|
})
|
|
).coherence.compatible;
|
|
expect(verifierVerdict, `${fixture.name}: verifier`).toBe(
|
|
fixture.expectedCompatible,
|
|
);
|
|
expect(rollbackDrillVerdict, `${fixture.name}: rollback drill`).toBe(
|
|
fixture.expectedCompatible,
|
|
);
|
|
expect(rollbackDrillVerdict, `${fixture.name}: identical verdict`).toBe(
|
|
verifierVerdict,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("falls back to public rollback artifacts only when primary dist is absent", async () => {
|
|
const exactV2 = await releaseV2With(contractPackages);
|
|
const tamperedPrimary = await releaseV2With(contractPackages.slice(0, 1));
|
|
const validPublic = {
|
|
"public/release-manifest.json": exactV2,
|
|
"public/config.json": runtimeV2,
|
|
};
|
|
const unavailablePrimary = [
|
|
new SyntaxError("invalid primary JSON"),
|
|
Object.assign(new Error("unreadable primary"), { code: "EACCES" }),
|
|
{ ...exactV2, unexpected: "tampered" },
|
|
];
|
|
|
|
for (const primaryFailure of unavailablePrimary) {
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader({
|
|
"dist/release-manifest.json": primaryFailure,
|
|
"dist/config.json": runtimeV2,
|
|
...validPublic,
|
|
}),
|
|
contractPackages,
|
|
}),
|
|
).rejects.toBeDefined();
|
|
}
|
|
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader({
|
|
"dist/release-manifest.json": tamperedPrimary,
|
|
"dist/config.json": runtimeV2,
|
|
...validPublic,
|
|
}),
|
|
contractPackages,
|
|
}),
|
|
).resolves.toMatchObject({ coherence: { compatible: false } });
|
|
|
|
const releaseOnlyMissingReads: string[] = [];
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader(
|
|
{
|
|
"dist/config.json": runtimeV2,
|
|
...validPublic,
|
|
},
|
|
releaseOnlyMissingReads,
|
|
),
|
|
contractPackages,
|
|
}),
|
|
).rejects.toThrow("primary rollback artifact pair");
|
|
expect(releaseOnlyMissingReads).toEqual([
|
|
"dist/release-manifest.json",
|
|
"dist/config.json",
|
|
]);
|
|
|
|
const runtimeOnlyMissingReads: string[] = [];
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader(
|
|
{
|
|
"dist/release-manifest.json": exactV2,
|
|
...validPublic,
|
|
},
|
|
runtimeOnlyMissingReads,
|
|
),
|
|
contractPackages,
|
|
}),
|
|
).rejects.toThrow("primary rollback artifact pair");
|
|
expect(runtimeOnlyMissingReads).toEqual([
|
|
"dist/release-manifest.json",
|
|
"dist/config.json",
|
|
]);
|
|
|
|
const missingPairReads: string[] = [];
|
|
await expect(
|
|
verifyRollbackReleaseCoherence({
|
|
readArtifact: artifactReader(validPublic, missingPairReads),
|
|
contractPackages,
|
|
}),
|
|
).resolves.toMatchObject({ coherence: { compatible: true } });
|
|
expect(missingPairReads).toEqual([
|
|
"dist/release-manifest.json",
|
|
"dist/config.json",
|
|
"public/release-manifest.json",
|
|
"public/config.json",
|
|
]);
|
|
});
|
|
});
|