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