1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "testing"
11
12 "github.com/patriceckhart/zot/packages/provider"
13)
14
15func mustJSON(t *testing.T, v any) json.RawMessage {
16 t.Helper()
17 b, err := json.Marshal(v)
18 if err != nil {
19 t.Fatal(err)
20 }
21 return b
22}
23
24func TestReadText(t *testing.T) {
25 dir := t.TempDir()
26 p := filepath.Join(dir, "a.txt")
27 if err := os.WriteFile(p, []byte("hello\nworld\n"), 0o644); err != nil {
28 t.Fatal(err)
29 }
30 tool := &ReadTool{CWD: dir}
31 res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "a.txt"}), nil)
32 if err != nil {
33 t.Fatal(err)
34 }
35 got := res.Content[0].(provider.TextBlock).Text
36 if !strings.Contains(got, "hello") || !strings.Contains(got, "world") {
37 t.Fatalf("got %q", got)
38 }
39}
40
41func TestReadImageMimeFromContentNotExtension(t *testing.T) {
42 // A file named .png whose bytes are actually JPEG. The MIME must be
43 // sniffed from the content (image/jpeg), not the extension, or
44 // providers that validate the declared media type reject the request.
45 dir := t.TempDir()
46 p := filepath.Join(dir, "shot.png")
47 jpegBytes := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 'J', 'F', 'I', 'F'}
48 if err := os.WriteFile(p, jpegBytes, 0o644); err != nil {
49 t.Fatal(err)
50 }
51 tool := &ReadTool{CWD: dir}
52 res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "shot.png"}), nil)
53 if err != nil {
54 t.Fatal(err)
55 }
56 img, ok := res.Content[0].(provider.ImageBlock)
57 if !ok {
58 t.Fatalf("expected ImageBlock, got %T", res.Content[0])
59 }
60 if img.MimeType != "image/jpeg" {
61 t.Fatalf("mime from extension not corrected: got %s want image/jpeg", img.MimeType)
62 }
63}
64
65func TestReadOffsetLimit(t *testing.T) {
66 dir := t.TempDir()
67 p := filepath.Join(dir, "a.txt")
68 os.WriteFile(p, []byte("1\n2\n3\n4\n5\n"), 0o644)
69 tool := &ReadTool{CWD: dir}
70 res, _ := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "a.txt", "offset": 2, "limit": 2}), nil)
71 got := res.Content[0].(provider.TextBlock).Text
72 // Current output format is raw bytes (no embedded line numbers):
73 // the tui draws its own gutter from the `start_line` detail.
74 if got != "2\n3\n" {
75 t.Fatalf("want \"2\\n3\\n\", got %q", got)
76 }
77 if start, ok := res.Details.(map[string]any)["start_line"]; !ok || start != 2 {
78 t.Errorf("start_line detail want 2, got %v", start)
79 }
80}
81
82func TestReadBinaryRejected(t *testing.T) {
83 dir := t.TempDir()
84 p := filepath.Join(dir, "b.bin")
85 os.WriteFile(p, []byte{0x00, 0x01, 0x02}, 0o644)
86 tool := &ReadTool{CWD: dir}
87 if _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "b.bin"}), nil); err == nil {
88 t.Fatal("want binary rejection")
89 }
90}
91
92func TestWriteCreatesDirs(t *testing.T) {
93 dir := t.TempDir()
94 tool := &WriteTool{CWD: dir}
95 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "sub/a.txt", "content": "hi"}), nil)
96 if err != nil {
97 t.Fatal(err)
98 }
99 b, err := os.ReadFile(filepath.Join(dir, "sub", "a.txt"))
100 if err != nil {
101 t.Fatal(err)
102 }
103 if string(b) != "hi" {
104 t.Fatalf("got %q", string(b))
105 }
106}
107
108func TestEditSingle(t *testing.T) {
109 dir := t.TempDir()
110 p := filepath.Join(dir, "a.txt")
111 os.WriteFile(p, []byte("hello world\n"), 0o644)
112 tool := &EditTool{CWD: dir}
113 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
114 "path": "a.txt",
115 "edits": []map[string]any{{"oldText": "world", "newText": "gopher"}},
116 }), nil)
117 if err != nil {
118 t.Fatal(err)
119 }
120 b, _ := os.ReadFile(p)
121 if string(b) != "hello gopher\n" {
122 t.Fatalf("got %q", string(b))
123 }
124}
125
126func TestEditMultiple(t *testing.T) {
127 dir := t.TempDir()
128 p := filepath.Join(dir, "a.txt")
129 os.WriteFile(p, []byte("a\nb\nc\n"), 0o644)
130 tool := &EditTool{CWD: dir}
131 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
132 "path": "a.txt",
133 "edits": []map[string]any{
134 {"oldText": "a", "newText": "A"},
135 {"oldText": "c", "newText": "C"},
136 },
137 }), nil)
138 if err != nil {
139 t.Fatal(err)
140 }
141 b, _ := os.ReadFile(p)
142 if string(b) != "A\nb\nC\n" {
143 t.Fatalf("got %q", string(b))
144 }
145}
146
147func TestEditAmbiguous(t *testing.T) {
148 dir := t.TempDir()
149 p := filepath.Join(dir, "a.txt")
150 os.WriteFile(p, []byte("x\nx\n"), 0o644)
151 tool := &EditTool{CWD: dir}
152 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
153 "path": "a.txt",
154 "edits": []map[string]any{{"oldText": "x", "newText": "y"}},
155 }), nil)
156 if err == nil {
157 t.Fatal("want ambiguous error")
158 }
159}
160
161func TestEditPreservesCRLF(t *testing.T) {
162 dir := t.TempDir()
163 p := filepath.Join(dir, "a.txt")
164 os.WriteFile(p, []byte("hello\r\nworld\r\n"), 0o644)
165 tool := &EditTool{CWD: dir}
166 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
167 "path": "a.txt",
168 "edits": []map[string]any{{"oldText": "world", "newText": "gopher"}},
169 }), nil)
170 if err != nil {
171 t.Fatal(err)
172 }
173 b, _ := os.ReadFile(p)
174 if string(b) != "hello\r\ngopher\r\n" {
175 t.Fatalf("got %q", string(b))
176 }
177}
178
179func TestBashSuccess(t *testing.T) {
180 if runtime.GOOS == "windows" {
181 t.Skip("posix shell only")
182 }
183 tool := &BashTool{CWD: t.TempDir()}
184 res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"command": "echo hi"}), nil)
185 if err != nil {
186 t.Fatal(err)
187 }
188 got := res.Content[0].(provider.TextBlock).Text
189 if !strings.Contains(got, "hi") || !strings.Contains(got, "[exit 0]") {
190 t.Fatalf("got %q", got)
191 }
192 if res.IsError {
193 t.Fatal("unexpected error flag")
194 }
195}
196
197func TestBashFailure(t *testing.T) {
198 if runtime.GOOS == "windows" {
199 t.Skip("posix shell only")
200 }
201 tool := &BashTool{CWD: t.TempDir()}
202 res, _ := tool.Execute(context.Background(), mustJSON(t, map[string]any{"command": "false"}), nil)
203 if !res.IsError {
204 t.Fatal("want error")
205 }
206 got := res.Content[0].(provider.TextBlock).Text
207 if !strings.Contains(got, "[exit 1]") {
208 t.Fatalf("got %q", got)
209 }
210}