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