返回 DeepSeek-Reasonix
fakehost_test.go
根目录 / sdk / go / fakehost_test.go
1 package extension
2
3 import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "log"
10 "sync"
11 "testing"
12 "time"
13 )
14
15 // fakeHost is a scriptable in-memory Reasonix host speaking raw JSON-RPC
16 // over two io.Pipes: it writes Host → Extension frames into the SDK's stdin
17 // pipe and reads the SDK's stdout pipe, answering Extension → Host requests
18 // with scripted handlers.
19 type fakeHost struct {
20 t *testing.T
21
22 toSDK *io.PipeWriter // host writes, SDK reads
23 fromSDK *io.PipeReader // SDK writes, host reads
24
25 writeMu sync.Mutex
26 nextID int64
27
28 pendingMu sync.Mutex
29 pending map[int64]chan hostResponse
30
31 strayMu sync.Mutex
32 strays []strayResponse
33 handlersMu sync.Mutex
34 handlers map[string]func(params json.RawMessage) (any, *hostError)
35 requestLog map[string][]json.RawMessage
36
37 notesMu sync.Mutex
38 notifications []hostNotification
39
40 readerDone chan struct{}
41 }
42
43 type hostError struct {
44 Code int
45 Message string
46 Data any
47 }
48
49 type hostResponse struct {
50 Result json.RawMessage
51 Err *hostError
52 }
53
54 // strayResponse is an SDK response with no matching pending host request —
55 // typically a -32600 rejection of a malformed raw frame.
56 type strayResponse struct {
57 ID json.RawMessage
58 Error *hostError
59 }
60
61 type hostNotification struct {
62 Method string
63 Params json.RawMessage
64 }
65
66 type hostFrame struct {
67 JSONRPC string `json:"jsonrpc"`
68 ID json.RawMessage `json:"id"`
69 Method string `json:"method"`
70 Params json.RawMessage `json:"params"`
71 Result json.RawMessage `json:"result"`
72 Error *hostErrorFrame `json:"error"`
73 }
74
75 type hostErrorFrame struct {
76 Code int `json:"code"`
77 Message string `json:"message"`
78 Data json.RawMessage `json:"data,omitempty"`
79 }
80
81 // serveWaiter caches Serve's result so both the test and its cleanup can
82 // observe it exactly once.
83 type serveWaiter struct {
84 ch chan error
85 mu sync.Mutex
86 err error
87 received bool
88 }
89
90 // wait blocks up to timeout for Serve's result; later calls return the
91 // cached value. ok is false on timeout.
92 func (w *serveWaiter) wait(timeout time.Duration) (err error, ok bool) {
93 w.mu.Lock()
94 if w.received {
95 w.mu.Unlock()
96 return w.err, true
97 }
98 w.mu.Unlock()
99 select {
100 case err := <-w.ch:
101 w.mu.Lock()
102 w.err = err
103 w.received = true
104 w.mu.Unlock()
105 return err, true
106 case <-time.After(timeout):
107 return nil, false
108 }
109 }
110
111 // startFakeHost launches Serve against a fake host and returns both. The
112 // host's read loop answers SDK requests until cleanup.
113 func startFakeHost(t *testing.T, h Handler, opts Options) (*fakeHost, *serveWaiter) {
114 t.Helper()
115 sdkStdinR, sdkStdinW := io.Pipe()
116 sdkStdoutR, sdkStdoutW := io.Pipe()
117 opts.Stdin = sdkStdinR
118 opts.Stdout = sdkStdoutW
119 if opts.Logger == nil {
120 opts.Logger = log.New(io.Discard, "", 0)
121 }
122 waiter := &serveWaiter{ch: make(chan error, 1)}
123 go func() { waiter.ch <- Serve(context.Background(), h, opts) }()
124 host := &fakeHost{
125 t: t,
126 toSDK: sdkStdinW,
127 fromSDK: sdkStdoutR,
128 pending: make(map[int64]chan hostResponse),
129 handlers: make(map[string]func(json.RawMessage) (any, *hostError)),
130 requestLog: make(map[string][]json.RawMessage),
131 readerDone: make(chan struct{}),
132 }
133 go host.readLoop()
134 t.Cleanup(func() {
135 _ = host.toSDK.Close()
136 _ = host.fromSDK.Close()
137 <-host.readerDone
138 if _, ok := waiter.wait(5 * time.Second); !ok {
139 t.Errorf("Serve did not return after the transport closed")
140 }
141 })
142 return host, waiter
143 }
144
145 // readLoop consumes every frame the SDK writes: responses resolve pending
146 // host requests, requests are routed to scripted handlers, notifications are
147 // recorded.
148 func (h *fakeHost) readLoop() {
149 defer close(h.readerDone)
150 scanner := bufio.NewScanner(h.fromSDK)
151 scanner.Buffer(make([]byte, 0, 64<<10), FrameBytes*2)
152 for scanner.Scan() {
153 line := scanner.Bytes()
154 var frame hostFrame
155 if err := json.Unmarshal(line, &frame); err != nil {
156 h.t.Errorf("fake host: undecodable SDK frame %q: %v", line, err)
157 continue
158 }
159 switch {
160 case frame.Method != "" && len(frame.ID) > 0:
161 h.serveSDKRequest(frame)
162 case frame.Method != "":
163 h.notesMu.Lock()
164 h.notifications = append(h.notifications, hostNotification{Method: frame.Method, Params: frame.Params})
165 h.notesMu.Unlock()
166 case len(frame.ID) > 0:
167 resp := hostResponse{Result: frame.Result}
168 if frame.Error != nil {
169 resp.Err = &hostError{Code: frame.Error.Code, Message: frame.Error.Message}
170 if len(frame.Error.Data) > 0 {
171 var data ProtocolErrorData
172 if err := json.Unmarshal(frame.Error.Data, &data); err == nil {
173 resp.Err.Data = data
174 }
175 }
176 }
177 var id int64
178 if err := json.Unmarshal(frame.ID, &id); err != nil {
179 h.recordStray(frame.ID, resp.Err)
180 continue
181 }
182 h.pendingMu.Lock()
183 ch := h.pending[id]
184 delete(h.pending, id)
185 h.pendingMu.Unlock()
186 if ch == nil {
187 h.recordStray(frame.ID, resp.Err)
188 continue
189 }
190 ch <- resp
191 }
192 }
193 }
194
195 func (h *fakeHost) recordStray(id json.RawMessage, herr *hostError) {
196 h.strayMu.Lock()
197 defer h.strayMu.Unlock()
198 h.strays = append(h.strays, strayResponse{ID: append(json.RawMessage(nil), id...), Error: herr})
199 }
200
201 // nextStray waits for one stray error response and returns it.
202 func (h *fakeHost) nextStray() strayResponse {
203 deadline := time.Now().Add(5 * time.Second)
204 for time.Now().Before(deadline) {
205 h.strayMu.Lock()
206 if len(h.strays) > 0 {
207 stray := h.strays[0]
208 h.strays = h.strays[1:]
209 h.strayMu.Unlock()
210 return stray
211 }
212 h.strayMu.Unlock()
213 time.Sleep(5 * time.Millisecond)
214 }
215 h.t.Fatalf("fake host: no stray response within 5s")
216 return strayResponse{}
217 }
218
219 func (h *fakeHost) serveSDKRequest(frame hostFrame) {
220 h.handlersMu.Lock()
221 handler := h.handlers[frame.Method]
222 h.requestLog[frame.Method] = append(h.requestLog[frame.Method], append(json.RawMessage(nil), frame.Params...))
223 h.handlersMu.Unlock()
224 var result any
225 var herr *hostError
226 if handler == nil {
227 herr = &hostError{Code: CodeMethodNotFound, Message: "method not found: " + frame.Method}
228 } else {
229 result, herr = handler(frame.Params)
230 }
231 var out []byte
232 if herr != nil {
233 errorFrame := map[string]any{"code": herr.Code, "message": herr.Message}
234 if herr.Data != nil {
235 errorFrame["data"] = herr.Data
236 }
237 out, _ = json.Marshal(map[string]any{"jsonrpc": "2.0", "id": json.RawMessage(frame.ID), "error": errorFrame})
238 } else {
239 raw, _ := json.Marshal(result)
240 out, _ = json.Marshal(map[string]any{"jsonrpc": "2.0", "id": json.RawMessage(frame.ID), "result": json.RawMessage(raw)})
241 }
242 h.writeLine(out)
243 }
244
245 // onRequest installs the scripted answerer for one Extension → Host method.
246 func (h *fakeHost) onRequest(method string, handler func(params json.RawMessage) (any, *hostError)) {
247 h.handlersMu.Lock()
248 defer h.handlersMu.Unlock()
249 h.handlers[method] = handler
250 }
251
252 // lastRawParams returns the raw params of the most recent Extension → Host
253 // request for method.
254 func (h *fakeHost) lastRawParams(t *testing.T, method string) json.RawMessage {
255 t.Helper()
256 h.handlersMu.Lock()
257 defer h.handlersMu.Unlock()
258 log := h.requestLog[method]
259 if len(log) == 0 {
260 t.Fatalf("fake host: no %s request recorded", method)
261 }
262 return log[len(log)-1]
263 }
264
265 // request sends one Host → Extension request and waits for its response.
266 func (h *fakeHost) request(method string, params any) hostResponse {
267 _, ch := h.startRequest(method, params)
268 select {
269 case resp := <-ch:
270 return resp
271 case <-time.After(10 * time.Second):
272 h.t.Fatalf("fake host: no response to %s", method)
273 return hostResponse{}
274 }
275 }
276
277 // startRequest sends one Host → Extension request without waiting; the
278 // response arrives on the returned channel.
279 func (h *fakeHost) startRequest(method string, params any) (int64, chan hostResponse) {
280 h.pendingMu.Lock()
281 h.nextID++
282 id := h.nextID
283 ch := make(chan hostResponse, 1)
284 h.pending[id] = ch
285 h.pendingMu.Unlock()
286 raw, _ := json.Marshal(params)
287 frame, _ := json.Marshal(map[string]any{
288 "jsonrpc": "2.0", "id": id, "method": method, "params": json.RawMessage(raw),
289 })
290 h.writeLine(frame)
291 return id, ch
292 }
293
294 // notify sends one Host → Extension notification.
295 func (h *fakeHost) notify(method string, params any) {
296 raw, _ := json.Marshal(params)
297 frame, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "method": method, "params": json.RawMessage(raw)})
298 h.writeLine(frame)
299 }
300
301 // writeRaw sends one unvalidated frame, for strict-frame violation tests.
302 func (h *fakeHost) writeRaw(frame []byte) { h.writeLine(frame) }
303
304 func (h *fakeHost) writeLine(frame []byte) {
305 h.writeMu.Lock()
306 defer h.writeMu.Unlock()
307 if _, err := h.toSDK.Write(append(frame, '\n')); err != nil {
308 h.t.Errorf("fake host: write to SDK: %v", err)
309 }
310 }
311
312 // handshake runs the standard initialize + initialized sequence and returns
313 // the decoded initialize result.
314 func (h *fakeHost) handshake(t *testing.T) InitializeResult {
315 t.Helper()
316 resp := h.request(MethodExtensionInitialize, InitializeParams{
317 ProtocolVersion: ProtocolVersion,
318 ProtocolID: ProtocolID,
319 Manifest: ManifestExpectation{Intercepts: InterceptEvents(), Capabilities: []string{"providers", "ui"}},
320 Session: SessionContext{SessionID: "sess-1", WorkspaceRoot: "/repo", Generation: 7},
321 Capabilities: HostCapabilities{ContentRefs: true, UIHost: UIHostHeadless, ProtocolVersion: ProtocolVersion},
322 })
323 if resp.Err != nil {
324 t.Fatalf("initialize failed: %+v", resp.Err)
325 }
326 var result InitializeResult
327 if err := json.Unmarshal(resp.Result, &result); err != nil {
328 t.Fatalf("decode initialize result: %v", err)
329 }
330 h.notify(MethodExtensionInitialized, InitializedParams{})
331 return result
332 }
333
334 // nextNotification waits for one SDK notification with the given method.
335 func (h *fakeHost) nextNotification(method string) hostNotification {
336 deadline := time.Now().Add(5 * time.Second)
337 for time.Now().Before(deadline) {
338 h.notesMu.Lock()
339 for i, note := range h.notifications {
340 if note.Method == method {
341 h.notifications = append(h.notifications[:i], h.notifications[i+1:]...)
342 h.notesMu.Unlock()
343 return note
344 }
345 }
346 h.notesMu.Unlock()
347 time.Sleep(5 * time.Millisecond)
348 }
349 h.t.Fatalf("fake host: no %s notification within 5s", method)
350 return hostNotification{}
351 }
352
353 // notificationsSnapshot returns all recorded SDK notifications.
354 func (h *fakeHost) notificationsSnapshot() []hostNotification {
355 h.notesMu.Lock()
356 defer h.notesMu.Unlock()
357 return append([]hostNotification(nil), h.notifications...)
358 }
359
360 // streamNotifications returns all stream/chunk and stream/end notifications
361 // recorded so far, in arrival order, decoded.
362 func (h *fakeHost) streamNotifications() (chunks []StreamChunkParams, ends []StreamEndParams) {
363 for _, note := range h.notificationsSnapshot() {
364 switch note.Method {
365 case MethodExtensionProviderStreamChunk:
366 var p StreamChunkParams
367 if err := json.Unmarshal(note.Params, &p); err == nil {
368 chunks = append(chunks, p)
369 }
370 case MethodExtensionProviderStreamEnd:
371 var p StreamEndParams
372 if err := json.Unmarshal(note.Params, &p); err == nil {
373 ends = append(ends, p)
374 }
375 }
376 }
377 return chunks, ends
378 }
379
380 // waitStreamEnd polls until one stream/end notification arrives and returns
381 // it decoded. It does not consume anything.
382 func (h *fakeHost) waitStreamEnd() StreamEndParams {
383 deadline := time.Now().Add(5 * time.Second)
384 for time.Now().Before(deadline) {
385 _, ends := h.streamNotifications()
386 if len(ends) > 0 {
387 return ends[len(ends)-1]
388 }
389 time.Sleep(5 * time.Millisecond)
390 }
391 h.t.Fatalf("fake host: no stream/end within 5s")
392 return StreamEndParams{}
393 }
394
395 // testHandler is a Handler returning a fixed declaration.
396 type testHandler struct {
397 result *InitializeResult
398 err error
399 seen *InitializeParams
400 }
401
402 func (h *testHandler) Initialize(_ context.Context, p InitializeParams) (*InitializeResult, error) {
403 if h.seen != nil {
404 *h.seen = p
405 }
406 if h.err != nil {
407 return nil, h.err
408 }
409 return h.result, nil
410 }
411
412 // HandlerFunc adapts a function to the Handler interface.
413 type HandlerFunc func(ctx context.Context, p InitializeParams) (*InitializeResult, error)
414
415 // Initialize implements Handler.
416 func (f HandlerFunc) Initialize(ctx context.Context, p InitializeParams) (*InitializeResult, error) {
417 return f(ctx, p)
418 }
419
420 func basicHandler() *testHandler {
421 return &testHandler{result: &InitializeResult{
422 Name: "test-ext", Version: "0.1.0",
423 Subscriptions: []string{"tool.before"},
424 }}
425 }
426
427 var errTest = errors.New("test error")
428
428 lines GO