@@ -1,461 +0,0 @@
1βpackage tools
2β
3βimport (
4β "context"
5β "encoding/json"
6β "os"
7β "path/filepath"
8β "testing"
9β "time"
10β
11β "github.com/kujtimiihoxha/opencode/internal/lsp"
12β "github.com/stretchr/testify/assert"
13β "github.com/stretchr/testify/require"
14β)
15β
16βfunc TestEditTool_Info(t *testing.T) {
17β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
18β info := tool.Info()
19β
20β assert.Equal(t, EditToolName, info.Name)
21β assert.NotEmpty(t, info.Description)
22β assert.Contains(t, info.Parameters, "file_path")
23β assert.Contains(t, info.Parameters, "old_string")
24β assert.Contains(t, info.Parameters, "new_string")
25β assert.Contains(t, info.Required, "file_path")
26β assert.Contains(t, info.Required, "old_string")
27β assert.Contains(t, info.Required, "new_string")
28β}
29β
30βfunc TestEditTool_Run(t *testing.T) {
31β // Create a temporary directory for testing
32β tempDir, err := os.MkdirTemp("", "edit_tool_test")
33β require.NoError(t, err)
34β defer os.RemoveAll(tempDir)
35β
36β t.Run("creates a new file successfully", func(t *testing.T) {
37β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
38β
39β filePath := filepath.Join(tempDir, "new_file.txt")
40β content := "This is a test content"
41β
42β params := EditParams{
43β FilePath: filePath,
44β OldString: "",
45β NewString: content,
46β }
47β
48β paramsJSON, err := json.Marshal(params)
49β require.NoError(t, err)
50β
51β call := ToolCall{
52β Name: EditToolName,
53β Input: string(paramsJSON),
54β }
55β
56β response, err := tool.Run(context.Background(), call)
57β require.NoError(t, err)
58β assert.Contains(t, response.Content, "File created")
59β
60β // Verify file was created with correct content
61β fileContent, err := os.ReadFile(filePath)
62β require.NoError(t, err)
63β assert.Equal(t, content, string(fileContent))
64β })
65β
66β t.Run("creates file with nested directories", func(t *testing.T) {
67β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
68β
69β filePath := filepath.Join(tempDir, "nested/dirs/new_file.txt")
70β content := "Content in nested directory"
71β
72β params := EditParams{
73β FilePath: filePath,
74β OldString: "",
75β NewString: content,
76β }
77β
78β paramsJSON, err := json.Marshal(params)
79β require.NoError(t, err)
80β
81β call := ToolCall{
82β Name: EditToolName,
83β Input: string(paramsJSON),
84β }
85β
86β response, err := tool.Run(context.Background(), call)
87β require.NoError(t, err)
88β assert.Contains(t, response.Content, "File created")
89β
90β // Verify file was created with correct content
91β fileContent, err := os.ReadFile(filePath)
92β require.NoError(t, err)
93β assert.Equal(t, content, string(fileContent))
94β })
95β
96β t.Run("fails to create file that already exists", func(t *testing.T) {
97β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
98β
99β // Create a file first
100β filePath := filepath.Join(tempDir, "existing_file.txt")
101β initialContent := "Initial content"
102β err := os.WriteFile(filePath, []byte(initialContent), 0o644)
103β require.NoError(t, err)
104β
105β // Try to create the same file
106β params := EditParams{
107β FilePath: filePath,
108β OldString: "",
109β NewString: "New content",
110β }
111β
112β paramsJSON, err := json.Marshal(params)
113β require.NoError(t, err)
114β
115β call := ToolCall{
116β Name: EditToolName,
117β Input: string(paramsJSON),
118β }
119β
120β response, err := tool.Run(context.Background(), call)
121β require.NoError(t, err)
122β assert.Contains(t, response.Content, "file already exists")
123β })
124β
125β t.Run("fails to create file when path is a directory", func(t *testing.T) {
126β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
127β
128β // Create a directory
129β dirPath := filepath.Join(tempDir, "test_dir")
130β err := os.Mkdir(dirPath, 0o755)
131β require.NoError(t, err)
132β
133β // Try to create a file with the same path as the directory
134β params := EditParams{
135β FilePath: dirPath,
136β OldString: "",
137β NewString: "Some content",
138β }
139β
140β paramsJSON, err := json.Marshal(params)
141β require.NoError(t, err)
142β
143β call := ToolCall{
144β Name: EditToolName,
145β Input: string(paramsJSON),
146β }
147β
148β response, err := tool.Run(context.Background(), call)
149β require.NoError(t, err)
150β assert.Contains(t, response.Content, "path is a directory")
151β })
152β
153β t.Run("replaces content successfully", func(t *testing.T) {
154β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
155β
156β // Create a file first
157β filePath := filepath.Join(tempDir, "replace_content.txt")
158β initialContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
159β err := os.WriteFile(filePath, []byte(initialContent), 0o644)
160β require.NoError(t, err)
161β
162β // Record the file read to avoid modification time check failure
163β recordFileRead(filePath)
164β
165β // Replace content
166β oldString := "Line 2\nLine 3"
167β newString := "Line 2 modified\nLine 3 modified"
168β params := EditParams{
169β FilePath: filePath,
170β OldString: oldString,
171β NewString: newString,
172β }
173β
174β paramsJSON, err := json.Marshal(params)
175β require.NoError(t, err)
176β
177β call := ToolCall{
178β Name: EditToolName,
179β Input: string(paramsJSON),
180β }
181β
182β response, err := tool.Run(context.Background(), call)
183β require.NoError(t, err)
184β assert.Contains(t, response.Content, "Content replaced")
185β
186β // Verify file was updated with correct content
187β expectedContent := "Line 1\nLine 2 modified\nLine 3 modified\nLine 4\nLine 5"
188β fileContent, err := os.ReadFile(filePath)
189β require.NoError(t, err)
190β assert.Equal(t, expectedContent, string(fileContent))
191β })
192β
193β t.Run("deletes content successfully", func(t *testing.T) {
194β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
195β
196β // Create a file first
197β filePath := filepath.Join(tempDir, "delete_content.txt")
198β initialContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
199β err := os.WriteFile(filePath, []byte(initialContent), 0o644)
200β require.NoError(t, err)
201β
202β // Record the file read to avoid modification time check failure
203β recordFileRead(filePath)
204β
205β // Delete content
206β oldString := "Line 2\nLine 3\n"
207β params := EditParams{
208β FilePath: filePath,
209β OldString: oldString,
210β NewString: "",
211β }
212β
213β paramsJSON, err := json.Marshal(params)
214β require.NoError(t, err)
215β
216β call := ToolCall{
217β Name: EditToolName,
218β Input: string(paramsJSON),
219β }
220β
221β response, err := tool.Run(context.Background(), call)
222β require.NoError(t, err)
223β assert.Contains(t, response.Content, "Content deleted")
224β
225β // Verify file was updated with correct content
226β expectedContent := "Line 1\nLine 4\nLine 5"
227β fileContent, err := os.ReadFile(filePath)
228β require.NoError(t, err)
229β assert.Equal(t, expectedContent, string(fileContent))
230β })
231β
232β t.Run("handles invalid parameters", func(t *testing.T) {
233β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
234β
235β call := ToolCall{
236β Name: EditToolName,
237β Input: "invalid json",
238β }
239β
240β response, err := tool.Run(context.Background(), call)
241β require.NoError(t, err)
242β assert.Contains(t, response.Content, "invalid parameters")
243β })
244β
245β t.Run("handles missing file_path", func(t *testing.T) {
246β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
247β
248β params := EditParams{
249β FilePath: "",
250β OldString: "old",
251β NewString: "new",
252β }
253β
254β paramsJSON, err := json.Marshal(params)
255β require.NoError(t, err)
256β
257β call := ToolCall{
258β Name: EditToolName,
259β Input: string(paramsJSON),
260β }
261β
262β response, err := tool.Run(context.Background(), call)
263β require.NoError(t, err)
264β assert.Contains(t, response.Content, "file_path is required")
265β })
266β
267β t.Run("handles file not found", func(t *testing.T) {
268β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
269β
270β filePath := filepath.Join(tempDir, "non_existent_file.txt")
271β params := EditParams{
272β FilePath: filePath,
273β OldString: "old content",
274β NewString: "new content",
275β }
276β
277β paramsJSON, err := json.Marshal(params)
278β require.NoError(t, err)
279β
280β call := ToolCall{
281β Name: EditToolName,
282β Input: string(paramsJSON),
283β }
284β
285β response, err := tool.Run(context.Background(), call)
286β require.NoError(t, err)
287β assert.Contains(t, response.Content, "file not found")
288β })
289β
290β t.Run("handles old_string not found in file", func(t *testing.T) {
291β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
292β
293β // Create a file first
294β filePath := filepath.Join(tempDir, "content_not_found.txt")
295β initialContent := "Line 1\nLine 2\nLine 3"
296β err := os.WriteFile(filePath, []byte(initialContent), 0o644)
297β require.NoError(t, err)
298β
299β // Record the file read to avoid modification time check failure
300β recordFileRead(filePath)
301β
302β // Try to replace content that doesn't exist
303β params := EditParams{
304β FilePath: filePath,
305β OldString: "This content does not exist",
306β NewString: "new content",
307β }
308β
309β paramsJSON, err := json.Marshal(params)
310β require.NoError(t, err)
311β
312β call := ToolCall{
313β Name: EditToolName,
314β Input: string(paramsJSON),
315β }
316β
317β response, err := tool.Run(context.Background(), call)
318β require.NoError(t, err)
319β assert.Contains(t, response.Content, "old_string not found in file")
320β })
321β
322β t.Run("handles multiple occurrences of old_string", func(t *testing.T) {
323β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
324β
325β // Create a file with duplicate content
326β filePath := filepath.Join(tempDir, "duplicate_content.txt")
327β initialContent := "Line 1\nDuplicate\nLine 3\nDuplicate\nLine 5"
328β err := os.WriteFile(filePath, []byte(initialContent), 0o644)
329β require.NoError(t, err)
330β
331β // Record the file read to avoid modification time check failure
332β recordFileRead(filePath)
333β
334β // Try to replace content that appears multiple times
335β params := EditParams{
336β FilePath: filePath,
337β OldString: "Duplicate",
338β NewString: "Replaced",
339β }
340β
341β paramsJSON, err := json.Marshal(params)
342β require.NoError(t, err)
343β
344β call := ToolCall{
345β Name: EditToolName,
346β Input: string(paramsJSON),
347β }
348β
349β response, err := tool.Run(context.Background(), call)
350β require.NoError(t, err)
351β assert.Contains(t, response.Content, "appears multiple times")
352β })
353β
354β t.Run("handles file modified since last read", func(t *testing.T) {
355β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
356β
357β // Create a file
358β filePath := filepath.Join(tempDir, "modified_file.txt")
359β initialContent := "Initial content"
360β err := os.WriteFile(filePath, []byte(initialContent), 0o644)
361β require.NoError(t, err)
362β
363β // Record an old read time
364β fileRecordMutex.Lock()
365β fileRecords[filePath] = fileRecord{
366β path: filePath,
367β readTime: time.Now().Add(-1 * time.Hour),
368β }
369β fileRecordMutex.Unlock()
370β
371β // Try to update the file
372β params := EditParams{
373β FilePath: filePath,
374β OldString: "Initial",
375β NewString: "Updated",
376β }
377β
378β paramsJSON, err := json.Marshal(params)
379β require.NoError(t, err)
380β
381β call := ToolCall{
382β Name: EditToolName,
383β Input: string(paramsJSON),
384β }
385β
386β response, err := tool.Run(context.Background(), call)
387β require.NoError(t, err)
388β assert.Contains(t, response.Content, "has been modified since it was last read")
389β
390β // Verify file was not modified
391β fileContent, err := os.ReadFile(filePath)
392β require.NoError(t, err)
393β assert.Equal(t, initialContent, string(fileContent))
394β })
395β
396β t.Run("handles file not read before editing", func(t *testing.T) {
397β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true), newMockFileHistoryService())
398β
399β // Create a file
400β filePath := filepath.Join(tempDir, "not_read_file.txt")
401β initialContent := "Initial content"
402β err := os.WriteFile(filePath, []byte(initialContent), 0o644)
403β require.NoError(t, err)
404β
405β // Try to update the file without reading it first
406β params := EditParams{
407β FilePath: filePath,
408β OldString: "Initial",
409β NewString: "Updated",
410β }
411β
412β paramsJSON, err := json.Marshal(params)
413β require.NoError(t, err)
414β
415β call := ToolCall{
416β Name: EditToolName,
417β Input: string(paramsJSON),
418β }
419β
420β response, err := tool.Run(context.Background(), call)
421β require.NoError(t, err)
422β assert.Contains(t, response.Content, "you must read the file before editing it")
423β })
424β
425β t.Run("handles permission denied", func(t *testing.T) {
426β tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(false), newMockFileHistoryService())
427β
428β // Create a file
429β filePath := filepath.Join(tempDir, "permission_denied.txt")
430β initialContent := "Initial content"
431β err := os.WriteFile(filePath, []byte(initialContent), 0o644)
432β require.NoError(t, err)
433β
434β // Record the file read to avoid modification time check failure
435β recordFileRead(filePath)
436β
437β // Try to update the file
438β params := EditParams{
439β FilePath: filePath,
440β OldString: "Initial",
441β NewString: "Updated",
442β }
443β
444β paramsJSON, err := json.Marshal(params)
445β require.NoError(t, err)
446β
447β call := ToolCall{
448β Name: EditToolName,
449β Input: string(paramsJSON),
450β }
451β
452β response, err := tool.Run(context.Background(), call)
453β require.NoError(t, err)
454β assert.Contains(t, response.Content, "permission denied")
455β
456β // Verify file was not modified
457β fileContent, err := os.ReadFile(filePath)
458β require.NoError(t, err)
459β assert.Equal(t, initialContent, string(fileContent))
460β })
461β}