import { describe, expect, it } from "vitest"; import { JsonRpcClient } from "./rpc.js"; function makeClient(): { client: JsonRpcClient; written: Uint8Array[]; feedMessage: (msg: unknown) => void; } { const written: Uint8Array[] = []; const client = new JsonRpcClient((bytes) => { written.push(bytes); }); return { client, written, feedMessage: (msg: unknown) => { client.handleMessage(JSON.stringify(msg)); }, }; } describe("JsonRpcClient", () => { it("request returns result", async () => { const { client, feedMessage } = makeClient(); const resultPromise = client.request("initialize", { protocolVersion: "2025-11-25" }); feedMessage({ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2025-11-25" } }); const result = await resultPromise; expect(result).toEqual({ protocolVersion: "2025-11-25" }); }); it("request rejects on error response", async () => { const { client, feedMessage } = makeClient(); const resultPromise = client.request("bad-method"); feedMessage({ jsonrpc: "2.0", id: 1, error: { code: -32601, message: "Method not found" }, }); await expect(resultPromise).rejects.toThrow("Method not found"); }); it("notify sends without expecting response", () => { const { client, written } = makeClient(); client.notify("notifications/initialized", {}); expect(written.length).toBe(1); const sent = new TextDecoder().decode(written[0]); expect(sent).toContain('"method":"notifications/initialized"'); expect(sent).not.toContain('"id"'); }); it("onNotification fires for matching method", () => { const { client, feedMessage } = makeClient(); let received: unknown = "unset"; client.onNotification("notifications/tools/list_changed", (params) => { received = params; }); feedMessage({ jsonrpc: "2.0", method: "notifications/tools/list_changed", params: { a: 1 } }); expect(received).toEqual({ a: 1 }); }); it("pending request rejected on close", async () => { const { client } = makeClient(); const resultPromise = client.request("slow-method"); client.close(); await expect(resultPromise).rejects.toThrow("Connection closed"); }); it("incremental request ids", () => { const { client, written } = makeClient(); client.request("a"); client.request("b"); client.request("c"); expect(written.length).toBe(3); // Extract JSON body from Content-Length framed messages const parse = (bytes: Uint8Array): { id: number } => { const text = new TextDecoder().decode(bytes); const bodyStart = text.indexOf("\r\n\r\n") + 4; return JSON.parse(text.slice(bodyStart)) as { id: number }; }; const msg1 = parse(written[0]); const msg2 = parse(written[1]); const msg3 = parse(written[2]); expect(msg1.id).toBe(1); expect(msg2.id).toBe(2); expect(msg3.id).toBe(3); }); it("notify after close is silently dropped", () => { const { client, written } = makeClient(); client.close(); const count = written.length; client.notify("test"); expect(written.length).toBe(count); }); it("request after close rejects immediately", async () => { const { client } = makeClient(); client.close(); await expect(client.request("test")).rejects.toThrow("Connection closed"); }); });