import { describe, expect, test } from "bun:test"; import { createRpcEngine } from "@getuserfeedback/protocol/internal/v3-rpc-engine"; import { createGeneration } from "./v3-generation.js"; import type { RpcRequest, RpcResponse } from "./v3-rpc-messages.js"; function createHarness() { const outbound: Array = []; const engine = createRpcEngine((message) => { outbound.push(message); }); return { engine, outbound }; } describe("v3 RPC engine", () => { test("uncached handlers bypass replay and do not evict ordinary cached results", async () => { const { engine, outbound } = createHarness(); let ordinary = 0; let drafts = 0; engine.registerHandler("ordinary", () => ++ordinary); engine.registerHandler("draft", () => ++drafts, { cacheResponse: false }); const request = (method: string, idemKey: string): RpcRequest => ({ t: "getuserfeedback:rpc:request", gen: createGeneration(1), rpcId: crypto.randomUUID(), method, idemKey, params: {}, }); await engine.handleMessage(request("ordinary", "retained")); // A pre-existing cached key must not short-circuit an uncached handler. await engine.handleMessage(request("draft", "retained")); await engine.handleMessage(request("draft", "retained")); expect(outbound.at(-1)).toMatchObject({ ok: true, result: 2 }); for (let index = 0; index < 1001; index++) await engine.handleMessage(request("draft", `draft-${index}`)); await engine.handleMessage(request("ordinary", "retained")); expect(outbound.at(-1)).toMatchObject({ ok: true, result: 1 }); expect(ordinary).toBe(1); expect(drafts).toBe(1003); }); test("resolves calls when response arrives", async () => { const { engine, outbound } = createHarness(); const callPromise = engine.call("echo", { value: 1 }); const request = outbound[0] as RpcRequest; await engine.handleMessage({ t: "getuserfeedback:rpc:response", gen: request.gen, rpcId: request.rpcId, ok: true, result: { echoed: true }, viewId: request.viewId, }); await expect(callPromise).resolves.toEqual({ echoed: true }); }); test("times out when no response", async () => { const { engine } = createHarness(); const promise = engine.call("slow", {}, { timeoutMs: 1 }); await expect(promise).rejects.toBeInstanceOf(Error); }); test("deduplicates responses using idemKey", async () => { const { engine, outbound } = createHarness(); engine.registerHandler("add", (params) => { if ( typeof params === "object" && params !== null && "value" in params && typeof (params as { value?: unknown }).value === "number" ) { return (params as { value: number }).value + 1; } throw new Error("invalid params"); }); const gen = createGeneration(1); const callA = engine.call("add", { value: 1 }, { idemKey: "same", gen }); const request = outbound[0] as RpcRequest; await engine.handleMessage(request); // server-side handling const responseA = outbound.at(-1); if (responseA && responseA.t === "getuserfeedback:rpc:response") { await engine.handleMessage(responseA); } const callB = engine.call("add", { value: 2 }, { idemKey: "same", gen }); const requestB = outbound.at(-1); if (requestB && requestB.t === "getuserfeedback:rpc:request") { await engine.handleMessage(requestB); const responseB = outbound.at(-1); if (responseB && responseB.t === "getuserfeedback:rpc:response") { await engine.handleMessage(responseB); } } await expect(callA).resolves.toBe(2); await expect(callB).resolves.toBe(2); }); test("passes request metadata to handlers", async () => { const { engine } = createHarness(); let receivedMeta: unknown = null; engine.registerHandler("inspect", (_params, meta) => { receivedMeta = meta ?? null; return { ok: true }; }); const request: RpcRequest = { t: "getuserfeedback:rpc:request", gen: createGeneration(1), rpcId: "00000000-0000-4000-8000-000000000000", method: "inspect", params: { value: 1 }, idemKey: "idem-123", viewId: "view-123", }; await engine.handleMessage(request); expect(receivedMeta).toEqual({ rpcId: request.rpcId, viewId: request.viewId, idemKey: request.idemKey, gen: request.gen, method: request.method, }); }); test("resets in-flight calls on channel close", async () => { const { engine } = createHarness(); const promise = engine.call("pending", {}); engine.reset("closed"); await expect(promise).rejects.toBeInstanceOf(Error); }); test("generates UUID rpcIds when randomUUID is unavailable", async () => { const originalCrypto = globalThis.crypto; const cryptoWithoutRandomUuid = originalCrypto && typeof originalCrypto === "object" && "getRandomValues" in originalCrypto && typeof originalCrypto.getRandomValues === "function" ? { getRandomValues: originalCrypto.getRandomValues.bind(originalCrypto), } : undefined; Object.defineProperty(globalThis, "crypto", { configurable: true, value: cryptoWithoutRandomUuid, }); try { const { engine, outbound } = createHarness(); void engine.call("echo", { value: 1 }); const request = outbound[0]; expect(request?.t).toBe("getuserfeedback:rpc:request"); if (!request || request.t !== "getuserfeedback:rpc:request") { throw new Error("Expected an RPC request"); } expect(request.rpcId).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, ); } finally { Object.defineProperty(globalThis, "crypto", { configurable: true, value: originalCrypto, }); } }); });