The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
351 lines
11 KiB
TypeScript
351 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
composeBrowserRpcOperationRegistry,
|
|
composeBrowserRpcProviderProfileRegistry,
|
|
composeBrowserRpcRequestEncoderRegistry,
|
|
defineBrowserRpcOperation,
|
|
defineBrowserRpcProviderProfile,
|
|
installBrowserRpcContractBindings,
|
|
validateBrowserRpcContractBindings,
|
|
type BrowserRpcProviderProfile,
|
|
} from "../../../src/contracts/browser-rpc.ts";
|
|
import {
|
|
DESCRIPTOR_DIGEST,
|
|
MAPPERS,
|
|
RUNTIME_DIGEST,
|
|
SCHEMA_CODECS,
|
|
STREAM_ENCODER,
|
|
UNARY_ENCODER,
|
|
streamOperation,
|
|
streamProfile,
|
|
unaryOperation,
|
|
unaryProfile,
|
|
} from "./fixture.ts";
|
|
|
|
describe("Browser RPC contract registry", () => {
|
|
it("snapshots installed bindings before later source mutation", () => {
|
|
const operations: Record<string, ReturnType<typeof unaryOperation>> = {
|
|
GET_RPC_RESOURCE: unaryOperation(),
|
|
};
|
|
const installed = installBrowserRpcContractBindings({
|
|
operations,
|
|
profiles: { [unaryProfile().runtimeProfileId]: unaryProfile() },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
|
|
});
|
|
const before = installed.operations.get("GET_RPC_RESOURCE");
|
|
expect(before?.totalDeadlineMs).toBeDefined();
|
|
|
|
// R-04. A post-validation mutation of the source registry must not reach
|
|
// the installed snapshot.
|
|
operations.GET_RPC_RESOURCE = {
|
|
...operations.GET_RPC_RESOURCE!,
|
|
totalDeadlineMs: 999_999,
|
|
};
|
|
expect(installed.operations.get("GET_RPC_RESOURCE")).toBe(before);
|
|
expect(
|
|
installed.operations.get("GET_RPC_RESOURCE")?.totalDeadlineMs,
|
|
).not.toBe(999_999);
|
|
});
|
|
|
|
it("rejects extra accessor and symbol keys without invoking getters", () => {
|
|
let getterCalls = 0;
|
|
const accessorOperation = Object.defineProperty(
|
|
{ ...unaryOperation() },
|
|
"totalDeadlineMs",
|
|
{
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
getterCalls += 1;
|
|
return 1_000;
|
|
},
|
|
},
|
|
);
|
|
expect(() =>
|
|
installBrowserRpcContractBindings({
|
|
operations: { GET_RPC_RESOURCE: accessorOperation },
|
|
profiles: { [unaryProfile().runtimeProfileId]: unaryProfile() },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
|
|
}),
|
|
).toThrow(TypeError);
|
|
expect(getterCalls).toBe(0);
|
|
|
|
const extraKeyOperation = {
|
|
...unaryOperation(),
|
|
unexpectedKey: "smuggled",
|
|
};
|
|
expect(() =>
|
|
installBrowserRpcContractBindings({
|
|
operations: {
|
|
GET_RPC_RESOURCE: extraKeyOperation as never,
|
|
},
|
|
profiles: { [unaryProfile().runtimeProfileId]: unaryProfile() },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
|
|
}),
|
|
).toThrow(/unexpected key/u);
|
|
|
|
const symbolRegistry: Record<string, unknown> = {
|
|
GET_RPC_RESOURCE: unaryOperation(),
|
|
};
|
|
Object.defineProperty(symbolRegistry, Symbol("hidden"), {
|
|
enumerable: true,
|
|
value: unaryOperation(),
|
|
});
|
|
expect(() =>
|
|
installBrowserRpcContractBindings({
|
|
operations: symbolRegistry as never,
|
|
profiles: { [unaryProfile().runtimeProfileId]: unaryProfile() },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER },
|
|
}),
|
|
).toThrow(/symbol keys/u);
|
|
});
|
|
|
|
it("closes exact operation, provider, schema, mapper and encoder bindings", () => {
|
|
const operation = unaryOperation();
|
|
const profile = unaryProfile();
|
|
const operations = composeBrowserRpcOperationRegistry([
|
|
{ GET_RPC_RESOURCE: operation },
|
|
]);
|
|
const profiles = composeBrowserRpcProviderProfileRegistry([
|
|
{ CONNECT_REFERENCE_UNARY: profile },
|
|
]);
|
|
const encoders = composeBrowserRpcRequestEncoderRegistry([
|
|
{ RpcResourceRequestEncoder: UNARY_ENCODER },
|
|
]);
|
|
|
|
expect(
|
|
validateBrowserRpcContractBindings({
|
|
operations,
|
|
profiles,
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: encoders,
|
|
}),
|
|
).toBe(true);
|
|
expect(Object.isFrozen(operations)).toBe(true);
|
|
expect(Object.isFrozen(profiles.CONNECT_REFERENCE_UNARY)).toBe(true);
|
|
expect(
|
|
Object.isFrozen(
|
|
profiles.CONNECT_REFERENCE_UNARY?.allowedProcedures,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects duplicate rows and descriptor/provider drift", () => {
|
|
const operation = unaryOperation();
|
|
expect(() =>
|
|
composeBrowserRpcOperationRegistry([
|
|
{ GET_RPC_RESOURCE: operation },
|
|
{ GET_RPC_RESOURCE: operation },
|
|
]),
|
|
).toThrow("duplicate Browser RPC operation");
|
|
|
|
expect(() =>
|
|
validateBrowserRpcContractBindings({
|
|
operations: { GET_RPC_RESOURCE: operation },
|
|
profiles: {
|
|
CONNECT_REFERENCE_UNARY: unaryProfile({
|
|
descriptorDigest: "c".repeat(64),
|
|
}),
|
|
},
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: {
|
|
RpcResourceRequestEncoder: UNARY_ENCODER,
|
|
},
|
|
}),
|
|
).toThrow("provider binding is invalid");
|
|
});
|
|
|
|
it("permits only a public headerless NO_SIDE_EFFECTS Connect GET", () => {
|
|
const getProfile = defineBrowserRpcProviderProfile({
|
|
...unaryProfile(),
|
|
runtimeProfileId: "CONNECT_PUBLIC_GET",
|
|
requestMethod: "GET",
|
|
authProfileId: "ANONYMOUS",
|
|
csrfProfileId: "NONE",
|
|
allowedProcedures: [
|
|
"example.resource.v1.ResourceService/GetResource",
|
|
],
|
|
});
|
|
const validGet = defineBrowserRpcOperation({
|
|
...unaryOperation(),
|
|
runtimeProfileId: "CONNECT_PUBLIC_GET",
|
|
authProfileId: "ANONYMOUS",
|
|
csrfProfileId: "NONE",
|
|
idempotencyLevel: "NO_SIDE_EFFECTS",
|
|
dataClassification: "PUBLIC",
|
|
});
|
|
expect(
|
|
validateBrowserRpcContractBindings({
|
|
operations: { GET_RPC_RESOURCE: validGet },
|
|
profiles: { CONNECT_PUBLIC_GET: getProfile },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: {
|
|
RpcResourceRequestEncoder: UNARY_ENCODER,
|
|
},
|
|
}),
|
|
).toBe(true);
|
|
|
|
expect(() =>
|
|
validateBrowserRpcContractBindings({
|
|
operations: {
|
|
GET_RPC_RESOURCE: defineBrowserRpcOperation({
|
|
...validGet,
|
|
dataClassification: "CONFIDENTIAL",
|
|
}),
|
|
},
|
|
profiles: { CONNECT_PUBLIC_GET: getProfile },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: {
|
|
RpcResourceRequestEncoder: UNARY_ENCODER,
|
|
},
|
|
}),
|
|
).toThrow("GET binding is invalid");
|
|
});
|
|
|
|
it("separates official grpc-web, Connect-Web and Connect tuples", () => {
|
|
expect(() =>
|
|
defineBrowserRpcProviderProfile({
|
|
...streamProfile(),
|
|
runtimeProfileId: "OFFICIAL_BINARY_STREAM",
|
|
runtimeId: "official-grpc-web",
|
|
runtimeVersion: "1.5.0",
|
|
runtimeDigest: RUNTIME_DIGEST,
|
|
protocol: "GRPC_WEB",
|
|
runtimeKind: "OFFICIAL_GRPC_WEB_XHR",
|
|
clientApiKind: "CALLBACK_STREAM",
|
|
messageEncoding: "PROTO",
|
|
framing: "GRPC_WEB_BINARY_ENVELOPE",
|
|
deadlineDialect: "OFFICIAL_DEADLINE_METADATA",
|
|
cancelDialect: "CLIENT_READABLE_STREAM_CANCEL",
|
|
descriptorDigest: DESCRIPTOR_DIGEST,
|
|
}),
|
|
).toThrow("provider profile is invalid");
|
|
|
|
expect(() =>
|
|
defineBrowserRpcProviderProfile({
|
|
...unaryProfile(),
|
|
framing: "GRPC_WEB_BINARY_ENVELOPE",
|
|
}),
|
|
).toThrow("provider profile is invalid");
|
|
|
|
expect(
|
|
defineBrowserRpcProviderProfile({
|
|
...unaryProfile(),
|
|
runtimeProfileId: "CONNECT_GRPC_WEB_UNARY",
|
|
protocol: "GRPC_WEB",
|
|
framing: "GRPC_WEB_BINARY_ENVELOPE",
|
|
deadlineDialect: "GRPC_TIMEOUT",
|
|
}),
|
|
).toMatchObject({
|
|
protocol: "GRPC_WEB",
|
|
runtimeKind: "CONNECT_WEB_FETCH",
|
|
deadlineDialect: "GRPC_TIMEOUT",
|
|
});
|
|
});
|
|
|
|
it("revalidates raw registry rows instead of trusting TypeScript assertions", () => {
|
|
const invalidProfile = {
|
|
...unaryProfile(),
|
|
messageEncoding: "XML",
|
|
} as unknown as BrowserRpcProviderProfile;
|
|
|
|
expect(() =>
|
|
validateBrowserRpcContractBindings({
|
|
operations: { GET_RPC_RESOURCE: unaryOperation() },
|
|
profiles: { CONNECT_REFERENCE_UNARY: invalidProfile },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: {
|
|
RpcResourceRequestEncoder: UNARY_ENCODER,
|
|
},
|
|
}),
|
|
).toThrow("provider profile is invalid");
|
|
|
|
expect(() =>
|
|
validateBrowserRpcContractBindings({
|
|
operations: { WRONG_REGISTRY_KEY: unaryOperation() },
|
|
profiles: { CONNECT_REFERENCE_UNARY: unaryProfile() },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: {
|
|
RpcResourceRequestEncoder: UNARY_ENCODER,
|
|
},
|
|
}),
|
|
).toThrow("operation registry is invalid");
|
|
});
|
|
|
|
it("disallows frontend stream retry and unsafe unary replay", () => {
|
|
expect(() =>
|
|
defineBrowserRpcProviderProfile({
|
|
...streamProfile(),
|
|
retryOwner: "FRONTEND_ADAPTER",
|
|
maxAttempts: 2,
|
|
backoffMs: [10],
|
|
retryableFailures: ["UNAVAILABLE"],
|
|
}),
|
|
).toThrow("provider profile is invalid");
|
|
|
|
const retryProfile = unaryProfile({
|
|
retryProfileId: "RPC_RETRY_TWO",
|
|
retryOwner: "FRONTEND_ADAPTER",
|
|
maxAttempts: 2,
|
|
backoffMs: [10],
|
|
retryableFailures: ["UNAVAILABLE"],
|
|
});
|
|
expect(() =>
|
|
validateBrowserRpcContractBindings({
|
|
operations: {
|
|
CREATE_RPC_RESOURCE: defineBrowserRpcOperation({
|
|
...unaryOperation(),
|
|
operationId: "CREATE_RPC_RESOURCE",
|
|
semantics: "COMMAND",
|
|
replayPolicy: "NON_REPLAYABLE",
|
|
runtimeProfileId: retryProfile.runtimeProfileId,
|
|
retryProfileId: retryProfile.retryProfileId,
|
|
}),
|
|
},
|
|
profiles: { CONNECT_REFERENCE_UNARY: retryProfile },
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: {
|
|
RpcResourceRequestEncoder: {
|
|
...UNARY_ENCODER,
|
|
operationId: "CREATE_RPC_RESOURCE",
|
|
},
|
|
},
|
|
}),
|
|
).toThrow("retry binding is invalid");
|
|
});
|
|
|
|
it("supports a bounded Connect server-stream contract without composing it", () => {
|
|
expect(
|
|
validateBrowserRpcContractBindings({
|
|
operations: {
|
|
WATCH_RPC_RESOURCES: streamOperation(),
|
|
},
|
|
profiles: {
|
|
CONNECT_REFERENCE_STREAM: streamProfile(),
|
|
},
|
|
schemaCodecs: SCHEMA_CODECS,
|
|
mappers: MAPPERS,
|
|
requestEncoders: {
|
|
RpcResourceStreamRequestEncoder: STREAM_ENCODER,
|
|
},
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
});
|