返回 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 TestWriteFileOverlayAppliesWrite(t *testing.T) {
75 dir := t.TempDir()
76 path := filepath.Join(dir, "c.go")
77 overlay := &fakeOverlay{writes: map[string]string{}}
78 wf := writeFile{workDir: dir, roots: realRoots([]string{dir}), overlay: overlay}
79
80 args, _ := json.Marshal(map[string]string{"path": "c.go", "content": "hello"})
81 out, err := wf.Execute(context.Background(), json.RawMessage(args))
82 if err != nil {
83 t.Fatalf("Execute: %v", err)
84 }
85 if overlay.writes[path] != "hello" {
86 t.Fatalf("overlay writes = %v, want %s→hello", overlay.writes, path)
87 }
88 if _, statErr := os.Stat(path); statErr == nil {
89 t.Fatal("overlay-handled write must not also write the local disk")
90 }
91 if !strings.Contains(out, "wrote 5 bytes") {
92 t.Fatalf("output = %q", out)
93 }
94
95 // A client-side write failure surfaces instead of silently double-applying.
96 overlay.wErr = fmt.Errorf("readonly buffer")
97 if _, err := wf.Execute(context.Background(), json.RawMessage(args)); err == nil {
98 t.Fatal("overlay write error must surface")
99 }
100 }
101
102 func TestWriteFileOverlaySkipsNonUTF8(t *testing.T) {
103 dir := t.TempDir()
104 path := filepath.Join(dir, "utf16.txt")
105 // UTF-16LE BOM + "hi" — the overlay is text-only, so this file must stay on
106 // the local encoding-preserving path.
107 if err := os.WriteFile(path, []byte{0xFF, 0xFE, 'h', 0, 'i', 0}, 0o644); err != nil {
108 t.Fatal(err)
109 }
110 overlay := &fakeOverlay{writes: map[string]string{}}
111 wf := writeFile{workDir: dir, roots: realRoots([]string{dir}), overlay: overlay}
112 args, _ := json.Marshal(map[string]string{"path": "utf16.txt", "content": "changed"})
113 if _, err := wf.Execute(context.Background(), json.RawMessage(args)); err != nil {
114 t.Fatalf("Execute: %v", err)
115 }
116 if len(overlay.writes) != 0 {
117 t.Fatalf("non-UTF-8 target must bypass the overlay; writes = %v", overlay.writes)
118 }
119 b, err := os.ReadFile(path)
120 if err != nil || len(b) < 2 || b[0] != 0xFF || b[1] != 0xFE {
121 t.Fatalf("local write must preserve the UTF-16 BOM; got % x, %v", b, err)
122 }
123 }
124
125 // fakeTerminal records commands and returns a scripted result.
126 type fakeTerminal struct {
127 out string
128 ok bool
129 err error
130 called []string
131 }
132
133 func (f *fakeTerminal) RunCommand(_ context.Context, command, _ string, _ time.Duration, _ map[string]string) (string, bool, error) {
134 f.called = append(f.called, command)
135 return f.out, f.ok, f.err
136 }
137
138 func TestBashRoutesToClientTerminal(t *testing.T) {
139 term := &fakeTerminal{out: "client says hi", ok: true}
140 b := bash{workDir: t.TempDir(), terminal: term}
141 out, err := b.Execute(context.Background(), json.RawMessage(`{"command":"echo hi"}`))
142 if err != nil || out != "client says hi" {
143 t.Fatalf("Execute = %q, %v", out, err)
144 }
145 if len(term.called) != 1 || term.called[0] != "echo hi" {
146 t.Fatalf("terminal calls = %v", term.called)
147 }
148 }
149
150 func TestBashTerminalFallsBackWhenUnhandled(t *testing.T) {
151 term := &fakeTerminal{ok: false}
152 b := bash{workDir: t.TempDir(), terminal: term}
153 out, err := b.Execute(context.Background(), json.RawMessage(`{"command":"printf local"}`))
154 if err != nil || !strings.Contains(out, "local") {
155 t.Fatalf("unhandled terminal must fall back to local execution; got %q, %v", out, err)
156 }
157 }
158
159 func TestBashTerminalSkippedWhenSandboxEnforced(t *testing.T) {
160 term := &fakeTerminal{out: "must not run", ok: true}
161 b := bash{workDir: t.TempDir(), sb: sandbox.Spec{Mode: "enforce"}, terminal: term}
162 // The command itself may fail (no sandbox binary in the test env); the
163 // assertion is only that the client terminal was never consulted.
164 _, _ = b.Execute(context.Background(), json.RawMessage(`{"command":"echo hi"}`))
165 if len(term.called) != 0 {
166 t.Fatalf("enforced sandbox must never route to the client terminal; calls = %v", term.called)
167 }
168 }
169
170 func TestBashTerminalSkippedWhenEnvFilteringEnabled(t *testing.T) {
171 secrets.SetFilterSubprocessEnv(true)
172 t.Cleanup(func() { secrets.SetFilterSubprocessEnv(false) })
173 term := &fakeTerminal{out: "must not run", ok: true}
174 b := bash{workDir: t.TempDir(), terminal: term}
175 // The client terminal spawns with its own unfiltered environment, so an
176 // enabled [secrets].filter_subprocess_env must force local execution.
177 out, err := b.Execute(context.Background(), json.RawMessage(`{"command":"printf local"}`))
178 if err != nil || !strings.Contains(out, "local") {
179 t.Fatalf("env filtering must fall back to local execution; got %q, %v", out, err)
180 }
181 if len(term.called) != 0 {
182 t.Fatalf("env filtering must never route to the client terminal; calls = %v", term.called)
183 }
184 }
185
185 lines GO