415 lines
13 KiB
TypeScript
415 lines
13 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
createRuntimeAdapters,
|
|
createRuntimeHttpClient,
|
|
} from "../../src/bootstrap/runtime-adapters.ts";
|
|
import { createBrowserMutationIntentFactory } from "../../src/adapters/platform/browser-mutation-intent-factory.ts";
|
|
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.ts";
|
|
|
|
type Runtime = Parameters<typeof createRuntimeAdapters>[0]["runtime"];
|
|
type Release = Parameters<typeof createRuntimeAdapters>[0]["release"];
|
|
|
|
const runtime: Runtime = {
|
|
config: {
|
|
APP_ENV: "local",
|
|
API_BASE_URL: "http://localhost:8080",
|
|
TELEMETRY_ENABLED: false,
|
|
AUTH_MODE: "demo",
|
|
REQUEST_TIMEOUT_MS: 4321,
|
|
MAX_RETRY_ATTEMPTS: 0,
|
|
RELEASE_MANIFEST_URL: "/release-manifest.json",
|
|
CONFIG_SCHEMA_VERSION: "2.0",
|
|
CAPABILITY_OVERRIDES: {
|
|
REALTIME: "DEFAULT",
|
|
WEB_WORKER: "DEFAULT",
|
|
SERVICE_WORKER: "DEFAULT",
|
|
OFFLINE_COMMANDS: "DEFAULT",
|
|
},
|
|
},
|
|
configSchema: "V2",
|
|
build: {
|
|
buildId: "build-a",
|
|
commitSha: "abc123",
|
|
routerBasePath: "/",
|
|
runtimeConfigUrl: "/config.json",
|
|
},
|
|
validationDurationMs: 0,
|
|
};
|
|
const release: Release = {
|
|
schemaVersion: 2,
|
|
appVersion: "0.1.0",
|
|
buildId: "build-a",
|
|
commitSha: "abc123",
|
|
configSchemaVersion: "2.0",
|
|
contractSet: {
|
|
setAlgorithm: "CA_CONTRACT_SET_V1",
|
|
setDigest: `sha256:${"0".repeat(64)}`,
|
|
packages: [],
|
|
},
|
|
assetManifestHash: "hash-a",
|
|
releaseId: "release-a",
|
|
builtAt: "2026-07-25T00:00:00Z",
|
|
routeChunks: { "route-home": "assets/home.js" },
|
|
};
|
|
|
|
describe("runtime adapter composition", () => {
|
|
it("creates validated intent and idempotency identities with independent UUID calls", () => {
|
|
const randomUUID = vi
|
|
.fn<() => string>()
|
|
.mockReturnValueOnce("intent-uuid")
|
|
.mockReturnValueOnce("idempotency-uuid");
|
|
const factory = createBrowserMutationIntentFactory({
|
|
randomUUID,
|
|
monotonicNow: () => 12.5,
|
|
});
|
|
|
|
const intent = factory.create({
|
|
operationId: "CREATE_REFERENCE_RESOURCE",
|
|
canonicalInputIdentity: "opaque-canonical-input",
|
|
requiresIdempotencyKey: true,
|
|
});
|
|
|
|
expect(intent).toEqual({
|
|
intentId: "intent-uuid",
|
|
operationId: "CREATE_REFERENCE_RESOURCE",
|
|
canonicalInputIdentity: "opaque-canonical-input",
|
|
idempotencyKey: "idempotency-uuid",
|
|
createdAtMonotonicMs: 12.5,
|
|
});
|
|
expect(Object.isFrozen(intent)).toBe(true);
|
|
expect(randomUUID).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("rejects invalid or unbounded mutation intent values", () => {
|
|
const factory = createBrowserMutationIntentFactory({
|
|
randomUUID: () => "opaque-runtime-identifier",
|
|
monotonicNow: () => 1,
|
|
});
|
|
|
|
expect(() =>
|
|
factory.create({
|
|
operationId: " ",
|
|
canonicalInputIdentity: "valid-identity",
|
|
requiresIdempotencyKey: false,
|
|
}),
|
|
).toThrow(TypeError);
|
|
expect(() =>
|
|
factory.create({
|
|
operationId: "CREATE_REFERENCE_RESOURCE",
|
|
canonicalInputIdentity: "x".repeat(16_385),
|
|
requiresIdempotencyKey: false,
|
|
}),
|
|
).toThrow(TypeError);
|
|
expect(() =>
|
|
createBrowserMutationIntentFactory({
|
|
randomUUID: () => "opaque-runtime-identifier",
|
|
monotonicNow: () => -1,
|
|
}).create({
|
|
operationId: "CREATE_REFERENCE_RESOURCE",
|
|
canonicalInputIdentity: "valid-identity",
|
|
requiresIdempotencyKey: false,
|
|
}),
|
|
).toThrow(TypeError);
|
|
});
|
|
|
|
it("constructs the local demo seam and infrastructure adapters", async () => {
|
|
const adapters = await createRuntimeAdapters({
|
|
runtime,
|
|
release,
|
|
host: {},
|
|
});
|
|
|
|
expect(adapters.outputPorts.session.getState()).toBe("unauthenticated");
|
|
await expect(adapters.outputPorts.releaseInfo.getCurrent()).resolves.toMatchObject({
|
|
releaseId: "release-a",
|
|
});
|
|
expect(adapters.infrastructure.queryClient).toBeDefined();
|
|
expect(adapters.infrastructure.queryInvalidation).toBeDefined();
|
|
expect(adapters.infrastructure.mutationIntentFactory).toBeDefined();
|
|
expect(
|
|
adapters.infrastructure.crossContextInvalidationStatus(),
|
|
).toBe("DEGRADED_LOCAL_ONLY");
|
|
expect(adapters.outputPorts.diagnostics.record).toEqual(expect.any(Function));
|
|
expect(adapters.outputPorts.telemetry.emit).toEqual(expect.any(Function));
|
|
expect(adapters.outputPorts.telemetry.pendingCount()).toBe(0);
|
|
expect(adapters).not.toHaveProperty("http");
|
|
expect(adapters).not.toHaveProperty("storage");
|
|
adapters.infrastructure.dispose();
|
|
});
|
|
|
|
it("replaces the QueryClient and coordinator for each session generation", async () => {
|
|
const adapters = await createRuntimeAdapters({ runtime, release, host: {} });
|
|
const previousClient = adapters.infrastructure.queryClient;
|
|
const previousCoordinator = adapters.infrastructure.queryInvalidation;
|
|
const runtimeMutationIntentFactory =
|
|
adapters.infrastructure.mutationIntentFactory;
|
|
const clearPrevious = vi.spyOn(previousClient, "clear");
|
|
|
|
await adapters.outputPorts.session.beginSignIn();
|
|
|
|
await vi.waitFor(() =>
|
|
expect(adapters.infrastructure.queryClient).not.toBe(previousClient),
|
|
);
|
|
expect(adapters.infrastructure.queryInvalidation).not.toBe(
|
|
previousCoordinator,
|
|
);
|
|
expect(adapters.infrastructure.mutationIntentFactory).toBe(
|
|
runtimeMutationIntentFactory,
|
|
);
|
|
|
|
const clearCallsAfterReplacement = clearPrevious.mock.calls.length;
|
|
await previousCoordinator.resetLocal();
|
|
expect(clearPrevious).toHaveBeenCalledTimes(clearCallsAfterReplacement);
|
|
|
|
adapters.infrastructure.dispose();
|
|
});
|
|
|
|
it("passes the supplied command intent unchanged and keeps private identity out of URLs and diagnostics", async () => {
|
|
const requests: Array<Readonly<{ url: string; headers: Headers }>> = [];
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
requests.push({
|
|
url: String(input),
|
|
headers: new Headers(init?.headers),
|
|
});
|
|
return Response.json(
|
|
{ id: "resource-1", name: "Created resource" },
|
|
{ status: 201 },
|
|
);
|
|
});
|
|
const adapters = await createRuntimeAdapters({
|
|
runtime,
|
|
release,
|
|
host: {},
|
|
fetcher,
|
|
});
|
|
await adapters.outputPorts.session.beginSignIn();
|
|
await vi.waitFor(() =>
|
|
expect(adapters.infrastructure.serverStateScope.getPhase()).toBe("READY"),
|
|
);
|
|
const intent = Object.freeze({
|
|
intentId: "private-intent-id",
|
|
operationId: "CREATE_REFERENCE_RESOURCE",
|
|
canonicalInputIdentity: "private-canonical-input",
|
|
idempotencyKey: "private-idempotency-key",
|
|
createdAtMonotonicMs: 42,
|
|
});
|
|
|
|
await expect(
|
|
adapters.featureInputs["reference-feature"].createResource(
|
|
{ name: "Created resource" },
|
|
{ intent },
|
|
),
|
|
).resolves.toMatchObject({ ok: true });
|
|
|
|
expect(requests).toHaveLength(1);
|
|
expect(requests[0]?.headers.get("Idempotency-Key")).toBe(
|
|
"private-idempotency-key",
|
|
);
|
|
expect(requests[0]?.url).toBe(
|
|
"http://localhost:8080/api/reference-resources",
|
|
);
|
|
const safeEvidence = JSON.stringify({
|
|
requests: requests.map((request) => request.url),
|
|
diagnostics: adapters.outputPorts.diagnostics.entries(),
|
|
});
|
|
expect(safeEvidence).not.toContain("private-intent-id");
|
|
expect(safeEvidence).not.toContain("private-canonical-input");
|
|
expect(safeEvidence).not.toContain("private-idempotency-key");
|
|
adapters.infrastructure.dispose();
|
|
});
|
|
|
|
it("does not fail boot when Web Storage capability getters throw", async () => {
|
|
const host: Record<string, unknown> = {};
|
|
Object.defineProperties(host, {
|
|
localStorage: {
|
|
get() {
|
|
throw new DOMException("denied", "SecurityError");
|
|
},
|
|
},
|
|
sessionStorage: {
|
|
get() {
|
|
throw new DOMException("denied", "SecurityError");
|
|
},
|
|
},
|
|
});
|
|
|
|
const adapters = await createRuntimeAdapters({
|
|
runtime,
|
|
release,
|
|
host,
|
|
});
|
|
|
|
expect(adapters.infrastructure.queryClient).toBeDefined();
|
|
expect(adapters.outputPorts.preferences.read("COLOR_SCHEME")).toEqual({
|
|
ok: true,
|
|
value: undefined,
|
|
});
|
|
adapters.infrastructure.dispose();
|
|
});
|
|
|
|
it("fails closed when an external auth owner was not installed", async () => {
|
|
const adapters = await createRuntimeAdapters({
|
|
runtime: {
|
|
...runtime,
|
|
config: { ...runtime.config, AUTH_MODE: "external" },
|
|
},
|
|
release,
|
|
host: {},
|
|
});
|
|
expect(adapters.outputPorts.session.getState()).toBe("integration-failed");
|
|
});
|
|
|
|
it("reports the static selection when no capability override disables it", async () => {
|
|
const adapters = await createRuntimeAdapters({ runtime, release, host: {} });
|
|
|
|
const snapshot = adapters.outputPorts.runtimeCapabilities.getSnapshot();
|
|
|
|
expect(snapshot.map((status) => status.capabilityId)).toEqual([
|
|
"REALTIME",
|
|
"WEB_WORKER",
|
|
"SERVICE_WORKER",
|
|
"OFFLINE_COMMANDS",
|
|
]);
|
|
expect(snapshot.every((status) => status.override === "DEFAULT")).toBe(true);
|
|
});
|
|
|
|
it("carries a disabling override into the capability snapshot", async () => {
|
|
const adapters = await createRuntimeAdapters({
|
|
runtime: {
|
|
...runtime,
|
|
config: {
|
|
...runtime.config,
|
|
CAPABILITY_OVERRIDES: {
|
|
...runtime.config.CAPABILITY_OVERRIDES,
|
|
SERVICE_WORKER: "DISABLED",
|
|
},
|
|
},
|
|
},
|
|
release,
|
|
host: {},
|
|
});
|
|
|
|
const serviceWorker = adapters.outputPorts.runtimeCapabilities
|
|
.getSnapshot()
|
|
.find((status) => status.capabilityId === "SERVICE_WORKER");
|
|
|
|
expect(serviceWorker?.override).toBe("DISABLED");
|
|
expect(serviceWorker?.active).toBe(0);
|
|
});
|
|
|
|
it("states contract identity as a digest for a V2 release manifest", async () => {
|
|
const adapters = await createRuntimeAdapters({ runtime, release, host: {} });
|
|
|
|
const current = await adapters.outputPorts.releaseInfo.getCurrent();
|
|
|
|
expect(current.contractSetDigest).toBe(release.contractSet?.setDigest);
|
|
expect(current.apiContractVersion).toBeUndefined();
|
|
expect(current).not.toHaveProperty("contractSet");
|
|
});
|
|
|
|
it("states contract identity as the legacy scalar for a V1 release manifest", async () => {
|
|
const adapters = await createRuntimeAdapters({
|
|
runtime,
|
|
release: {
|
|
...release,
|
|
schemaVersion: 1,
|
|
contractSet: null,
|
|
legacyApiContractVersion: "1.4",
|
|
},
|
|
host: {},
|
|
});
|
|
|
|
const current = await adapters.outputPorts.releaseInfo.getCurrent();
|
|
|
|
expect(current.apiContractVersion).toBe("1.4");
|
|
expect(current.contractSetDigest).toBeUndefined();
|
|
expect(current).not.toHaveProperty("legacyApiContractVersion");
|
|
});
|
|
|
|
it("refetches the active release manifest with no-store semantics", async () => {
|
|
const activeRelease = {
|
|
...release,
|
|
buildId: "build-b",
|
|
releaseId: "release-b",
|
|
routeChunks: { "route-home": "assets/home-b.js" },
|
|
};
|
|
const fetcher = vi.fn(async () =>
|
|
new Response(JSON.stringify(activeRelease), {
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
);
|
|
const adapters = await createRuntimeAdapters({
|
|
runtime,
|
|
release,
|
|
fetcher,
|
|
host: {},
|
|
});
|
|
|
|
await expect(adapters.outputPorts.releaseInfo.refresh()).resolves.toMatchObject({
|
|
buildId: "build-b",
|
|
releaseId: "release-b",
|
|
});
|
|
expect(fetcher).toHaveBeenCalledWith(
|
|
"/release-manifest.json",
|
|
expect.objectContaining({
|
|
method: "GET",
|
|
cache: "no-store",
|
|
credentials: "same-origin",
|
|
redirect: "error",
|
|
referrerPolicy: "no-referrer",
|
|
headers: { Accept: "application/json" },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("injects runtime timeout and max-attempt policy into HTTP execution", async () => {
|
|
const scheduled: Array<{ callback: () => void; milliseconds: number }> = [];
|
|
const scheduler = {
|
|
setTimeout: vi.fn((callback: () => void, milliseconds: number) => {
|
|
scheduled.push({ callback, milliseconds });
|
|
return scheduled.length;
|
|
}),
|
|
clearTimeout: vi.fn(),
|
|
};
|
|
const fetcher = vi.fn(async () =>
|
|
Response.json(
|
|
{
|
|
success: false,
|
|
error: { code: "TEMPORARY" },
|
|
meta: { requestId: "request-1", traceId: "trace-1" },
|
|
},
|
|
{ status: 503 },
|
|
),
|
|
);
|
|
const authSession =
|
|
(await createRuntimeAdapters({
|
|
runtime,
|
|
release,
|
|
host: {},
|
|
})).outputPorts.session;
|
|
const client = createRuntimeHttpClient(
|
|
{
|
|
runtime,
|
|
authSession,
|
|
fetcher,
|
|
clock: { now: () => 0, sleep: async () => {} },
|
|
scheduler,
|
|
},
|
|
TEST_HTTP_CONTRACT,
|
|
);
|
|
|
|
await client.execute({
|
|
operationId: "LIST_ENTITIES",
|
|
routeId: "TEST_ROUTE",
|
|
});
|
|
|
|
expect(fetcher).toHaveBeenCalledOnce();
|
|
expect(scheduler.setTimeout).toHaveBeenCalledWith(
|
|
expect.any(Function),
|
|
4321,
|
|
);
|
|
expect(scheduler.clearTimeout).toHaveBeenCalledOnce();
|
|
});
|
|
});
|