返回 DeepSeek-Reasonix
clientio_test.go
根目录 / internal / tool / builtin / clientio_test.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12
13 "reasonix/internal/sandbox"
14 "reasonix/internal/secrets"
15 )
16
17 // fakeOverlay serves a fixed path→content map and records writes.
18 type fakeOverlay struct {
19 files map[string]string
20 writes map[string]string
21 wErr error
22 }
23
24 func (f *fakeOverlay) ReadTextFile(_ context.Context, path string) (string, bool) {
25 content, ok := f.files[path]
26 return content, ok
27 }
28
29 func (f *fakeOverlay) WriteTextFile(_ context.Context, path, content string) (bool, error) {
30 if f.writes == nil {
31 return false, nil
32 }
33 if f.wErr != nil {
34 return true, f.wErr
35 }
36 f.writes[path] = content
37 return true, nil
38 }
39
40 func TestReadFileOverlayServesBufferContent(t *testing.T) {
41 dir := t.TempDir()
42 path := filepath.Join(dir, "a.go")
43 if err := os.WriteFile(path, []byte("disk line\n"), 0o644); err != nil {
44 t.Fatal(err)
45 }
46 overlay := &fakeOverlay{files: map[string]string{path: "buffer line one\nbuffer line two\n"}}
47 rf := readFile{workDir: dir, overlay: overlay}
48
49 out, err := rf.Execute(context.Background(), json.RawMessage(`{"path":"a.go"}`))
50 if err != nil {
51 t.Fatalf("Execute: %v", err)
52 }
53 if !strings.Contains(out, "buffer line one") || strings.Contains(out, "disk line") {
54 t.Fatalf("overlay content should win over disk; got:\n%s", out)
55 }
56 if !strings.Contains(out, "1→") && !strings.Contains(out, "1\t") {
57 t.Fatalf("overlay content must keep the numbered-line rendering; got:\n%s", out)
58 }
59 }
60
61 func TestReadFileOverlayFallsBackToDisk(t *testing.T) {
62 dir := t.TempDir()
63 path := filepath.Join(dir, "b.go")
64 if err := os.WriteFile(path, []byte("disk only\n"), 0o644); err != nil {
65 t.Fatal(err)
66 }
67 rf := readFile{workDir: dir, overlay: &fakeOverlay{files: map[string]string{}}}
68 out, err := rf.Execute(context.Background(), json.RawMessage(`{"path":"b.go"}`))
69 if err != nil || !strings.Contains(out, "disk only") {
70 t.Fatalf("overlay miss must fall back to disk; got %q, %v", out, err)
71 }
72 }
73
74 func TestGrepOverlayServesUnsavedBufferContent(t *testing.T) {
75 dir := t.TempDir()
76 path := filepath.Join(dir, "overlay-only.go")
77 overlay := &fakeOverlay{files: map[string]string{
78 path: "package overlay\n// BUFFER_NEEDLE exists only in the unsaved buffer\n",
79 }}
80 grep := byName(Workspace{
81 Dir: dir,
82 // Overlay-backed single-file searches must not delegate to ripgrep,
83 // which can only see the disk snapshot.
84 Search: SearchSpec{RgPath: filepath.Join(dir, "must-not-run-rg")},
85 FileOverlay: overlay,
86 }.Tools("grep"))["grep"]
87
88 out, err := grep.Execute(context.Background(), json.RawMessage(`{"pattern":"BUFFER_NEEDLE","path":"overlay-only.go"}`))
89 if err != nil {
90 t.Fatalf("Execute: %v", err)
91 }
92 if !strings.Contains(out, "BUFFER_NEEDLE") || !strings.Contains(out, ":2:") {
93 t.Fatalf("grep did not search the unsaved overlay content:\n%s", out)
94 }
95 }
96
97 func TestGrepOverlayDoesNotBypassReadConfinement(t *testing.T) {
98 dir := t.TempDir()
99 path := filepath.Join(dir, "secret.txt")
100 overlay := &fakeOverlay{files: map[string]string{path: "OVERLAY_SECRET\n"}}
101 grep := grepTool{workDir: dir, forbidRoots: realRoots([]string{dir}), overlay: overlay}
102
103 out, err := grep.Execute(context.Background(), json.RawMessage(`{"pattern":"OVERLAY_SECRET","path":"secret.txt"}`))
104 if err == nil && strings.Contains(out, "OVERLAY_SECRET") {
105 t.Fatalf("forbidden overlay content escaped confinement: %q", out)
106 }
107 }
108
109 func TestGrepOverlayFallsBackToDisk(t *testing.T) {
110 dir := t.TempDir()
111 path := filepath.Join(dir, "disk-only.txt")
112 if err := os.WriteFile(path, []byte("DISK_NEEDLE\n"), 0o644); err != nil {
113 t.Fatal(err)
114 }
115 grep := byName(Workspace{Dir: dir, FileOverlay: &fakeOverlay{files: map[string]string{}}}.Tools("grep"))["grep"]
116
117 out, err := grep.Execute(context.Background(), json.RawMessage(`{"pattern":"DISK_NEEDLE","path":"disk-only.txt"}`))
118 if err != nil || !strings.Contains(out, "DISK_NEEDLE") {
119 t.Fatalf("overlay miss did not fall back to disk: out=%q err=%v", out, err)
120 }
121 }
122
123 func TestWriteFileOverlayAppliesWrite(t *testing.T) {
124 dir := t.TempDir()
125 path := filepath.Join(dir, "c.go")
126 overlay := &fakeOverlay{writes: map[string]string{}}
127 receipts := 0
128 wf := writeFile{workDir: dir, roots: realRoots([]string{dir}), overlay: overlay, receipt: func(gotPath string, hadPrior bool, prior []byte) {
129 receipts++
130 if gotPath != path || hadPrior || len(prior) != 0 {
131 t.Fatalf("overlay receipt = path:%q hadPrior:%v prior:%q", gotPath, hadPrior, prior)
132 }
133 }}
134
135 args, _ := json.Marshal(map[string]string{"path": "c.go", "content": "hello"})
136 out, err := wf.Execute(context.Background(), json.RawMessage(args))
137 if err != nil {
138 t.Fatalf("Execute: %v", err)
139 }
140 if overlay.writes[path] != "hello" {
141 t.Fatalf("overlay writes = %v, want %s→hello", overlay.writes, path)
142 }
143 if _, statErr := os.Stat(path); statErr == nil {
144 t.Fatal("overlay-handled write must not also write the local disk")
145 }
146 if !strings.Contains(out, "wrote 5 bytes") {
147 t.Fatalf("output = %q", out)
148 }
149 if receipts != 1 {
150 t.Fatalf("receipts = %d, want 1", receipts)
151 }
152
153 // A client-side write failure surfaces instead of silently double-applying.
154 overlay.wErr = fmt.Errorf("readonly buffer")
155 if _, err := wf.Execute(context.Background(), json.RawMessage(args)); err == nil {
156 t.Fatal("overlay write error must surface")
157 }
158 if receipts != 1 {
159 t.Fatal("failed overlay writes must not record a receipt")
160 }
161 }
162
163 func TestWriteFileOverlaySkipsNonUTF8(t *testing.T) {
164 dir := t.TempDir()
165 path := filepath.Join(dir, "utf16.txt")
166 // UTF-16LE BOM + "hi" — the overlay is text-only, so this file must stay on
167 // the local encoding-preserving path.
168 if err := os.WriteFile(path, []byte{0xFF, 0xFE, 'h', 0, 'i', 0}, 0o644); err != nil {
169 t.Fatal(err)
170 }
171 overlay := &fakeOverlay{writes: map[string]string{}}
172 wf := writeFile{workDir: dir, roots: realRoots([]string{dir}), overlay: overlay}
173 args, _ := json.Marshal(map[string]string{"path": "utf16.txt", "content": "changed"})
174 if _, err := wf.Execute(context.Background(), json.RawMessage(args)); err != nil {
175 t.Fatalf("Execute: %v", err)
176 }
177 if len(overlay.writes) != 0 {
178 t.Fatalf("non-UTF-8 target must bypass the overlay; writes = %v", overlay.writes)
179 }
180 b, err := os.ReadFile(path)
181 if err != nil || len(b) < 2 || b[0] != 0xFF || b[1] != 0xFE {
182 t.Fatalf("local write must preserve the UTF-16 BOM; got % x, %v", b, err)
183 }
184 }
185
186 // fakeTerminal records commands and returns a scripted result.
187 type fakeTerminal struct {
188 out string
189 ok bool
190 err error
191 called []string
192 }
193
194 func (f *fakeTerminal) RunCommand(_ context.Context, command, _ string, _ time.Duration, _ map[string]string) (string, bool, error) {
195 f.called = append(f.called, command)
196 return f.out, f.ok, f.err
197 }
198
199 func TestBashRoutesToClientTerminal(t *testing.T) {
200 requirePOSIXShellTest(t)
201 term := &fakeTerminal{out: "client says hi", ok: true}
202 b := bash{workDir: t.TempDir(), terminal: term}
203 out, err := b.Execute(fullAccessBashTestContext(t.Context()), json.RawMessage(`{"command":"echo hi"}`))
204 if err != nil || out != "client says hi" {
205 t.Fatalf("Execute = %q, %v", out, err)
206 }
207 if len(term.called) != 1 || term.called[0] != "echo hi" {
208 t.Fatalf("terminal calls = %v", term.called)
209 }
210 }
211
212 func TestBashTerminalFallsBackWhenUnhandled(t *testing.T) {
213 term := &fakeTerminal{ok: false}
214 b := bash{workDir: t.TempDir(), terminal: term}
215 out, err := b.Execute(fullAccessBashTestContext(t.Context()), json.RawMessage(`{"command":"printf local"}`))
216 if err != nil || !strings.Contains(out, "local") {
217 t.Fatalf("unhandled terminal must fall back to local execution; got %q, %v", out, err)
218 }
219 }
220
221 func TestBashTerminalSkippedWhenSandboxEnforced(t *testing.T) {
222 term := &fakeTerminal{out: "must not run", ok: true}
223 b := bash{workDir: t.TempDir(), sb: sandbox.Spec{Mode: "enforce"}, terminal: term}
224 // The command itself may fail (no sandbox binary in the test env); the
225 // assertion is only that the client terminal was never consulted.
226 _, _ = b.Execute(context.Background(), json.RawMessage(`{"command":"echo hi"}`))
227 if len(term.called) != 0 {
228 t.Fatalf("enforced sandbox must never route to the client terminal; calls = %v", term.called)
229 }
230 }
231
232 func TestBashTerminalSkippedWhenEnvFilteringEnabled(t *testing.T) {
233 secrets.SetFilterSubprocessEnv(true)
234 t.Cleanup(func() { secrets.SetFilterSubprocessEnv(false) })
235 term := &fakeTerminal{out: "must not run", ok: true}
236 b := bash{workDir: t.TempDir(), terminal: term}
237 // The client terminal spawns with its own unfiltered environment, so an
238 // enabled [secrets].filter_subprocess_env must force local execution.
239 out, err := b.Execute(fullAccessBashTestContext(t.Context()), json.RawMessage(`{"command":"printf local"}`))
240 if err != nil || !strings.Contains(out, "local") {
241 t.Fatalf("env filtering must fall back to local execution; got %q, %v", out, err)
242 }
243 if len(term.called) != 0 {
244 t.Fatalf("env filtering must never route to the client terminal; calls = %v", term.called)
245 }
246 }
247
247 lines GO