@@ -0,0 +1,509 @@
11+ "github.com/kujtimiihoxha/termai/internal/lsp"
12+ "github.com/stretchr/testify/assert"
13+ "github.com/stretchr/testify/require"
16+func TestEditTool_Info(t *testing.T) {
17+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
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")
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)
36+ t.Run("creates a new file successfully", func(t *testing.T) {
37+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
39+ filePath := filepath.Join(tempDir, "new_file.txt")
40+ content := "This is a test content"
42+ params := EditParams{
48+ paramsJSON, err := json.Marshal(params)
49+ require.NoError(t, err)
53+ Input: string(paramsJSON),
56+ response, err := tool.Run(context.Background(), call)
57+ require.NoError(t, err)
58+ assert.Contains(t, response.Content, "File created")
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))
66+ t.Run("creates file with nested directories", func(t *testing.T) {
67+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
69+ filePath := filepath.Join(tempDir, "nested/dirs/new_file.txt")
70+ content := "Content in nested directory"
72+ params := EditParams{
78+ paramsJSON, err := json.Marshal(params)
79+ require.NoError(t, err)
83+ Input: string(paramsJSON),
86+ response, err := tool.Run(context.Background(), call)
87+ require.NoError(t, err)
88+ assert.Contains(t, response.Content, "File created")
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))
96+ t.Run("fails to create file that already exists", func(t *testing.T) {
97+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
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)
105+ // Try to create the same file
106+ params := EditParams{
109+ NewString: "New content",
112+ paramsJSON, err := json.Marshal(params)
113+ require.NoError(t, err)
117+ Input: string(paramsJSON),
120+ response, err := tool.Run(context.Background(), call)
121+ require.NoError(t, err)
122+ assert.Contains(t, response.Content, "file already exists")
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))
128+ // Create a directory
129+ dirPath := filepath.Join(tempDir, "test_dir")
130+ err := os.Mkdir(dirPath, 0o755)
131+ require.NoError(t, err)
133+ // Try to create a file with the same path as the directory
134+ params := EditParams{
137+ NewString: "Some content",
140+ paramsJSON, err := json.Marshal(params)
141+ require.NoError(t, err)
145+ Input: string(paramsJSON),
148+ response, err := tool.Run(context.Background(), call)
149+ require.NoError(t, err)
150+ assert.Contains(t, response.Content, "path is a directory")
153+ t.Run("replaces content successfully", func(t *testing.T) {
154+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
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)
162+ // Record the file read to avoid modification time check failure
163+ recordFileRead(filePath)
166+ oldString := "Line 2\nLine 3"
167+ newString := "Line 2 modified\nLine 3 modified"
168+ params := EditParams{
170+ OldString: oldString,
171+ NewString: newString,
174+ paramsJSON, err := json.Marshal(params)
175+ require.NoError(t, err)
179+ Input: string(paramsJSON),
182+ response, err := tool.Run(context.Background(), call)
183+ require.NoError(t, err)
184+ assert.Contains(t, response.Content, "Content replaced")
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))
193+ t.Run("deletes content successfully", func(t *testing.T) {
194+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
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)
202+ // Record the file read to avoid modification time check failure
203+ recordFileRead(filePath)
206+ oldString := "Line 2\nLine 3\n"
207+ params := EditParams{
209+ OldString: oldString,
213+ paramsJSON, err := json.Marshal(params)
214+ require.NoError(t, err)
218+ Input: string(paramsJSON),
221+ response, err := tool.Run(context.Background(), call)
222+ require.NoError(t, err)
223+ assert.Contains(t, response.Content, "Content deleted")
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))
232+ t.Run("handles invalid parameters", func(t *testing.T) {
233+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
237+ Input: "invalid json",
240+ response, err := tool.Run(context.Background(), call)
241+ require.NoError(t, err)
242+ assert.Contains(t, response.Content, "invalid parameters")
245+ t.Run("handles missing file_path", func(t *testing.T) {
246+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
248+ params := EditParams{
254+ paramsJSON, err := json.Marshal(params)
255+ require.NoError(t, err)
259+ Input: string(paramsJSON),
262+ response, err := tool.Run(context.Background(), call)
263+ require.NoError(t, err)
264+ assert.Contains(t, response.Content, "file_path is required")
267+ t.Run("handles file not found", func(t *testing.T) {
268+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
270+ filePath := filepath.Join(tempDir, "non_existent_file.txt")
271+ params := EditParams{
273+ OldString: "old content",
274+ NewString: "new content",
277+ paramsJSON, err := json.Marshal(params)
278+ require.NoError(t, err)
282+ Input: string(paramsJSON),
285+ response, err := tool.Run(context.Background(), call)
286+ require.NoError(t, err)
287+ assert.Contains(t, response.Content, "file not found")
290+ t.Run("handles old_string not found in file", func(t *testing.T) {
291+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
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)
299+ // Record the file read to avoid modification time check failure
300+ recordFileRead(filePath)
302+ // Try to replace content that doesn't exist
303+ params := EditParams{
305+ OldString: "This content does not exist",
306+ NewString: "new content",
309+ paramsJSON, err := json.Marshal(params)
310+ require.NoError(t, err)
314+ Input: string(paramsJSON),
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")
322+ t.Run("handles multiple occurrences of old_string", func(t *testing.T) {
323+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
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)
331+ // Record the file read to avoid modification time check failure
332+ recordFileRead(filePath)
334+ // Try to replace content that appears multiple times
335+ params := EditParams{
337+ OldString: "Duplicate",
338+ NewString: "Replaced",
341+ paramsJSON, err := json.Marshal(params)
342+ require.NoError(t, err)
346+ Input: string(paramsJSON),
349+ response, err := tool.Run(context.Background(), call)
350+ require.NoError(t, err)
351+ assert.Contains(t, response.Content, "appears multiple times")
354+ t.Run("handles file modified since last read", func(t *testing.T) {
355+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
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)
363+ // Record an old read time
364+ fileRecordMutex.Lock()
365+ fileRecords[filePath] = fileRecord{
367+ readTime: time.Now().Add(-1 * time.Hour),
369+ fileRecordMutex.Unlock()
371+ // Try to update the file
372+ params := EditParams{
374+ OldString: "Initial",
375+ NewString: "Updated",
378+ paramsJSON, err := json.Marshal(params)
379+ require.NoError(t, err)
383+ Input: string(paramsJSON),
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")
390+ // Verify file was not modified
391+ fileContent, err := os.ReadFile(filePath)
392+ require.NoError(t, err)
393+ assert.Equal(t, initialContent, string(fileContent))
396+ t.Run("handles file not read before editing", func(t *testing.T) {
397+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true))
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)
405+ // Try to update the file without reading it first
406+ params := EditParams{
408+ OldString: "Initial",
409+ NewString: "Updated",
412+ paramsJSON, err := json.Marshal(params)
413+ require.NoError(t, err)
417+ Input: string(paramsJSON),
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")
425+ t.Run("handles permission denied", func(t *testing.T) {
426+ tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(false))
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)
434+ // Record the file read to avoid modification time check failure
435+ recordFileRead(filePath)
437+ // Try to update the file
438+ params := EditParams{
440+ OldString: "Initial",
441+ NewString: "Updated",
444+ paramsJSON, err := json.Marshal(params)
445+ require.NoError(t, err)
449+ Input: string(paramsJSON),
452+ response, err := tool.Run(context.Background(), call)
453+ require.NoError(t, err)
454+ assert.Contains(t, response.Content, "permission denied")
456+ // Verify file was not modified
457+ fileContent, err := os.ReadFile(filePath)
458+ require.NoError(t, err)
459+ assert.Equal(t, initialContent, string(fileContent))
463+func TestGenerateDiff(t *testing.T) {
464+ testCases := []struct {
471+ name: "add content",
472+ oldContent: "Line 1\nLine 2\n",
473+ newContent: "Line 1\nLine 2\nLine 3\n",
474+ expectedDiff: "Changes:\n Line 1\n Line 2\n+ Line 3\n",
477+ name: "remove content",
478+ oldContent: "Line 1\nLine 2\nLine 3\n",
479+ newContent: "Line 1\nLine 3\n",
480+ expectedDiff: "Changes:\n Line 1\n- Line 2\n Line 3\n",
483+ name: "replace content",
484+ oldContent: "Line 1\nLine 2\nLine 3\n",
485+ newContent: "Line 1\nModified Line\nLine 3\n",
486+ expectedDiff: "Changes:\n Line 1\n- Line 2\n+ Modified Line\n Line 3\n",
489+ name: "empty to content",
491+ newContent: "Line 1\nLine 2\n",
492+ expectedDiff: "Changes:\n+ Line 1\n+ Line 2\n",
495+ name: "content to empty",
496+ oldContent: "Line 1\nLine 2\n",
498+ expectedDiff: "Changes:\n- Line 1\n- Line 2\n",
502+ for _, tc := range testCases {
503+ t.Run(tc.name, func(t *testing.T) {
504+ diff := GenerateDiff(tc.oldContent, tc.newContent)
505+ assert.Contains(t, diff, tc.expectedDiff)