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