1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
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");
});
});
|