write_test.go 8.1 KB

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