write_test.go

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