import { describe, expect, it } from "vitest"; import { createBrowserRpcRuntime, defineBrowserRpcTransport, type BrowserRpcStreamFrame, type BrowserRpcTransport, } from "../../../src/adapters/browser-rpc/index.ts"; import { installBrowserRpcContractBindings } from "../../../src/contracts/browser-rpc.ts"; import { MAPPERS, SCHEMA_CODECS, STREAM_ENCODER, UNARY_ENCODER, isResourceView, streamOperation, streamProfile, unaryOperation, unaryProfile, } from "./fixture.ts"; function unaryRuntime( transport: BrowserRpcTransport, extra: Record = {}, ) { return createBrowserRpcRuntime({ operations: { GET_RPC_RESOURCE: unaryOperation() }, profiles: { CONNECT_REFERENCE_UNARY: unaryProfile() }, schemaCodecs: SCHEMA_CODECS, mappers: MAPPERS, requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER }, transports: { CONNECT_REFERENCE_UNARY: transport }, ...extra, }); } function streamingRuntime( transport: BrowserRpcTransport, extra: Record = {}, ) { return createBrowserRpcRuntime({ operations: { WATCH_RPC_RESOURCES: streamOperation() }, profiles: { CONNECT_REFERENCE_STREAM: streamProfile() }, schemaCodecs: SCHEMA_CODECS, mappers: MAPPERS, requestEncoders: { RpcResourceStreamRequestEncoder: STREAM_ENCODER }, transports: { CONNECT_REFERENCE_STREAM: transport }, ...extra, }); } function unaryTransport( invokeUnary: BrowserRpcTransport["invokeUnary"], ): BrowserRpcTransport { return defineBrowserRpcTransport({ runtimeProfileId: "CONNECT_REFERENCE_UNARY", providerId: "REFERENCE_RPC", protocol: "CONNECT_HTTP", rpcKind: "UNARY", invokeUnary, }); } function streamTransport( frames: readonly BrowserRpcStreamFrame[], ): BrowserRpcTransport { return defineBrowserRpcTransport({ runtimeProfileId: "CONNECT_REFERENCE_STREAM", providerId: "REFERENCE_RPC", protocol: "CONNECT_HTTP", rpcKind: "SERVER_STREAM", openServerStream: () => ({ async *[Symbol.asyncIterator]() { for (const frame of frames) yield frame; }, }), }); } async function collect( source: AsyncIterable, ): Promise { const seen: unknown[] = []; for await (const value of source) seen.push(value); return seen; } /** * RPC-RR-02. A collaborator that throws synchronously must not escape the * Result contract. A generation fence captured outside the protected boundary * and a `clock.sleep` invoked outside a promise boundary both did exactly that, * and the second also skipped the listener and timer release. */ describe("RPC-RR-02 synchronous collaborator throws stay inside Result", () => { const throwingFence = { capture: () => { throw new TypeError("fence exploded"); }, isCurrent: () => true, }; it("closes a unary call whose fence throws", async () => { const runtime = unaryRuntime( unaryTransport(async () => ({ ok: true, message: { id: "a", name: "A" }, encodedBytes: 8, })), { generationFence: throwingFence }, ); const result = await runtime .bindUnary("GET_RPC_RESOURCE", isResourceView) .execute({ resourceId: "resource-1" }); expect(result.ok).toBe(false); }); it("closes a stream whose fence throws", async () => { const runtime = streamingRuntime( streamTransport([{ kind: "TERMINAL", ok: true }]), { generationFence: throwingFence }, ); const results = await collect( runtime .bindServerStream("WATCH_RPC_RESOURCES", isResourceView) .open({ topic: "resources" }), ); expect(results.length).toBeGreaterThan(0); expect(results.every((value) => (value as { ok: boolean }).ok === false)).toBe( true, ); }); it("closes a unary call whose clock throws synchronously", async () => { const runtime = unaryRuntime( unaryTransport(() => new Promise(() => {})), { clock: { now: () => 0, sleep: () => { throw new TypeError("clock exploded"); }, }, }, ); const result = await runtime .bindUnary("GET_RPC_RESOURCE", isResourceView) .execute({ resourceId: "resource-1" }); expect(result.ok).toBe(false); }); }); /** * RPC-RR-03. The runtime reads a validated snapshot, never the caller's * objects: an accessor is refused without being invoked, and no installed * registry exposes a mutator. */ describe("RPC-RR-03 transport and binding registries are snapshots", () => { it("never invokes a transport accessor", () => { let getterCalls = 0; const hostile = {} as Record; Object.defineProperties(hostile, { runtimeProfileId: { enumerable: true, get: () => { getterCalls += 1; return "CONNECT_REFERENCE_UNARY"; }, }, providerId: { enumerable: true, value: "REFERENCE_RPC" }, protocol: { enumerable: true, value: "CONNECT_HTTP" }, rpcKind: { enumerable: true, value: "UNARY" }, invokeUnary: { enumerable: true, value: async () => ({ ok: true, message: {}, encodedBytes: 1 }), }, }); expect(() => unaryRuntime(hostile as unknown as BrowserRpcTransport), ).toThrow(TypeError); expect(getterCalls).toBe(0); }); it("rejects an unexpected own field on a transport row", () => { const transport = unaryTransport(async () => ({ ok: true, message: { id: "a", name: "A" }, encodedBytes: 8, })); const widened = { ...transport, injected: true }; expect(() => unaryRuntime(widened as unknown as BrowserRpcTransport), ).toThrow(TypeError); }); it("exposes no mutation API on any installed binding registry", () => { const installed = installBrowserRpcContractBindings({ operations: { GET_RPC_RESOURCE: unaryOperation() }, profiles: { CONNECT_REFERENCE_UNARY: unaryProfile() }, schemaCodecs: SCHEMA_CODECS, mappers: MAPPERS, requestEncoders: { RpcResourceRequestEncoder: UNARY_ENCODER }, }); for (const registry of Object.values(installed)) { const record = registry as unknown as Record; for (const mutator of ["set", "delete", "clear"]) { expect(record[mutator]).toBeUndefined(); } expect(() => Map.prototype.clear.call(registry as never), ).toThrow(); } expect(installed.operations.get("GET_RPC_RESOURCE")).toBeDefined(); }); }); /** * RPC-RR-04. Transport values are decoded, not adopted. `in` and a direct * property read run accessors and admit inherited or extra fields, and keeping * the caller's object lets it change after validation. */ describe("RPC-RR-04 transport results and frames are exactly decoded", () => { const hostileResults: readonly (readonly [string, () => unknown])[] = [ ["extra own field", () => ({ ok: true, message: {}, encodedBytes: 1, injected: 1 })], [ "inherited fields", () => Object.create({ ok: true, message: {}, encodedBytes: 1 }) as object, ], [ "throwing getter", () => { const value: Record = { message: {}, encodedBytes: 1 }; Object.defineProperty(value, "ok", { enumerable: true, get: () => { throw new TypeError("hostile getter"); }, }); return value; }, ], [ "symbol key", () => ({ ok: true, message: {}, encodedBytes: 1, [Symbol("injected")]: 1, }), ], ]; for (const [label, build] of hostileResults) { it(`refuses a unary result with ${label}`, async () => { const runtime = unaryRuntime( unaryTransport(async () => build() as never), ); const result = await runtime .bindUnary("GET_RPC_RESOURCE", isResourceView) .execute({ resourceId: "resource-1" }); expect(result.ok).toBe(false); }); } it("refuses an unknown transport failure code", async () => { const runtime = unaryRuntime( unaryTransport(async () => ({ ok: false, failure: { code: "MADE_UP_CODE" }, }) as never), ); const result = await runtime .bindUnary("GET_RPC_RESOURCE", isResourceView) .execute({ resourceId: "resource-1" }); expect(result.ok).toBe(false); }); it("refuses a retryAfterMs outside the hard ceiling", async () => { const runtime = unaryRuntime( unaryTransport(async () => ({ ok: false, failure: { code: "NETWORK_UNREACHABLE", retryAfterMs: -1 }, }) as never), ); const result = await runtime .bindUnary("GET_RPC_RESOURCE", isResourceView) .execute({ resourceId: "resource-1" }); expect(result.ok).toBe(false); }); it("refuses a stream frame with an extra own field", async () => { const runtime = streamingRuntime( streamTransport([ { kind: "MESSAGE", message: { id: "a", name: "A" }, encodedBytes: 4, injected: 1, } as never, ]), ); const results = await collect( runtime .bindServerStream("WATCH_RPC_RESOURCES", isResourceView) .open({ topic: "resources" }), ); expect(results.every((value) => (value as { ok: boolean }).ok === false)).toBe( true, ); }); });