1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "testing"
9 "time"
10
11 "github.com/kujtimiihoxha/termai/internal/permission"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14)
15
16func TestWriteTool_Info(t *testing.T) {
17 tool := NewWriteTool()
18 info := tool.Info()
19
20 assert.Equal(t, WriteToolName, info.Name)
21 assert.NotEmpty(t, info.Description)
22 assert.Contains(t, info.Parameters, "file_path")
23 assert.Contains(t, info.Parameters, "content")
24 assert.Contains(t, info.Required, "file_path")
25 assert.Contains(t, info.Required, "content")
26}
27
28func TestWriteTool_Run(t *testing.T) {
29 // Setup a mock permission handler that always allows
30 origPermission := permission.Default
31 defer func() {
32 permission.Default = origPermission
33 }()
34 permission.Default = newMockPermissionService(true)
35
36 // Create a temporary directory for testing
37 tempDir, err := os.MkdirTemp("", "write_tool_test")
38 require.NoError(t, err)
39 defer os.RemoveAll(tempDir)
40
41 t.Run("creates a new file successfully", func(t *testing.T) {
42 permission.Default = newMockPermissionService(true)
43 tool := NewWriteTool()
44
45 filePath := filepath.Join(tempDir, "new_file.txt")
46 content := "This is a test content"
47
48 params := WriteParams{
49 FilePath: filePath,
50 Content: content,
51 }
52
53 paramsJSON, err := json.Marshal(params)
54 require.NoError(t, err)
55
56 call := ToolCall{
57 Name: WriteToolName,
58 Input: string(paramsJSON),
59 }
60
61 response, err := tool.Run(context.Background(), call)
62 require.NoError(t, err)
63 assert.Contains(t, response.Content, "successfully written")
64
65 // Verify file was created with correct content
66 fileContent, err := os.ReadFile(filePath)
67 require.NoError(t, err)
68 assert.Equal(t, content, string(fileContent))
69 })
70
71 t.Run("creates file with nested directories", func(t *testing.T) {
72 permission.Default = newMockPermissionService(true)
73 tool := NewWriteTool()
74
75 filePath := filepath.Join(tempDir, "nested/dirs/new_file.txt")
76 content := "Content in nested directory"
77
78 params := WriteParams{
79 FilePath: filePath,
80 Content: content,
81 }
82
83 paramsJSON, err := json.Marshal(params)
84 require.NoError(t, err)
85
86 call := ToolCall{
87 Name: WriteToolName,
88 Input: string(paramsJSON),
89 }
90
91 response, err := tool.Run(context.Background(), call)
92 require.NoError(t, err)
93 assert.Contains(t, response.Content, "successfully written")
94
95 // Verify file was created with correct content
96 fileContent, err := os.ReadFile(filePath)
97 require.NoError(t, err)
98 assert.Equal(t, content, string(fileContent))
99 })
100
101 t.Run("updates existing file", func(t *testing.T) {
102 permission.Default = newMockPermissionService(true)
103 tool := NewWriteTool()
104
105 // Create a file first
106 filePath := filepath.Join(tempDir, "existing_file.txt")
107 initialContent := "Initial content"
108 err := os.WriteFile(filePath, []byte(initialContent), 0644)
109 require.NoError(t, err)
110
111 // Record the file read to avoid modification time check failure
112 recordFileRead(filePath)
113
114 // Update the file
115 updatedContent := "Updated content"
116 params := WriteParams{
117 FilePath: filePath,
118 Content: updatedContent,
119 }
120
121 paramsJSON, err := json.Marshal(params)
122 require.NoError(t, err)
123
124 call := ToolCall{
125 Name: WriteToolName,
126 Input: string(paramsJSON),
127 }
128
129 response, err := tool.Run(context.Background(), call)
130 require.NoError(t, err)
131 assert.Contains(t, response.Content, "successfully written")
132
133 // Verify file was updated with correct content
134 fileContent, err := os.ReadFile(filePath)
135 require.NoError(t, err)
136 assert.Equal(t, updatedContent, string(fileContent))
137 })
138
139 t.Run("handles invalid parameters", func(t *testing.T) {
140 permission.Default = newMockPermissionService(true)
141 tool := NewWriteTool()
142
143 call := ToolCall{
144 Name: WriteToolName,
145 Input: "invalid json",
146 }
147
148 response, err := tool.Run(context.Background(), call)
149 require.NoError(t, err)
150 assert.Contains(t, response.Content, "error parsing parameters")
151 })
152
153 t.Run("handles missing file_path", func(t *testing.T) {
154 permission.Default = newMockPermissionService(true)
155 tool := NewWriteTool()
156
157 params := WriteParams{
158 FilePath: "",
159 Content: "Some content",
160 }
161
162 paramsJSON, err := json.Marshal(params)
163 require.NoError(t, err)
164
165 call := ToolCall{
166 Name: WriteToolName,
167 Input: string(paramsJSON),
168 }
169
170 response, err := tool.Run(context.Background(), call)
171 require.NoError(t, err)
172 assert.Contains(t, response.Content, "file_path is required")
173 })
174
175 t.Run("handles missing content", func(t *testing.T) {
176 permission.Default = newMockPermissionService(true)
177 tool := NewWriteTool()
178
179 params := WriteParams{
180 FilePath: filepath.Join(tempDir, "file.txt"),
181 Content: "",
182 }
183
184 paramsJSON, err := json.Marshal(params)
185 require.NoError(t, err)
186
187 call := ToolCall{
188 Name: WriteToolName,
189 Input: string(paramsJSON),
190 }
191
192 response, err := tool.Run(context.Background(), call)
193 require.NoError(t, err)
194 assert.Contains(t, response.Content, "content is required")
195 })
196
197 t.Run("handles writing to a directory path", func(t *testing.T) {
198 permission.Default = newMockPermissionService(true)
199 tool := NewWriteTool()
200
201 // Create a directory
202 dirPath := filepath.Join(tempDir, "test_dir")
203 err := os.Mkdir(dirPath, 0755)
204 require.NoError(t, err)
205
206 params := WriteParams{
207 FilePath: dirPath,
208 Content: "Some content",
209 }
210
211 paramsJSON, err := json.Marshal(params)
212 require.NoError(t, err)
213
214 call := ToolCall{
215 Name: WriteToolName,
216 Input: string(paramsJSON),
217 }
218
219 response, err := tool.Run(context.Background(), call)
220 require.NoError(t, err)
221 assert.Contains(t, response.Content, "Path is a directory")
222 })
223
224 t.Run("handles permission denied", func(t *testing.T) {
225 permission.Default = newMockPermissionService(false)
226 tool := NewWriteTool()
227
228 filePath := filepath.Join(tempDir, "permission_denied.txt")
229 params := WriteParams{
230 FilePath: filePath,
231 Content: "Content that should not be written",
232 }
233
234 paramsJSON, err := json.Marshal(params)
235 require.NoError(t, err)
236
237 call := ToolCall{
238 Name: WriteToolName,
239 Input: string(paramsJSON),
240 }
241
242 response, err := tool.Run(context.Background(), call)
243 require.NoError(t, err)
244 assert.Contains(t, response.Content, "Permission denied")
245
246 // Verify file was not created
247 _, err = os.Stat(filePath)
248 assert.True(t, os.IsNotExist(err))
249 })
250
251 t.Run("detects file modified since last read", func(t *testing.T) {
252 permission.Default = newMockPermissionService(true)
253 tool := NewWriteTool()
254
255 // Create a file
256 filePath := filepath.Join(tempDir, "modified_file.txt")
257 initialContent := "Initial content"
258 err := os.WriteFile(filePath, []byte(initialContent), 0644)
259 require.NoError(t, err)
260
261 // Record an old read time
262 fileRecordMutex.Lock()
263 fileRecords[filePath] = fileRecord{
264 path: filePath,
265 readTime: time.Now().Add(-1 * time.Hour),
266 }
267 fileRecordMutex.Unlock()
268
269 // Try to update the file
270 params := WriteParams{
271 FilePath: filePath,
272 Content: "Updated content",
273 }
274
275 paramsJSON, err := json.Marshal(params)
276 require.NoError(t, err)
277
278 call := ToolCall{
279 Name: WriteToolName,
280 Input: string(paramsJSON),
281 }
282
283 response, err := tool.Run(context.Background(), call)
284 require.NoError(t, err)
285 assert.Contains(t, response.Content, "has been modified since it was last read")
286
287 // Verify file was not modified
288 fileContent, err := os.ReadFile(filePath)
289 require.NoError(t, err)
290 assert.Equal(t, initialContent, string(fileContent))
291 })
292
293 t.Run("skips writing when content is identical", func(t *testing.T) {
294 permission.Default = newMockPermissionService(true)
295 tool := NewWriteTool()
296
297 // Create a file
298 filePath := filepath.Join(tempDir, "identical_content.txt")
299 content := "Content that won't change"
300 err := os.WriteFile(filePath, []byte(content), 0644)
301 require.NoError(t, err)
302
303 // Record a read time
304 recordFileRead(filePath)
305
306 // Try to write the same content
307 params := WriteParams{
308 FilePath: filePath,
309 Content: content,
310 }
311
312 paramsJSON, err := json.Marshal(params)
313 require.NoError(t, err)
314
315 call := ToolCall{
316 Name: WriteToolName,
317 Input: string(paramsJSON),
318 }
319
320 response, err := tool.Run(context.Background(), call)
321 require.NoError(t, err)
322 assert.Contains(t, response.Content, "already contains the exact content")
323 })
324}