| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- 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)
- })
- }
- }
|