turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

jsonrpc_test.go · 435 lines · 13.4 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1package jsonrpc_test
2
3import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net"
11 "strings"
12 "testing"
13 "time"
14
15 "codeberg.org/turbo-editors/turbo-core/jsonrpc"
16)
17
18// newlineFraming marks messages off with a newline, which is what the Agent
19// Client Protocol does. It is the simplest framing there is, so the tests are
20// about the JSON-RPC layer rather than about parsing headers.
21type newlineFraming struct{}
22
23func (newlineFraming) ReadMessage(r *bufio.Reader) ([]byte, error) {
24 line, err := r.ReadBytes('\n')
25 if err != nil {
26 return nil, err
27 }
28 return line, nil
29}
30
31func (newlineFraming) WriteMessage(w io.Writer, body []byte) error {
32 _, err := fmt.Fprintf(w, "%s\n", body)
33 return err
34}
35
36// frame is one message the peer read, or the reason it could not.
37type frame struct {
38 msg map[string]any
39 err error
40}
41
42// peer is the far end of a connection, driven from a test.
43//
44// It is deliberately not built on Conn: a peer sharing the code under test
45// could not catch that code writing a malformed frame.
46//
47// It reads continuously into a buffered channel rather than on demand, because
48// net.Pipe is synchronous — a write blocks until somebody reads. With on-demand
49// reads, a Reply made from the test goroutine deadlocks against the read that
50// was going to collect it. Reading continuously is also what a real client
51// does.
52type peer struct {
53 t *testing.T
54 stream net.Conn
55 frames chan frame
56}
57
58// newPair returns a Conn and the raw peer at the other end of it.
59func newPair(t *testing.T, onNotify jsonrpc.NotificationFunc, onRequest jsonrpc.RequestFunc) (*jsonrpc.Conn, *peer) {
60 t.Helper()
61
62 ours, theirs := net.Pipe()
63 conn := jsonrpc.NewConn(ours, newlineFraming{}, onNotify, onRequest)
64 go func() { _ = conn.Run() }()
65
66 p := &peer{t: t, stream: theirs, frames: make(chan frame, 32)}
67 go p.readLoop()
68
69 t.Cleanup(func() { _ = conn.Close(); _ = theirs.Close() })
70 return conn, p
71}
72
73// readLoop decodes every line the connection sends until it ends.
74//
75// It never calls t.Fatalf: that is not allowed outside the test goroutine, and
76// doing it here hangs the run instead of failing it. Failures travel back as
77// values instead.
78func (p *peer) readLoop() {
79 reader := bufio.NewReader(p.stream)
80 for {
81 line, err := reader.ReadBytes('\n')
82 if err != nil {
83 close(p.frames)
84 return
85 }
86
87 var msg map[string]any
88 if err := json.Unmarshal(line, &msg); err != nil {
89 p.frames <- frame{err: fmt.Errorf("the connection wrote %q, which is not JSON: %w", line, err)}
90 continue
91 }
92 p.frames <- frame{msg: msg}
93 }
94}
95
96// send writes one raw line to the connection under test.
97func (p *peer) send(line string) {
98 p.t.Helper()
99
100 _ = p.stream.SetWriteDeadline(time.Now().Add(2 * time.Second))
101 if _, err := fmt.Fprintf(p.stream, "%s\n", line); err != nil {
102 p.t.Fatalf("writing to the connection: %v", err)
103 }
104}
105
106// read returns the next message the connection sent, failing the test if none
107// arrives.
108func (p *peer) read() map[string]any {
109 p.t.Helper()
110
111 select {
112 case got, open := <-p.frames:
113 if !open {
114 p.t.Fatal("the connection ended without sending anything more")
115 }
116 if got.err != nil {
117 p.t.Fatal(got.err)
118 }
119 return got.msg
120 case <-time.After(2 * time.Second):
121 p.t.Fatal("the connection sent nothing")
122 return nil
123 }
124}
125
126// readsNothing fails the test if the connection sends anything within a short
127// window. It is how "this must not be answered" is stated.
128func (p *peer) readsNothing(what string) {
129 p.t.Helper()
130
131 select {
132 case got := <-p.frames:
133 p.t.Fatalf("%s: the connection sent %v", what, got.msg)
134 case <-time.After(150 * time.Millisecond):
135 }
136}
137
138func TestACallGetsItsAnswer(t *testing.T) {
139 conn, peer := newPair(t, nil, nil)
140
141 answered := make(chan error, 1)
142 var result struct{ Name string }
143 go func() { answered <- conn.Call(t.Context(), "greet", map[string]string{"who": "world"}, &result) }()
144
145 request := peer.read()
146 if request["method"] != "greet" {
147 t.Fatalf("the connection sent %v, want a greet request", request)
148 }
149 if request["jsonrpc"] != jsonrpc.Version {
150 t.Errorf("the request says jsonrpc %v, want %q", request["jsonrpc"], jsonrpc.Version)
151 }
152 peer.send(fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Name":"hello"}}`, request["id"]))
153
154 if err := <-answered; err != nil {
155 t.Fatalf("Call() error = %v", err)
156 }
157 if result.Name != "hello" {
158 t.Errorf("the result is %q, want %q", result.Name, "hello")
159 }
160}
161
162func TestAPeersRequestIdDoesNotStealAnAnswerMeantForOurOwnCall(t *testing.T) {
163 // Both sides number their requests from one, and an agent really does send
164 // `id: 1` while a call of ours with the same id is in flight — that is what
165 // the Agent Client Protocol's agents do. The two directions are separate
166 // id spaces, and a connection that shared one map would hand the peer's
167 // *request* to the caller waiting for an answer, or answer the request
168 // with the caller's result.
169 asked := make(chan string, 1)
170 onRequest := func(req *jsonrpc.Request) {
171 asked <- req.Method
172 req.Reply(map[string]string{"outcome": "allowed"}, nil)
173 }
174 conn, peer := newPair(t, nil, onRequest)
175
176 answered := make(chan error, 1)
177 var result struct{ Name string }
178 go func() { answered <- conn.Call(t.Context(), "ours", nil, &result) }()
179
180 ourRequest := peer.read()
181 if fmt.Sprint(ourRequest["id"]) != "1" {
182 t.Fatalf("our first call took id %v, want 1 — the rest of this test assumes it", ourRequest["id"])
183 }
184
185 // The peer now asks a question carrying the same id as our call in flight.
186 peer.send(`{"jsonrpc":"2.0","id":1,"method":"theirs","params":{}}`)
187
188 select {
189 case method := <-asked:
190 if method != "theirs" {
191 t.Errorf("the handler was given %q, want %q", method, "theirs")
192 }
193 case <-time.After(2 * time.Second):
194 t.Fatal("the peer's request never reached the handler")
195 }
196
197 reply := peer.read()
198 if fmt.Sprint(reply["id"]) != "1" {
199 t.Errorf("the answer carries id %v, want the peer's own 1", reply["id"])
200 }
201 if reply["result"] == nil {
202 t.Errorf("the peer's request was answered with %v, want a result", reply)
203 }
204
205 // Our own call must still be waiting, and must still get its own answer.
206 select {
207 case err := <-answered:
208 t.Fatalf("our call finished early with %v; the peer's request stole its answer", err)
209 case <-time.After(50 * time.Millisecond):
210 }
211
212 peer.send(`{"jsonrpc":"2.0","id":1,"result":{"Name":"ours"}}`)
213 if err := <-answered; err != nil {
214 t.Fatalf("Call() error = %v", err)
215 }
216 if result.Name != "ours" {
217 t.Errorf("our call got %q, want %q", result.Name, "ours")
218 }
219}
220
221func TestARequestMayBeAnsweredLongAfterItArrived(t *testing.T) {
222 // This is the whole reason this package exists apart from lsp: a permission
223 // dialog is answered by a person, on another goroutine, several turns of an
224 // event loop later. A handler that had to return its answer could not wait.
225 held := make(chan *jsonrpc.Request, 1)
226 _, peer := newPair(t, nil, func(req *jsonrpc.Request) { held <- req })
227
228 peer.send(`{"jsonrpc":"2.0","id":7,"method":"session/request_permission","params":{}}`)
229
230 request := <-held
231 if request.Method != "session/request_permission" {
232 t.Fatalf("the handler was given %q", request.Method)
233 }
234
235 // Nothing may be written until the answer is given.
236 peer.readsNothing("answered before the handler did")
237
238 // From another goroutine, as the event loop's dialog really does.
239 go request.Reply(map[string]any{"outcome": map[string]string{"outcome": "selected"}}, nil)
240
241 reply := peer.read()
242 if fmt.Sprint(reply["id"]) != "7" {
243 t.Errorf("the answer carries id %v, want 7", reply["id"])
244 }
245 if reply["result"] == nil {
246 t.Errorf("the answer is %v, want a result", reply)
247 }
248}
249
250func TestOnlyTheFirstReplyToARequestIsSent(t *testing.T) {
251 // A window closing tears down a pending permission that may already have
252 // been answered. Two responses carrying one id would desynchronise the
253 // peer, which matches answers to requests by exactly that id.
254 held := make(chan *jsonrpc.Request, 1)
255 _, peer := newPair(t, nil, func(req *jsonrpc.Request) { held <- req })
256
257 peer.send(`{"jsonrpc":"2.0","id":3,"method":"ask","params":{}}`)
258 request := <-held
259
260 go request.Reply("first", nil)
261
262 first := peer.read()
263
264 // Only now, with the first answer known to be on the wire, can the later
265 // ones be tried: Reply holds the once until its write has finished.
266 request.Reply("second", nil)
267 request.Reply(nil, errors.New("third"))
268 if first["result"] != "first" {
269 t.Errorf("the answer is %v, want the first one", first)
270 }
271
272 // A second frame must never arrive.
273 peer.readsNothing("a second answer was sent")
274}
275
276func TestAnErrorFromAHandlerReachesThePeerAsAnErrorObject(t *testing.T) {
277 _, peer := newPair(t, nil, func(req *jsonrpc.Request) {
278 req.Reply(nil, &jsonrpc.ResponseError{Code: jsonrpc.CodeInvalidParams, Message: "no uri"})
279 })
280
281 peer.send(`{"jsonrpc":"2.0","id":2,"method":"fs/read_text_file","params":{}}`)
282
283 reply := peer.read()
284 failure, ok := reply["error"].(map[string]any)
285 if !ok {
286 t.Fatalf("the answer is %v, want an error object", reply)
287 }
288 if failure["code"] != float64(jsonrpc.CodeInvalidParams) {
289 t.Errorf("the code is %v, want %d", failure["code"], jsonrpc.CodeInvalidParams)
290 }
291 if failure["message"] != "no uri" {
292 t.Errorf("the message is %v, want %q", failure["message"], "no uri")
293 }
294}
295
296func TestAPlainErrorFromAHandlerBecomesAnInternalError(t *testing.T) {
297 _, peer := newPair(t, nil, func(req *jsonrpc.Request) {
298 req.Reply(nil, errors.New("the file is a directory"))
299 })
300
301 peer.send(`{"jsonrpc":"2.0","id":2,"method":"fs/read_text_file","params":{}}`)
302
303 failure, ok := peer.read()["error"].(map[string]any)
304 if !ok {
305 t.Fatal("want an error object")
306 }
307 if failure["code"] != float64(jsonrpc.CodeInternalError) {
308 t.Errorf("the code is %v, want %d", failure["code"], jsonrpc.CodeInternalError)
309 }
310 if !strings.Contains(fmt.Sprint(failure["message"]), "the file is a directory") {
311 t.Errorf("the message is %v, want the handler's own words in it", failure["message"])
312 }
313}
314
315func TestARequestWithNoHandlerIsAnsweredMethodNotFound(t *testing.T) {
316 // Silence would leave the peer waiting for ever. A peer that asks for
317 // something this client does not do has to be told so.
318 _, peer := newPair(t, nil, nil)
319
320 peer.send(`{"jsonrpc":"2.0","id":4,"method":"terminal/create","params":{}}`)
321
322 failure, ok := peer.read()["error"].(map[string]any)
323 if !ok {
324 t.Fatal("want an error object")
325 }
326 if failure["code"] != float64(jsonrpc.CodeMethodNotFound) {
327 t.Errorf("the code is %v, want %d", failure["code"], jsonrpc.CodeMethodNotFound)
328 }
329 if failure["message"] != "terminal/create" {
330 t.Errorf("the message is %v, want the method's name", failure["message"])
331 }
332}
333
334func TestANotificationReachesTheHandlerAndIsNeverAnswered(t *testing.T) {
335 // A notification is exactly a message with no id, and answering one would
336 // send a response the peer has nothing to match it to.
337 arrived := make(chan string, 1)
338 _, peer := newPair(t, func(method string, _ json.RawMessage) { arrived <- method }, nil)
339
340 peer.send(`{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"s"}}`)
341
342 select {
343 case method := <-arrived:
344 if method != "session/update" {
345 t.Errorf("the handler was given %q", method)
346 }
347 case <-time.After(2 * time.Second):
348 t.Fatal("the notification never arrived")
349 }
350
351 peer.readsNothing("the notification was answered")
352}
353
354func TestNotifySendsNoId(t *testing.T) {
355 conn, peer := newPair(t, nil, nil)
356
357 go func() { _ = conn.Notify("session/cancel", map[string]string{"sessionId": "s"}) }()
358
359 sent := peer.read()
360 if _, carries := sent["id"]; carries {
361 t.Errorf("the notification carries an id: %v", sent)
362 }
363 if sent["method"] != "session/cancel" {
364 t.Errorf("the notification is %v", sent)
365 }
366}
367
368func TestACallFailsWhenTheConnectionEnds(t *testing.T) {
369 // A peer that dies mid-request must not leave the caller blocked for ever.
370 conn, peer := newPair(t, nil, nil)
371
372 answered := make(chan error, 1)
373 go func() { answered <- conn.Call(context.Background(), "greet", nil, nil) }()
374
375 peer.read() // wait until the request is really on the wire
376 _ = peer.stream.Close()
377
378 select {
379 case err := <-answered:
380 if !errors.Is(err, jsonrpc.ErrClosed) {
381 t.Errorf("Call() error = %v, want ErrClosed", err)
382 }
383 case <-time.After(2 * time.Second):
384 t.Fatal("the call never came back")
385 }
386}
387
388func TestACallEndsWithItsContext(t *testing.T) {
389 conn, peer := newPair(t, nil, nil)
390
391 ctx, cancel := context.WithCancel(context.Background())
392 answered := make(chan error, 1)
393 go func() { answered <- conn.Call(ctx, "slow", nil, nil) }()
394
395 peer.read()
396 cancel()
397
398 select {
399 case err := <-answered:
400 if !errors.Is(err, context.Canceled) {
401 t.Errorf("Call() error = %v, want context.Canceled", err)
402 }
403 case <-time.After(2 * time.Second):
404 t.Fatal("the call ignored its context")
405 }
406}
407
408func TestAResponseToAnIdWeNeverSentIsIgnored(t *testing.T) {
409 // The connection must not panic or die on one; an agent replaying a
410 // recorded session can easily send one.
411 conn, peer := newPair(t, nil, nil)
412
413 peer.send(`{"jsonrpc":"2.0","id":999,"result":{}}`)
414
415 // Still usable afterwards, which is the whole point.
416 answered := make(chan error, 1)
417 go func() { answered <- conn.Call(t.Context(), "greet", nil, nil) }()
418 request := peer.read()
419 peer.send(fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":null}`, request["id"]))
420
421 if err := <-answered; err != nil {
422 t.Errorf("the connection stopped working after a stray response: %v", err)
423 }
424}
425
426func TestCallingOnAClosedConnectionFails(t *testing.T) {
427 conn, _ := newPair(t, nil, nil)
428
429 if err := conn.Close(); err != nil {
430 t.Fatalf("Close() error = %v", err)
431 }
432 if err := conn.Call(t.Context(), "greet", nil, nil); !errors.Is(err, jsonrpc.ErrClosed) {
433 t.Errorf("Call() error = %v, want ErrClosed", err)
434 }
435}