返回 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 := range 100 {
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 := range 100 {
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 := t.Context()
234 go func() { _ = client.Serve(ctx) }()
235 go func() { _ = server.Serve(ctx) }()
236 _, err := client.Request(ctx, "fail", struct{}{})
237 var responseErr *ResponseError
238 if !errors.As(err, &responseErr) || responseErr.Code != -32000 || !bytes.Contains(responseErr.Data, []byte("HOST_BUSY")) {
239 t.Fatalf("error = %#v", err)
240 }
241 _ = clientToServerW.Close()
242 _ = serverToClientW.Close()
243 }
244
245 func TestStrictJSONRPCRejectsMissingVersionAndInvalidShape(t *testing.T) {
246 input := strings.Join([]string{
247 `{"id":1,"method":"ping","params":{}}`,
248 `{"jsonrpc":"2.0","id":2,"method":"ping","result":{}}`,
249 `{"jsonrpc":"2.0","id":3,"method":"ping","params":"bad"}`,
250 `{"jsonrpc":"2.0","id":{},"method":"ping","params":{}}`,
251 `{"jsonrpc":"2.0","id":5,"error":{"code":-32000}}`,
252 `{"jsonrpc":"2.0","id":6,"error":"bad"}`,
253 }, "\n") + "\n"
254 var out bytes.Buffer
255 conn := NewConn(strings.NewReader(input), &out, Options{StrictJSONRPC: true})
256 called := 0
257 conn.Handle("ping", func(context.Context, json.RawMessage) (any, error) {
258 called++
259 return struct{}{}, nil
260 })
261 if err := conn.Serve(context.Background()); err != nil {
262 t.Fatal(err)
263 }
264 if called != 0 {
265 t.Fatalf("handler called %d times", called)
266 }
267 dec := json.NewDecoder(&out)
268 wantIDs := []string{"1", "2", "3", "null", "5", "null"}
269 for i := range 6 {
270 var frame struct {
271 ID json.RawMessage `json:"id"`
272 Error *ErrorObject `json:"error"`
273 }
274 if err := dec.Decode(&frame); err != nil {
275 t.Fatalf("decode response %d: %v", i, err)
276 }
277 if frame.Error == nil || frame.Error.Code != ErrInvalidRequest {
278 t.Fatalf("response %d error = %+v", i, frame.Error)
279 }
280 if string(frame.ID) != wantIDs[i] {
281 t.Fatalf("response %d id = %s, want %s", i, frame.ID, wantIDs[i])
282 }
283 }
284 }
285
286 func TestOversizedHandlerResultGetsSmallErrorResponse(t *testing.T) {
287 request := "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"large\",\"params\":{}}\n"
288 var out bytes.Buffer
289 conn := NewConn(strings.NewReader(request), &out, Options{MaxOutboundBytes: 160})
290 conn.Handle("large", func(context.Context, json.RawMessage) (any, error) {
291 return map[string]string{"body": strings.Repeat("x", 1024)}, nil
292 })
293 if err := conn.Serve(context.Background()); err != nil {
294 t.Fatal(err)
295 }
296 var frame struct {
297 Error *ErrorObject `json:"error"`
298 }
299 if err := json.Unmarshal(bytes.TrimSpace(out.Bytes()), &frame); err != nil {
300 t.Fatal(err)
301 }
302 if frame.Error == nil || frame.Error.Code != ErrInternal || frame.Error.Message != "response exceeds frame size limit" {
303 t.Fatalf("error = %+v", frame.Error)
304 }
305 }
306
307 func TestBeforeRequestObservesArrivalOrderBeforeHandlersRun(t *testing.T) {
308 input := strings.Join([]string{
309 `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`,
310 `{"jsonrpc":"2.0","id":2,"method":"business","params":{}}`,
311 }, "\n") + "\n"
312 var out bytes.Buffer
313 state := "new"
314 var stateMu sync.Mutex
315 businessSeen := make(chan struct{})
316 conn := NewConn(strings.NewReader(input), &out, Options{
317 StrictJSONRPC: true,
318 BeforeRequest: func(method string, _ json.RawMessage) error {
319 stateMu.Lock()
320 defer stateMu.Unlock()
321 switch state {
322 case "new":
323 if method != "initialize" {
324 return &RPCError{Code: ErrInvalidRequest, Message: "initialize must be first"}
325 }
326 state = "initializing"
327 return nil
328 case "initializing":
329 if method == "business" {
330 close(businessSeen)
331 }
332 return &RPCError{Code: ErrInvalidRequest, Message: "initialize incomplete"}
333 default:
334 return nil
335 }
336 },
337 })
338 started := make(chan struct{})
339 release := make(chan struct{})
340 businessRan := make(chan struct{}, 1)
341 conn.Handle("initialize", func(context.Context, json.RawMessage) (any, error) {
342 close(started)
343 <-release
344 stateMu.Lock()
345 state = "ready"
346 stateMu.Unlock()
347 return struct{}{}, nil
348 })
349 conn.Handle("business", func(context.Context, json.RawMessage) (any, error) {
350 businessRan <- struct{}{}
351 return struct{}{}, nil
352 })
353 done := make(chan error, 1)
354 go func() { done <- conn.Serve(context.Background()) }()
355 <-started
356 select {
357 case <-businessSeen:
358 case <-time.After(2 * time.Second):
359 close(release)
360 t.Fatal("business request did not pass through the arrival gate")
361 }
362 close(release)
363 if err := <-done; err != nil {
364 t.Fatal(err)
365 }
366 dec := json.NewDecoder(bytes.NewReader(out.Bytes()))
367 seenRejected := false
368 for {
369 var frame struct {
370 ID json.RawMessage `json:"id"`
371 Error *ErrorObject `json:"error"`
372 }
373 if err := dec.Decode(&frame); errors.Is(err, io.EOF) {
374 break
375 } else if err != nil {
376 t.Fatal(err)
377 }
378 if string(frame.ID) == "2" && frame.Error != nil && frame.Error.Message == "initialize incomplete" {
379 seenRejected = true
380 }
381 }
382 if !seenRejected {
383 t.Fatalf("frames = %s", out.String())
384 }
385 select {
386 case <-businessRan:
387 t.Fatal("business handler ran before initialize completed")
388 default:
389 }
390 }
391
392 func TestBeforeNotificationSynchronouslyRejectsWithoutResponse(t *testing.T) {
393 input := "{\"jsonrpc\":\"2.0\",\"method\":\"client/note\",\"params\":{}}\n"
394 var out bytes.Buffer
395 gateCalled := false
396 handlerCalled := false
397 conn := NewConn(strings.NewReader(input), &out, Options{
398 StrictJSONRPC: true,
399 BeforeNotification: func(method string, _ json.RawMessage) error {
400 gateCalled = method == "client/note"
401 return &RPCError{Code: ErrInvalidRequest, Message: "notifications forbidden"}
402 },
403 })
404 conn.HandleNotify("client/note", func(context.Context, json.RawMessage) { handlerCalled = true })
405 if err := conn.Serve(context.Background()); err != nil {
406 t.Fatal(err)
407 }
408 if !gateCalled || handlerCalled {
409 t.Fatalf("gateCalled=%v handlerCalled=%v", gateCalled, handlerCalled)
410 }
411 if out.Len() != 0 {
412 t.Fatalf("JSON-RPC notification rejection emitted a response: %s", out.String())
413 }
414 }
415
415 lines GO