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