diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-08 19:15:23 +0200 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-04-08 19:15:23 +0200 |
| commit | 94923948e1a1525988bf13025a10cc9226652684 (patch) | |
| tree | 91d6b5b78b6af47a7272e92b54386eee42f21e50 /internal/llm/tools/edit_test.go | |
| parent | 5acf0cba6040aaf90acb5dcacd3e4127d6833ac5 (diff) | |
| download | opencode-94923948e1a1525988bf13025a10cc9226652684.tar.gz opencode-94923948e1a1525988bf13025a10cc9226652684.zip | |
structure tools the same
- add some tests
- fix some tests
- change how we handle permissions
Diffstat (limited to 'internal/llm/tools/edit_test.go')
| -rw-r--r-- | internal/llm/tools/edit_test.go | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/internal/llm/tools/edit_test.go b/internal/llm/tools/edit_test.go new file mode 100644 index 000000000..dbc6e488f --- /dev/null +++ b/internal/llm/tools/edit_test.go @@ -0,0 +1,509 @@ +package tools + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEditTool_Info(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + info := tool.Info() + + assert.Equal(t, EditToolName, info.Name) + assert.NotEmpty(t, info.Description) + assert.Contains(t, info.Parameters, "file_path") + assert.Contains(t, info.Parameters, "old_string") + assert.Contains(t, info.Parameters, "new_string") + assert.Contains(t, info.Required, "file_path") + assert.Contains(t, info.Required, "old_string") + assert.Contains(t, info.Required, "new_string") +} + +func TestEditTool_Run(t *testing.T) { + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "edit_tool_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + t.Run("creates a new file successfully", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + filePath := filepath.Join(tempDir, "new_file.txt") + content := "This is a test content" + + params := EditParams{ + FilePath: filePath, + OldString: "", + NewString: content, + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "File created") + + // Verify file was created with correct content + fileContent, err := os.ReadFile(filePath) + require.NoError(t, err) + assert.Equal(t, content, string(fileContent)) + }) + + t.Run("creates file with nested directories", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + filePath := filepath.Join(tempDir, "nested/dirs/new_file.txt") + content := "Content in nested directory" + + params := EditParams{ + FilePath: filePath, + OldString: "", + NewString: content, + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "File created") + + // Verify file was created with correct content + fileContent, err := os.ReadFile(filePath) + require.NoError(t, err) + assert.Equal(t, content, string(fileContent)) + }) + + t.Run("fails to create file that already exists", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + // Create a file first + filePath := filepath.Join(tempDir, "existing_file.txt") + initialContent := "Initial content" + err := os.WriteFile(filePath, []byte(initialContent), 0o644) + require.NoError(t, err) + + // Try to create the same file + params := EditParams{ + FilePath: filePath, + OldString: "", + NewString: "New content", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "file already exists") + }) + + t.Run("fails to create file when path is a directory", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + // Create a directory + dirPath := filepath.Join(tempDir, "test_dir") + err := os.Mkdir(dirPath, 0o755) + require.NoError(t, err) + + // Try to create a file with the same path as the directory + params := EditParams{ + FilePath: dirPath, + OldString: "", + NewString: "Some content", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "path is a directory") + }) + + t.Run("replaces content successfully", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + // Create a file first + filePath := filepath.Join(tempDir, "replace_content.txt") + initialContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" + err := os.WriteFile(filePath, []byte(initialContent), 0o644) + require.NoError(t, err) + + // Record the file read to avoid modification time check failure + recordFileRead(filePath) + + // Replace content + oldString := "Line 2\nLine 3" + newString := "Line 2 modified\nLine 3 modified" + params := EditParams{ + FilePath: filePath, + OldString: oldString, + NewString: newString, + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "Content replaced") + + // Verify file was updated with correct content + expectedContent := "Line 1\nLine 2 modified\nLine 3 modified\nLine 4\nLine 5" + fileContent, err := os.ReadFile(filePath) + require.NoError(t, err) + assert.Equal(t, expectedContent, string(fileContent)) + }) + + t.Run("deletes content successfully", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + // Create a file first + filePath := filepath.Join(tempDir, "delete_content.txt") + initialContent := "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" + err := os.WriteFile(filePath, []byte(initialContent), 0o644) + require.NoError(t, err) + + // Record the file read to avoid modification time check failure + recordFileRead(filePath) + + // Delete content + oldString := "Line 2\nLine 3\n" + params := EditParams{ + FilePath: filePath, + OldString: oldString, + NewString: "", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "Content deleted") + + // Verify file was updated with correct content + expectedContent := "Line 1\nLine 4\nLine 5" + fileContent, err := os.ReadFile(filePath) + require.NoError(t, err) + assert.Equal(t, expectedContent, string(fileContent)) + }) + + t.Run("handles invalid parameters", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + call := ToolCall{ + Name: EditToolName, + Input: "invalid json", + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "invalid parameters") + }) + + t.Run("handles missing file_path", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + params := EditParams{ + FilePath: "", + OldString: "old", + NewString: "new", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "file_path is required") + }) + + t.Run("handles file not found", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + filePath := filepath.Join(tempDir, "non_existent_file.txt") + params := EditParams{ + FilePath: filePath, + OldString: "old content", + NewString: "new content", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "file not found") + }) + + t.Run("handles old_string not found in file", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + // Create a file first + filePath := filepath.Join(tempDir, "content_not_found.txt") + initialContent := "Line 1\nLine 2\nLine 3" + err := os.WriteFile(filePath, []byte(initialContent), 0o644) + require.NoError(t, err) + + // Record the file read to avoid modification time check failure + recordFileRead(filePath) + + // Try to replace content that doesn't exist + params := EditParams{ + FilePath: filePath, + OldString: "This content does not exist", + NewString: "new content", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "old_string not found in file") + }) + + t.Run("handles multiple occurrences of old_string", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + // Create a file with duplicate content + filePath := filepath.Join(tempDir, "duplicate_content.txt") + initialContent := "Line 1\nDuplicate\nLine 3\nDuplicate\nLine 5" + err := os.WriteFile(filePath, []byte(initialContent), 0o644) + require.NoError(t, err) + + // Record the file read to avoid modification time check failure + recordFileRead(filePath) + + // Try to replace content that appears multiple times + params := EditParams{ + FilePath: filePath, + OldString: "Duplicate", + NewString: "Replaced", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "appears multiple times") + }) + + t.Run("handles file modified since last read", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + // Create a file + filePath := filepath.Join(tempDir, "modified_file.txt") + initialContent := "Initial content" + err := os.WriteFile(filePath, []byte(initialContent), 0o644) + require.NoError(t, err) + + // Record an old read time + fileRecordMutex.Lock() + fileRecords[filePath] = fileRecord{ + path: filePath, + readTime: time.Now().Add(-1 * time.Hour), + } + fileRecordMutex.Unlock() + + // Try to update the file + params := EditParams{ + FilePath: filePath, + OldString: "Initial", + NewString: "Updated", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "has been modified since it was last read") + + // Verify file was not modified + fileContent, err := os.ReadFile(filePath) + require.NoError(t, err) + assert.Equal(t, initialContent, string(fileContent)) + }) + + t.Run("handles file not read before editing", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(true)) + + // Create a file + filePath := filepath.Join(tempDir, "not_read_file.txt") + initialContent := "Initial content" + err := os.WriteFile(filePath, []byte(initialContent), 0o644) + require.NoError(t, err) + + // Try to update the file without reading it first + params := EditParams{ + FilePath: filePath, + OldString: "Initial", + NewString: "Updated", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "you must read the file before editing it") + }) + + t.Run("handles permission denied", func(t *testing.T) { + tool := NewEditTool(make(map[string]*lsp.Client), newMockPermissionService(false)) + + // Create a file + filePath := filepath.Join(tempDir, "permission_denied.txt") + initialContent := "Initial content" + err := os.WriteFile(filePath, []byte(initialContent), 0o644) + require.NoError(t, err) + + // Record the file read to avoid modification time check failure + recordFileRead(filePath) + + // Try to update the file + params := EditParams{ + FilePath: filePath, + OldString: "Initial", + NewString: "Updated", + } + + paramsJSON, err := json.Marshal(params) + require.NoError(t, err) + + call := ToolCall{ + Name: EditToolName, + Input: string(paramsJSON), + } + + response, err := tool.Run(context.Background(), call) + require.NoError(t, err) + assert.Contains(t, response.Content, "permission denied") + + // Verify file was not modified + fileContent, err := os.ReadFile(filePath) + require.NoError(t, err) + assert.Equal(t, initialContent, string(fileContent)) + }) +} + +func TestGenerateDiff(t *testing.T) { + testCases := []struct { + name string + oldContent string + newContent string + expectedDiff string + }{ + { + name: "add content", + oldContent: "Line 1\nLine 2\n", + newContent: "Line 1\nLine 2\nLine 3\n", + expectedDiff: "Changes:\n Line 1\n Line 2\n+ Line 3\n", + }, + { + name: "remove content", + oldContent: "Line 1\nLine 2\nLine 3\n", + newContent: "Line 1\nLine 3\n", + expectedDiff: "Changes:\n Line 1\n- Line 2\n Line 3\n", + }, + { + name: "replace content", + oldContent: "Line 1\nLine 2\nLine 3\n", + newContent: "Line 1\nModified Line\nLine 3\n", + expectedDiff: "Changes:\n Line 1\n- Line 2\n+ Modified Line\n Line 3\n", + }, + { + name: "empty to content", + oldContent: "", + newContent: "Line 1\nLine 2\n", + expectedDiff: "Changes:\n+ Line 1\n+ Line 2\n", + }, + { + name: "content to empty", + oldContent: "Line 1\nLine 2\n", + newContent: "", + expectedDiff: "Changes:\n- Line 1\n- Line 2\n", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + diff := GenerateDiff(tc.oldContent, tc.newContent) + assert.Contains(t, diff, tc.expectedDiff) + }) + } +} + |
