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/internal/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 TestReadOffsetLimit(t *testing.T) {
42 dir := t.TempDir()
43 p := filepath.Join(dir, "a.txt")
44 os.WriteFile(p, []byte("1\n2\n3\n4\n5\n"), 0o644)
45 tool := &ReadTool{CWD: dir}
46 res, _ := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "a.txt", "offset": 2, "limit": 2}), nil)
47 got := res.Content[0].(provider.TextBlock).Text
48 if !strings.Contains(got, "2\t2") || !strings.Contains(got, "3\t3") {
49 t.Fatalf("got %q", got)
50 }
51 if strings.Contains(got, "1\t1") || strings.Contains(got, "4\t4") {
52 t.Fatalf("leaked lines: %q", got)
53 }
54}
55
56func TestReadBinaryRejected(t *testing.T) {
57 dir := t.TempDir()
58 p := filepath.Join(dir, "b.bin")
59 os.WriteFile(p, []byte{0x00, 0x01, 0x02}, 0o644)
60 tool := &ReadTool{CWD: dir}
61 if _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "b.bin"}), nil); err == nil {
62 t.Fatal("want binary rejection")
63 }
64}
65
66func TestWriteCreatesDirs(t *testing.T) {
67 dir := t.TempDir()
68 tool := &WriteTool{CWD: dir}
69 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "sub/a.txt", "content": "hi"}), nil)
70 if err != nil {
71 t.Fatal(err)
72 }
73 b, err := os.ReadFile(filepath.Join(dir, "sub", "a.txt"))
74 if err != nil {
75 t.Fatal(err)
76 }
77 if string(b) != "hi" {
78 t.Fatalf("got %q", string(b))
79 }
80}
81
82func TestEditSingle(t *testing.T) {
83 dir := t.TempDir()
84 p := filepath.Join(dir, "a.txt")
85 os.WriteFile(p, []byte("hello world\n"), 0o644)
86 tool := &EditTool{CWD: dir}
87 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
88 "path": "a.txt",
89 "edits": []map[string]any{{"oldText": "world", "newText": "gopher"}},
90 }), nil)
91 if err != nil {
92 t.Fatal(err)
93 }
94 b, _ := os.ReadFile(p)
95 if string(b) != "hello gopher\n" {
96 t.Fatalf("got %q", string(b))
97 }
98}
99
100func TestEditMultiple(t *testing.T) {
101 dir := t.TempDir()
102 p := filepath.Join(dir, "a.txt")
103 os.WriteFile(p, []byte("a\nb\nc\n"), 0o644)
104 tool := &EditTool{CWD: dir}
105 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
106 "path": "a.txt",
107 "edits": []map[string]any{
108 {"oldText": "a", "newText": "A"},
109 {"oldText": "c", "newText": "C"},
110 },
111 }), nil)
112 if err != nil {
113 t.Fatal(err)
114 }
115 b, _ := os.ReadFile(p)
116 if string(b) != "A\nb\nC\n" {
117 t.Fatalf("got %q", string(b))
118 }
119}
120
121func TestEditAmbiguous(t *testing.T) {
122 dir := t.TempDir()
123 p := filepath.Join(dir, "a.txt")
124 os.WriteFile(p, []byte("x\nx\n"), 0o644)
125 tool := &EditTool{CWD: dir}
126 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
127 "path": "a.txt",
128 "edits": []map[string]any{{"oldText": "x", "newText": "y"}},
129 }), nil)
130 if err == nil {
131 t.Fatal("want ambiguous error")
132 }
133}
134
135func TestEditPreservesCRLF(t *testing.T) {
136 dir := t.TempDir()
137 p := filepath.Join(dir, "a.txt")
138 os.WriteFile(p, []byte("hello\r\nworld\r\n"), 0o644)
139 tool := &EditTool{CWD: dir}
140 _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
141 "path": "a.txt",
142 "edits": []map[string]any{{"oldText": "world", "newText": "gopher"}},
143 }), nil)
144 if err != nil {
145 t.Fatal(err)
146 }
147 b, _ := os.ReadFile(p)
148 if string(b) != "hello\r\ngopher\r\n" {
149 t.Fatalf("got %q", string(b))
150 }
151}
152
153func TestBashSuccess(t *testing.T) {
154 if runtime.GOOS == "windows" {
155 t.Skip("posix shell only")
156 }
157 tool := &BashTool{CWD: t.TempDir()}
158 res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"command": "echo hi"}), nil)
159 if err != nil {
160 t.Fatal(err)
161 }
162 got := res.Content[0].(provider.TextBlock).Text
163 if !strings.Contains(got, "hi") || !strings.Contains(got, "[exit 0]") {
164 t.Fatalf("got %q", got)
165 }
166 if res.IsError {
167 t.Fatal("unexpected error flag")
168 }
169}
170
171func TestBashFailure(t *testing.T) {
172 if runtime.GOOS == "windows" {
173 t.Skip("posix shell only")
174 }
175 tool := &BashTool{CWD: t.TempDir()}
176 res, _ := tool.Execute(context.Background(), mustJSON(t, map[string]any{"command": "false"}), nil)
177 if !res.IsError {
178 t.Fatal("want error")
179 }
180 got := res.Content[0].(provider.TextBlock).Text
181 if !strings.Contains(got, "[exit 1]") {
182 t.Fatalf("got %q", got)
183 }
184}