返回 DeepSeek-Reasonix
conn_test.go
根目录 / internal / extension / rpcwire / conn_test.go
1 package rpcwire
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "strings"
10 "sync"
11 "testing"
12 "time"
13 )
14
15 func TestStructuredHandlerError(t *testing.T) {
16 in := strings.NewReader("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"fail\",\"params\":{}}\n")
17 var out bytes.Buffer
18 conn := NewConn(in, &out, Options{MaxInboundBytes: 1024, MaxOutboundBytes: 1024})
19 conn.Handle("fail", func(context.Context, json.RawMessage) (any, error) {
20 return nil, &RPCError{Code: -32000, Message: "controlled", Data: map[string]any{"reasonixCode": "HOST_BUSY", "retryable": true}}
21 })
22 if err := conn.Serve(context.Background()); err != nil {
23 t.Fatal(err)
24 }
25 var frame struct {
26 Error *ErrorObject `json:"error"`
27 }
28 if err := json.Unmarshal(bytes.TrimSpace(out.Bytes()), &frame); err != nil {
29 t.Fatal(err)
30 }
31 if frame.Error == nil || frame.Error.Code != -32000 || frame.Error.Message != "controlled" {
32 t.Fatalf("error = %+v", frame.Error)
33 }
34 var data map[string]any
35 if err := json.Unmarshal(frame.Error.Data, &data); err != nil {
36 t.Fatal(err)
37 }
38 if data["reasonixCode"] != "HOST_BUSY" || data["retryable"] != true {
39 t.Fatalf("data = %#v", data)
40 }
41 }
42
43 func TestHandlerResponseAfterWriteRunsAfterSuccessfulFrame(t *testing.T) {
44 request := "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"detach\",\"params\":{}}\n"
45 var out bytes.Buffer
46 callback := make(chan struct {
47 err error
48 response string
49 }, 1)
50 conn := NewConn(strings.NewReader(request), &out, Options{StrictJSONRPC: true})
51 conn.Handle("detach", func(context.Context, json.RawMessage) (any, error) {
52 return RespondThen(map[string]bool{"detached": true}, func(err error) {
53 callback <- struct {
54 err error
55 response string
56 }{err: err, response: out.String()}
57 }), nil
58 })
59 if err := conn.Serve(context.Background()); err != nil {
60 t.Fatal(err)
61 }
62 got := <-callback
63 if got.err != nil {
64 t.Fatalf("callback error = %v", got.err)
65 }
66 if !strings.Contains(got.response, `"result":{"detached":true}`) {
67 t.Fatalf("callback ran before response write: %q", got.response)
68 }
69 }
70
71 func TestHandlerResponseAfterWriteReceivesTransportFailure(t *testing.T) {
72 wantErr := errors.New("write failed")
73 callback := make(chan error, 1)
74 conn := NewConn(
75 strings.NewReader("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"detach\",\"params\":{}}\n"),
76 failWriter{err: wantErr},
77 Options{StrictJSONRPC: true},
78 )
79 conn.Handle("detach", func(context.Context, json.RawMessage) (any, error) {
80 return RespondThen(map[string]bool{"detached": true}, func(err error) { callback <- err }), nil
81 })
82 if err := conn.Serve(context.Background()); !errors.Is(err, wantErr) {
83 t.Fatalf("Serve error = %v, want %v", err, wantErr)
84 }
85 if err := <-callback; !errors.Is(err, wantErr) {
86 t.Fatalf("callback error = %v, want %v", err, wantErr)
87 }
88 }
89
90 func TestRequestKeepsDeliveredResponseWhenPeerClosesAfterWrite(t *testing.T) {
91 for attempt := 0; attempt < 100; attempt++ {
92 serverToClientR, serverToClientW := io.Pipe()
93 clientToServerR, clientToServerW := io.Pipe()
94 client := NewConn(serverToClientR, clientToServerW, Options{Name: "response-close-client"})
95 server := NewConn(clientToServerR, serverToClientW, Options{Name: "response-close-server"})
96 server.Handle("detach", func(context.Context, json.RawMessage) (any, error) {
97 return RespondThen(map[string]bool{"detached": true}, func(error) {
98 _ = serverToClientW.Close()
99 }), nil
100 })
101 ctx, cancel := context.WithCancel(context.Background())
102 clientDone := make(chan struct{})
103 serverDone := make(chan struct{})
104 go func() { _ = client.Serve(ctx); close(clientDone) }()
105 go func() { _ = server.Serve(ctx); close(serverDone) }()
106 raw, err := client.Request(ctx, "detach", struct{}{})
107 cancel()
108 _ = clientToServerW.Close()
109 _ = serverToClientW.Close()
110 <-clientDone
111 <-serverDone
112 if err != nil {
113 t.Fatalf("attempt %d lost the written response to peer EOF: %v", attempt, err)
114 }
115 if !bytes.Contains(raw, []byte(`"detached":true`)) {
116 t.Fatalf("attempt %d response = %s", attempt, raw)
117 }
118 }
119 }
120
121 func TestInboundLimitIncludesNewline(t *testing.T) {
122 line := "{\"jsonrpc\":\"2.0\",\"method\":\"n\"}\n"
123 conn := NewConn(strings.NewReader(line), io.Discard, Options{MaxInboundBytes: len(line) - 1, Name: "test"})
124 err := conn.Serve(context.Background())
125 var tooLarge *FrameTooLargeError
126 if !errors.As(err, &tooLarge) || tooLarge.Direction != "inbound" || tooLarge.Limit != len(line)-1 {
127 t.Fatalf("error = %v", err)
128 }
129 }
130
131 func TestOutboundLimitIncludesNewline(t *testing.T) {
132 var out bytes.Buffer
133 conn := NewConn(strings.NewReader(""), &out, Options{MaxOutboundBytes: 8})
134 err := conn.Notify("event", map[string]string{"body": "too large"})
135 var tooLarge *FrameTooLargeError
136 if !errors.As(err, &tooLarge) || tooLarge.Direction != "outbound" || tooLarge.Size <= tooLarge.Limit {
137 t.Fatalf("error = %v", err)
138 }
139 if out.Len() != 0 {
140 t.Fatalf("wrote %d bytes after rejecting frame", out.Len())
141 }
142 }
143
144 type blockingTryNotifyWriter struct {
145 started chan struct{}
146 release chan struct{}
147 once sync.Once
148 }
149
150 func (w *blockingTryNotifyWriter) Write(p []byte) (int, error) {
151 w.once.Do(func() { close(w.started) })
152 <-w.release
153 return len(p), nil
154 }
155
156 func TestTryNotifyDoesNotWaitForPhysicalWrite(t *testing.T) {
157 w := &blockingTryNotifyWriter{started: make(chan struct{}), release: make(chan struct{})}
158 conn := NewConn(strings.NewReader(""), w, Options{})
159 done := make(chan error, 1)
160 go func() { done <- conn.TryNotify("event", map[string]int{"index": 1}) }()
161
162 select {
163 case err := <-done:
164 if err != nil {
165 t.Fatalf("TryNotify: %v", err)
166 }
167 case <-time.After(time.Second):
168 t.Fatal("TryNotify waited for the blocked physical write")
169 }
170 select {
171 case <-w.started:
172 case <-time.After(time.Second):
173 t.Fatal("writer never received the enqueued notification")
174 }
175 close(w.release)
176 }
177
178 func TestTryNotifyDropsImmediatelyWhenQueueIsFull(t *testing.T) {
179 w := &blockingTryNotifyWriter{started: make(chan struct{}), release: make(chan struct{})}
180 conn := NewConn(strings.NewReader(""), w, Options{})
181 if err := conn.TryNotify("event", map[string]int{"index": 0}); err != nil {
182 t.Fatalf("first TryNotify: %v", err)
183 }
184 select {
185 case <-w.started:
186 case <-time.After(time.Second):
187 t.Fatal("writer never blocked on the first notification")
188 }
189 for i := 1; i < bestEffortNotifyQueueLimit; i++ {
190 if err := conn.TryNotify("event", map[string]int{"index": i}); err != nil {
191 t.Fatalf("TryNotify %d before capacity: %v", i, err)
192 }
193 }
194 overflow := make(chan error, 1)
195 go func() {
196 overflow <- conn.TryNotify("event", map[string]int{"index": bestEffortNotifyQueueLimit})
197 }()
198 var err error
199 select {
200 case err = <-overflow:
201 case <-time.After(time.Second):
202 t.Fatal("full-queue TryNotify blocked instead of dropping the notification")
203 }
204 var full *OutboundQueueFullError
205 if !errors.As(err, &full) || full.Limit != bestEffortNotifyQueueLimit {
206 t.Fatalf("overflow error = %#v, want OutboundQueueFullError(%d)", err, bestEffortNotifyQueueLimit)
207 }
208 close(w.release)
209 }
210
211 func TestWriterExitsAfterGracefulServeClose(t *testing.T) {
212 for attempt := 0; attempt < 100; attempt++ {
213 conn := NewConn(strings.NewReader(""), io.Discard, Options{Name: "writer-exit"})
214 if err := conn.Serve(context.Background()); err != nil {
215 t.Fatalf("attempt %d Serve: %v", attempt, err)
216 }
217 select {
218 case <-conn.writerDone:
219 case <-time.After(time.Second):
220 t.Fatalf("attempt %d writer goroutine did not exit", attempt)
221 }
222 }
223 }
224
225 func TestRequestReturnsStructuredPeerError(t *testing.T) {
226 serverToClientR, serverToClientW := io.Pipe()
227 clientToServerR, clientToServerW := io.Pipe()
228 client := NewConn(serverToClientR, clientToServerW, Options{})
229 server := NewConn(clientToServerR, serverToClientW, Options{})
230 server.Handle("fail", func(context.Context, json.RawMessage) (any, error) {
231 return nil, &RPCError{Code: -32000, Message: "busy", Data: map[string]any{"reasonixCode": "HOST_BUSY"}}
232 })
233 ctx, cancel := context.WithCancel(context.Background())
234 defer cancel()
235 go func() { _ = client.Serve(ctx) }()
236 go func() { _ = server.Serve(ctx) }()
237 _, err := client.Request(ctx, "fail", struct{}{})
238 var responseErr *ResponseError
239 if !errors.As(err, &responseErr) || responseErr.Code != -32000 || !bytes.Contains(responseErr.Data, []byte("HOST_BUSY")) {
240 t.Fatalf("error = %#v", err)
241 }
242 _ = clientToServerW.Close()
243 _ = serverToClientW.Close()
244 }
245
246 func TestStrictJSONRPCRejectsMissingVersionAndInvalidShape(t *testing.T) {
247 input := strings.Join([]string{
248 `{"id":1,"method":"ping","params":{}}`,
249 `{"jsonrpc":"2.0","id":2,"method":"ping","result":{}}`,
250 `{"jsonrpc":"2.0","id":3,"method":"ping","params":"bad"}`,
251 `{"jsonrpc":"2.0","id":{},"method":"ping","params":{}}`,
252 `{"jsonrpc":"2.0","id":5,"error":{"code":-32000}}`,
253 `{"jsonrpc":"2.0","id":6,"error":"bad"}`,
254 }, "\n") + "\n"
255 var out bytes.Buffer
256 conn := NewConn(strings.NewReader(input), &out, Options{StrictJSONRPC: true})
257 called := 0
258 conn.Handle("ping", func(context.Context, json.RawMessage) (any, error) {
259 called++
260 return struct{}{}, nil
261 })
262 if err := conn.Serve(context.Background()); err != nil {
263 t.Fatal(err)
264 }
265 if called != 0 {
266 t.Fatalf("handler called %d times", called)
267 }
268 dec := json.NewDecoder(&out)
269 wantIDs := []string{"1", "2", "3", "null", "5", "null"}
270 for i := 0; i < 6; i++ {
271 var frame struct {
272 ID json.RawMessage `json:"id"`
273 Error *ErrorObject `json:"error"`
274 }
275 if err := dec.Decode(&frame); err != nil {
276 t.Fatalf("decode response %d: %v", i, err)
277 }
278 if frame.Error == nil || frame.Error.Code != ErrInvalidRequest {
279 t.Fatalf("response %d error = %+v", i, frame.Error)
280 }
281 if string(frame.ID) != wantIDs[i] {
282 t.Fatalf("response %d id = %s, want %s", i, frame.ID, wantIDs[i])
283 }
284 }
285 }
286
287 func TestOversizedHandlerResultGetsSmallErrorResponse(t *testing.T) {
288 request := "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"large\",\"params\":{}}\n"
289 var out bytes.Buffer
290 conn := NewConn(strings.NewReader(request), &out, Options{MaxOutboundBytes: 160})
291 conn.Handle("large", func(context.Context, json.RawMessage) (any, error) {
292 return map[string]string{"body": strings.Repeat("x", 1024)}, nil
293 })
294 if err := conn.Serve(context.Background()); err != nil {
295 t.Fatal(err)
296 }
297 var frame struct {
298 Error *ErrorObject `json:"error"`
299 }
300 if err := json.Unmarshal(bytes.TrimSpace(out.Bytes()), &frame); err != nil {
301 t.Fatal(err)
302 }
303 if frame.Error == nil || frame.Error.Code != ErrInternal || frame.Error.Message != "response exceeds frame size limit" {
304 t.Fatalf("error = %+v", frame.Error)
305 }
306 }
307
308 func TestBeforeRequestObservesArrivalOrderBeforeHandlersRun(t *testing.T) {
309 input := strings.Join([]string{
310 `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`,
311 `{"jsonrpc":"2.0","id":2,"method":"business","params":{}}`,
312 }, "\n") + "\n"
313 var out bytes.Buffer
314 state := "new"
315 var stateMu sync.Mutex
316 businessSeen := make(chan struct{})
317 conn := NewConn(strings.NewReader(input), &out, Options{
318 StrictJSONRPC: true,
319 BeforeRequest: func(method string, _ json.RawMessage) error {
320 stateMu.Lock()
321 defer stateMu.Unlock()
322 switch state {
323 case "new":
324 if method != "initialize" {
325 return &RPCError{Code: ErrInvalidRequest, Message: "initialize must be first"}
326 }
327 state = "initializing"
328 return nil
329 case "initializing":
330 if method == "business" {
331 close(businessSeen)
332 }
333 return &RPCError{Code: ErrInvalidRequest, Message: "initialize incomplete"}
334 default:
335 return nil
336 }
337 },
338 })
339 started := make(chan struct{})
340 release := make(chan struct{})
341 businessRan := make(chan struct{}, 1)
342 conn.Handle("initialize", func(context.Context, json.RawMessage) (any, error) {
343 close(started)
344 <-release
345 stateMu.Lock()
346 state = "ready"
347 stateMu.Unlock()
348 return struct{}{}, nil
349 })
350 conn.Handle("business", func(context.Context, json.RawMessage) (any, error) {
351 businessRan <- struct{}{}
352 return struct{}{}, nil
353 })
354 done := make(chan error, 1)
355 go func() { done <- conn.Serve(context.Background()) }()
356 <-started
357 select {
358 case <-businessSeen:
359 case <-time.After(2 * time.Second):
360 close(release)
361 t.Fatal("business request did not pass through the arrival gate")
362 }
363 close(release)
364 if err := <-done; err != nil {
365 t.Fatal(err)
366 }
367 dec := json.NewDecoder(bytes.NewReader(out.Bytes()))
368 seenRejected := false
369 for {
370 var frame struct {
371 ID json.RawMessage `json:"id"`
372 Error *ErrorObject `json:"error"`
373 }
374 if err := dec.Decode(&frame); errors.Is(err, io.EOF) {
375 break
376 } else if err != nil {
377 t.Fatal(err)
378 }
379 if string(frame.ID) == "2" && frame.Error != nil && frame.Error.Message == "initialize incomplete" {
380 seenRejected = true
381 }
382 }
383 if !seenRejected {
384 t.Fatalf("frames = %s", out.String())
385 }
386 select {
387 case <-businessRan:
388 t.Fatal("business handler ran before initialize completed")
389 default:
390 }
391 }
392
393 func TestBeforeNotificationSynchronouslyRejectsWithoutResponse(t *testing.T) {
394 input := "{\"jsonrpc\":\"2.0\",\"method\":\"client/note\",\"params\":{}}\n"
395 var out bytes.Buffer
396 gateCalled := false
397 handlerCalled := false
398 conn := NewConn(strings.NewReader(input), &out, Options{
399 StrictJSONRPC: true,
400 BeforeNotification: func(method string, _ json.RawMessage) error {
401 gateCalled = method == "client/note"
402 return &RPCError{Code: ErrInvalidRequest, Message: "notifications forbidden"}
403 },
404 })
405 conn.HandleNotify("client/note", func(context.Context, json.RawMessage) { handlerCalled = true })
406 if err := conn.Serve(context.Background()); err != nil {
407 t.Fatal(err)
408 }
409 if !gateCalled || handlerCalled {
410 t.Fatalf("gateCalled=%v handlerCalled=%v", gateCalled, handlerCalled)
411 }
412 if out.Len() != 0 {
413 t.Fatalf("JSON-RPC notification rejection emitted a response: %s", out.String())
414 }
415 }
416
416 lines GO