返回 DeepSeek-Reasonix
raw_test.go
根目录 / internal / extension / conformance / raw_test.go
1 package conformance
2
3 import (
4 "bufio"
5 "bytes"
6 "encoding/json"
7 "io"
8 "os/exec"
9 "strings"
10 "sync"
11 "testing"
12 "time"
13
14 "reasonix/internal/extension/protocol"
15 "reasonix/internal/extension/rpcwire"
16 )
17
18 // rawSidecar drives the example binary directly over hand-written JSON-RPC
19 // frames, for the transport-level conformance cases the typed host client
20 // cannot produce (unregistered methods, oversized frames, exit statuses).
21 type rawSidecar struct {
22 t *testing.T
23 cmd *exec.Cmd
24 stdin io.WriteCloser
25 stdout *bufio.Reader
26 stderr *bytes.Buffer
27
28 nextID int64
29 waitOnce sync.Once
30 waitErr error
31 }
32
33 type rawFrame struct {
34 ID json.RawMessage `json:"id"`
35 Method string `json:"method"`
36 Result json.RawMessage `json:"result"`
37 Error *struct {
38 Code int `json:"code"`
39 Message string `json:"message"`
40 Data json.RawMessage `json:"data"`
41 } `json:"error"`
42 }
43
44 func startRawSidecar(t *testing.T) *rawSidecar {
45 t.Helper()
46 cmd := exec.Command(examplePath)
47 stdin, err := cmd.StdinPipe()
48 if err != nil {
49 t.Fatalf("StdinPipe: %v", err)
50 }
51 stdout, err := cmd.StdoutPipe()
52 if err != nil {
53 t.Fatalf("StdoutPipe: %v", err)
54 }
55 stderr := &bytes.Buffer{}
56 cmd.Stderr = stderr
57 if err := cmd.Start(); err != nil {
58 t.Fatalf("start example: %v", err)
59 }
60 r := &rawSidecar{t: t, cmd: cmd, stdin: stdin, stdout: bufio.NewReader(stdout), stderr: stderr}
61 t.Cleanup(func() {
62 if r.cmd.Process != nil {
63 _ = r.cmd.Process.Kill()
64 }
65 _ = r.wait()
66 })
67 return r
68 }
69
70 // wait reaps the process exactly once.
71 func (r *rawSidecar) wait() error {
72 r.waitOnce.Do(func() { r.waitErr = r.cmd.Wait() })
73 return r.waitErr
74 }
75
76 // waitWithin reaps the process inside the budget.
77 func (r *rawSidecar) waitWithin(what string, budget time.Duration) error {
78 done := make(chan error, 1)
79 go func() { done <- r.wait() }()
80 select {
81 case err := <-done:
82 return err
83 case <-time.After(budget):
84 r.t.Fatalf("process did not exit within %s (%s)", budget, what)
85 return nil
86 }
87 }
88
89 // send marshals and writes one frame.
90 func (r *rawSidecar) send(v any) {
91 r.t.Helper()
92 raw, err := json.Marshal(v)
93 if err != nil {
94 r.t.Fatalf("marshal frame: %v", err)
95 }
96 if _, err := r.stdin.Write(append(raw, '\n')); err != nil {
97 r.t.Fatalf("write frame: %v", err)
98 }
99 }
100
101 // readFrame reads one NDJSON frame.
102 func (r *rawSidecar) readFrame() (rawFrame, error) {
103 line, err := r.stdout.ReadBytes('\n')
104 if err != nil {
105 return rawFrame{}, err
106 }
107 var frame rawFrame
108 if err := json.Unmarshal(line, &frame); err != nil {
109 return rawFrame{}, err
110 }
111 return frame, nil
112 }
113
114 // call writes one request and returns its response, failing on any
115 // interleaved host-bound traffic (none is expected in these scenarios).
116 func (r *rawSidecar) call(method string, params any) rawFrame {
117 r.t.Helper()
118 r.nextID++
119 id := r.nextID
120 r.send(map[string]any{"jsonrpc": "2.0", "id": id, "method": method, "params": params})
121 for {
122 frame, err := r.readFrame()
123 if err != nil {
124 r.t.Fatalf("read answer for %s: %v (stderr: %s)", method, err, strings.TrimSpace(r.stderr.String()))
125 }
126 if frame.Method != "" {
127 r.t.Fatalf("extension sent an unexpected host-bound request %q", frame.Method)
128 }
129 var gotID int64
130 if err := json.Unmarshal(frame.ID, &gotID); err == nil && gotID == id {
131 return frame
132 }
133 }
134 }
135
136 // handshake runs the initialize exchange and opens the barrier.
137 func (r *rawSidecar) handshake() {
138 r.t.Helper()
139 frame := r.call("extension/initialize", protocol.InitializeParams{
140 ProtocolVersion: protocol.ProtocolVersion,
141 ProtocolID: protocol.ProtocolID,
142 Session: protocol.SessionContext{SessionID: "raw-sess", WorkspaceRoot: "/ws", Generation: 1},
143 Capabilities: protocol.HostCapabilities{ContentRefs: true, UIHost: protocol.UIHostHeadless, ProtocolVersion: protocol.ProtocolVersion},
144 })
145 if frame.Error != nil {
146 r.t.Fatalf("initialize answered with an error: %+v", frame.Error)
147 }
148 var result protocol.InitializeResult
149 if err := json.Unmarshal(frame.Result, &result); err != nil {
150 r.t.Fatalf("decode initialize result: %v", err)
151 }
152 r.send(map[string]any{"jsonrpc": "2.0", "method": "extension/initialized", "params": map[string]any{}})
153 }
154
155 // TestUnknownMethod sends a request for an unregistered method past the
156 // handshake: the SDK must answer with the JSON-RPC method-not-found code and
157 // the frozen unknown_method reason.
158 func TestUnknownMethod(t *testing.T) {
159 r := startRawSidecar(t)
160 r.handshake()
161
162 frame := r.call("extension/bogus", map[string]any{})
163 if frame.Error == nil {
164 t.Fatalf("unknown method answered with result %s", string(frame.Result))
165 }
166 if frame.Error.Code != rpcwire.ErrMethodNotFound {
167 t.Fatalf("error code = %d, want %d", frame.Error.Code, rpcwire.ErrMethodNotFound)
168 }
169 var data protocol.ProtocolErrorData
170 if err := json.Unmarshal(frame.Error.Data, &data); err != nil {
171 t.Fatalf("error data does not decode: %v", err)
172 }
173 if data.Reason != protocol.ErrUnknownMethod {
174 t.Fatalf("error reason = %q, want %q", data.Reason, protocol.ErrUnknownMethod)
175 }
176 }
177
178 // TestOversizedFrame sends one NDJSON line beyond the frozen 8 MiB frame
179 // budget: the SDK must fail the connection and exit non-zero.
180 func TestOversizedFrame(t *testing.T) {
181 r := startRawSidecar(t)
182 line := strings.Repeat("a", protocol.FrameBytes+1024)
183 go func() {
184 // The write may fail with EPIPE once the SDK drops the connection;
185 // either way the connection error is what is being asserted.
186 _, _ = io.WriteString(r.stdin, line+"\n")
187 }()
188 if err := r.waitWithin("oversized frame", 15*time.Second); err == nil {
189 t.Fatal("process exited 0 after an oversized frame")
190 }
191 if !strings.Contains(r.stderr.String(), "frame") {
192 t.Fatalf("stderr does not mention the frame violation: %q", strings.TrimSpace(r.stderr.String()))
193 }
194 }
195
196 // TestBoundedShutdownExitZero runs the orderly shutdown: the example answers
197 // accepted:true and the process exits 0 inside the budget.
198 func TestBoundedShutdownExitZero(t *testing.T) {
199 r := startRawSidecar(t)
200 r.handshake()
201
202 frame := r.call("extension/shutdown", protocol.ShutdownParams{TimeoutMillis: 5000})
203 if frame.Error != nil {
204 t.Fatalf("shutdown answered with an error: %+v", frame.Error)
205 }
206 var result protocol.ShutdownResult
207 if err := json.Unmarshal(frame.Result, &result); err != nil {
208 t.Fatalf("decode shutdown result: %v", err)
209 }
210 if !result.Accepted {
211 t.Fatalf("shutdown not accepted: %+v", result)
212 }
213 // The real host closes the sidecar's stdin right after the shutdown
214 // request (proc.close); the SDK then sees EOF, Serve returns nil, and
215 // the process exits 0.
216 if err := r.stdin.Close(); err != nil {
217 t.Fatalf("close stdin: %v", err)
218 }
219 if err := r.waitWithin("shutdown", 10*time.Second); err != nil {
220 t.Fatalf("process exit = %v, want 0 (stderr: %s)", err, strings.TrimSpace(r.stderr.String()))
221 }
222 }
223
223 lines GO