`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.
306 lines
9.0 KiB
TypeScript
306 lines
9.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
loadReleaseManifest,
|
|
ReleaseManifestError,
|
|
} from "../../src/bootstrap/load-release-manifest.ts";
|
|
import {
|
|
computeContractSetDigest,
|
|
type ContractSetPackage,
|
|
} from "../../src/contracts/contract-set-canonical.ts";
|
|
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../../src/features/installed-contract-contributions.ts";
|
|
|
|
const EMPTY_SET_DIGEST = await computeContractSetDigest([]);
|
|
/**
|
|
* TechLog's Studio contribution is always installed (Task 3), so the build's
|
|
* real expected contract set is no longer empty. A coherent manifest fixture
|
|
* must declare exactly what this build actually compiled in, or the boot-time
|
|
* `verifyContractSet` check rejects it as `CONTRACT_SET_PACKAGE_MISSING`.
|
|
*/
|
|
const EXPECTED_PACKAGES =
|
|
EXPECTED_CONTRACT_SET_PACKAGES as readonly ContractSetPackage[];
|
|
const EXPECTED_SET_DIGEST = await computeContractSetDigest(EXPECTED_PACKAGES);
|
|
|
|
const runtime: Parameters<typeof loadReleaseManifest>[0] = {
|
|
build: {
|
|
buildId: "build-a",
|
|
commitSha: "abc123",
|
|
routerBasePath: "/",
|
|
runtimeConfigUrl: "/config.json",
|
|
},
|
|
config: {
|
|
APP_ENV: "local",
|
|
API_BASE_URL: "https://api.test",
|
|
REQUEST_TIMEOUT_MS: 10_000,
|
|
MAX_RETRY_ATTEMPTS: 2,
|
|
TELEMETRY_ENABLED: false,
|
|
AUTH_MODE: "external",
|
|
RELEASE_MANIFEST_URL: "/release-manifest.json",
|
|
BUILD_ID: "build-a",
|
|
RELEASE_ID: "release-a",
|
|
CONFIG_SCHEMA_VERSION: "2.0",
|
|
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",
|
|
},
|
|
configSchema: "V2",
|
|
validationDurationMs: 0,
|
|
};
|
|
const manifest = {
|
|
schemaVersion: 2,
|
|
appVersion: "0.1.0",
|
|
buildId: "build-a",
|
|
commitSha: "abc123",
|
|
configSchemaVersion: "2.0",
|
|
assetManifestHash: "hash-a",
|
|
releaseId: "release-a",
|
|
builtAt: "2026-07-25T00:00:00Z",
|
|
routeChunks: { "route-home": "assets/home.js" },
|
|
contractSet: {
|
|
setAlgorithm: "CA_CONTRACT_SET_V1",
|
|
setDigest: EXPECTED_SET_DIGEST,
|
|
packages: EXPECTED_PACKAGES,
|
|
},
|
|
};
|
|
|
|
const runtimeV1: Parameters<typeof loadReleaseManifest>[0] = {
|
|
...runtime,
|
|
config: {
|
|
...runtime.config,
|
|
CONFIG_SCHEMA_VERSION: "1",
|
|
LEGACY_API_CONTRACT_VERSION: "1",
|
|
},
|
|
configSchema: "V1",
|
|
};
|
|
|
|
const manifestV1 = {
|
|
schemaVersion: 1,
|
|
appVersion: "0.1.0",
|
|
buildId: "build-a",
|
|
commitSha: "abc123",
|
|
configSchemaVersion: "1",
|
|
apiContractVersion: "1",
|
|
assetManifestHash: "hash-a",
|
|
releaseId: "release-a",
|
|
builtAt: "2026-07-25T00:00:00Z",
|
|
routeChunks: { "route-home": "assets/home.js" },
|
|
};
|
|
|
|
function jsonResponse(body: unknown): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Round 2 review finding: a fixture built entirely from
|
|
* `EXPECTED_CONTRACT_SET_PACKAGES` proves the manifest matches itself, not
|
|
* that composition actually produced the TechLog package. This assertion is
|
|
* independent of that derivation — the expected value is a literal written
|
|
* here, not read back from `EXPECTED_PACKAGES`/`EXPECTED_SET_DIGEST` above —
|
|
* so if the unconditional install in `installed-contract-contributions.ts`
|
|
* is ever reverted and `EXPECTED_CONTRACT_SET_PACKAGES` silently shrinks to
|
|
* empty, this fails on its own regardless of what the fixture below does.
|
|
*/
|
|
describe("expected contract set composition", () => {
|
|
it("actually contains the TechLog Studio contract, not just an empty set matching itself", () => {
|
|
const techLog = EXPECTED_CONTRACT_SET_PACKAGES.find(
|
|
(entry) => entry.packageId === "@tech-log/studio-contract",
|
|
);
|
|
expect(techLog).toBeTruthy();
|
|
expect(techLog?.version).toBe("3.0.0");
|
|
});
|
|
});
|
|
|
|
describe("release manifest boot boundary", () => {
|
|
it("loads a coherent release tuple", async () => {
|
|
await expect(
|
|
loadReleaseManifest(
|
|
runtime,
|
|
{ fetcher: async () => jsonResponse(manifest) },
|
|
),
|
|
).resolves.toMatchObject({ releaseId: "release-a" });
|
|
});
|
|
|
|
it("fails before mount when release and runtime differ", async () => {
|
|
await expect(
|
|
loadReleaseManifest(
|
|
runtime,
|
|
{
|
|
fetcher: async () =>
|
|
jsonResponse({ ...manifest, buildId: "build-b" }),
|
|
},
|
|
),
|
|
).rejects.toMatchObject({
|
|
kind: "BUILD_MISMATCH",
|
|
code: "MANIFEST_BUILD_MISMATCH",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
{ releaseId: "release-b" },
|
|
{},
|
|
"RELEASE_MISMATCH",
|
|
"MANIFEST_RELEASE_MISMATCH",
|
|
],
|
|
[
|
|
{},
|
|
{ expectedAssetManifestHash: "different" },
|
|
"ASSET_MISMATCH",
|
|
"MANIFEST_ASSET_MISMATCH",
|
|
],
|
|
])(
|
|
"classifies tuple mismatch %# without a generic deploy error",
|
|
async (manifestOverride, options, kind, code) => {
|
|
await expect(
|
|
loadReleaseManifest(
|
|
runtime,
|
|
{
|
|
fetcher: async () =>
|
|
jsonResponse({ ...manifest, ...manifestOverride }),
|
|
...options,
|
|
},
|
|
),
|
|
).rejects.toMatchObject({ kind, code });
|
|
},
|
|
);
|
|
|
|
it.each(["3.0", "9.0"])(
|
|
"rejects unsupported V2 manifest config version %s at the schema boundary",
|
|
async (configSchemaVersion) => {
|
|
await expect(
|
|
loadReleaseManifest(runtime, {
|
|
fetcher: async () =>
|
|
jsonResponse({ ...manifest, configSchemaVersion }),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
kind: "RELEASE_MANIFEST_FAILURE",
|
|
code: "MANIFEST_SCHEMA_INVALID",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("rejects a contract set the build did not compile", async () => {
|
|
await expect(
|
|
loadReleaseManifest(runtime, {
|
|
fetcher: async () =>
|
|
jsonResponse({
|
|
...manifest,
|
|
contractSet: {
|
|
setAlgorithm: "CA_CONTRACT_SET_V1",
|
|
setDigest: EMPTY_SET_DIGEST,
|
|
packages: [
|
|
{
|
|
packageId: "@org-contracts/worklog",
|
|
version: "1.2.3",
|
|
digest: `sha256:${"a".repeat(64)}`,
|
|
runtimeProtocolVersion: 1,
|
|
sourceRevision: "abc1234",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
kind: "CONTRACT_SET_MISMATCH",
|
|
code: "CONTRACT_SET_PACKAGE_UNEXPECTED",
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Round 2 review finding: deriving the fixture's `contractSet.packages`
|
|
* from `EXPECTED_CONTRACT_SET_PACKAGES` made `CONTRACT_SET_PACKAGE_MISSING`
|
|
* unreachable from any test — the equality was satisfied by construction.
|
|
* This restores that failure path with an independently built manifest
|
|
* that omits a package the real expected set actually requires, mirroring
|
|
* the shape of "rejects a contract set the build did not compile" above.
|
|
*/
|
|
it("rejects a manifest that omits a package the build actually compiled in", async () => {
|
|
await expect(
|
|
loadReleaseManifest(runtime, {
|
|
fetcher: async () =>
|
|
jsonResponse({
|
|
...manifest,
|
|
contractSet: {
|
|
setAlgorithm: "CA_CONTRACT_SET_V1",
|
|
setDigest: EMPTY_SET_DIGEST,
|
|
packages: [],
|
|
},
|
|
}),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
kind: "CONTRACT_SET_MISMATCH",
|
|
code: "CONTRACT_SET_PACKAGE_MISSING",
|
|
});
|
|
});
|
|
|
|
it("still reads a V1 manifest during the compatibility window", async () => {
|
|
await expect(
|
|
loadReleaseManifest(
|
|
runtimeV1,
|
|
{
|
|
fetcher: async () => jsonResponse(manifestV1),
|
|
},
|
|
),
|
|
).resolves.toMatchObject({ schemaVersion: 1, contractSet: null });
|
|
});
|
|
|
|
it("rejects a V2 runtime paired with a V1 manifest", async () => {
|
|
await expect(
|
|
loadReleaseManifest(runtime, {
|
|
fetcher: async () => jsonResponse({
|
|
...manifestV1,
|
|
configSchemaVersion: "2.0",
|
|
}),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
kind: "PROTOCOL_PAIR_MISMATCH",
|
|
code: "MANIFEST_PROTOCOL_PAIR_MISMATCH",
|
|
});
|
|
});
|
|
|
|
it("rejects a V1 runtime paired with a V2 manifest", async () => {
|
|
await expect(
|
|
loadReleaseManifest(runtimeV1, {
|
|
fetcher: async () => jsonResponse(manifest),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
kind: "PROTOCOL_PAIR_MISMATCH",
|
|
code: "MANIFEST_PROTOCOL_PAIR_MISMATCH",
|
|
});
|
|
});
|
|
|
|
it("requires matching legacy scalar versions for a V1 pair", async () => {
|
|
await expect(
|
|
loadReleaseManifest(runtimeV1, {
|
|
fetcher: async () => jsonResponse({
|
|
...manifestV1,
|
|
apiContractVersion: "2",
|
|
}),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
kind: "API_CONTRACT_MISMATCH",
|
|
code: "MANIFEST_API_CONTRACT_MISMATCH",
|
|
});
|
|
});
|
|
|
|
it("rejects a manifest without a complete route chunk map", async () => {
|
|
const malformed = Object.fromEntries(
|
|
Object.entries(manifest).filter(([key]) => key !== "routeChunks"),
|
|
);
|
|
await expect(
|
|
loadReleaseManifest(
|
|
runtime,
|
|
{ fetcher: async () => jsonResponse(malformed) },
|
|
),
|
|
).rejects.toBeInstanceOf(ReleaseManifestError);
|
|
});
|
|
});
|