1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13)
14
15func TestLsTool_Info(t *testing.T) {
16 tool := NewLsTool()
17 info := tool.Info()
18
19 assert.Equal(t, LSToolName, info.Name)
20 assert.NotEmpty(t, info.Description)
21 assert.Contains(t, info.Parameters, "path")
22 assert.Contains(t, info.Parameters, "ignore")
23 assert.Contains(t, info.Required, "path")
24}
25
26func TestLsTool_Run(t *testing.T) {
27 // Create a temporary directory for testing
28 tempDir, err := os.MkdirTemp("", "ls_tool_test")
29 require.NoError(t, err)
30 defer os.RemoveAll(tempDir)
31
32 // Create a test directory structure
33 testDirs := []string{
34 "dir1",
35 "dir2",
36 "dir2/subdir1",
37 "dir2/subdir2",
38 "dir3",
39 "dir3/.hidden_dir",
40 "__pycache__",
41 }
42
43 testFiles := []string{
44 "file1.txt",
45 "file2.txt",
46 "dir1/file3.txt",
47 "dir2/file4.txt",
48 "dir2/subdir1/file5.txt",
49 "dir2/subdir2/file6.txt",
50 "dir3/file7.txt",
51 "dir3/.hidden_file.txt",
52 "__pycache__/cache.pyc",
53 ".hidden_root_file.txt",
54 }
55
56 // Create directories
57 for _, dir := range testDirs {
58 dirPath := filepath.Join(tempDir, dir)
59 err := os.MkdirAll(dirPath, 0755)
60 require.NoError(t, err)
61 }
62
63 // Create files
64 for _, file := range testFiles {
65 filePath := filepath.Join(tempDir, file)
66 err := os.WriteFile(filePath, []byte("test content"), 0644)
67 require.NoError(t, err)
68 }
69
70 t.Run("lists directory successfully", func(t *testing.T) {
71 tool := NewLsTool()
72 params := LSParams{
73 Path: tempDir,
74 }
75
76 paramsJSON, err := json.Marshal(params)
77 require.NoError(t, err)
78
79 call := ToolCall{
80 Name: LSToolName,
81 Input: string(paramsJSON),
82 }
83
84 response, err := tool.Run(context.Background(), call)
85 require.NoError(t, err)
86
87 // Check that visible directories and files are included
88 assert.Contains(t, response.Content, "dir1")
89 assert.Contains(t, response.Content, "dir2")
90 assert.Contains(t, response.Content, "dir3")
91 assert.Contains(t, response.Content, "file1.txt")
92 assert.Contains(t, response.Content, "file2.txt")
93
94 // Check that hidden files and directories are not included
95 assert.NotContains(t, response.Content, ".hidden_dir")
96 assert.NotContains(t, response.Content, ".hidden_file.txt")
97 assert.NotContains(t, response.Content, ".hidden_root_file.txt")
98
99 // Check that __pycache__ is not included
100 assert.NotContains(t, response.Content, "__pycache__")
101 })
102
103 t.Run("handles non-existent path", func(t *testing.T) {
104 tool := NewLsTool()
105 params := LSParams{
106 Path: filepath.Join(tempDir, "non_existent_dir"),
107 }
108
109 paramsJSON, err := json.Marshal(params)
110 require.NoError(t, err)
111
112 call := ToolCall{
113 Name: LSToolName,
114 Input: string(paramsJSON),
115 }
116
117 response, err := tool.Run(context.Background(), call)
118 require.NoError(t, err)
119 assert.Contains(t, response.Content, "path does not exist")
120 })
121
122 t.Run("handles empty path parameter", func(t *testing.T) {
123 // For this test, we need to mock the config.WorkingDirectory function
124 // Since we can't easily do that, we'll just check that the response doesn't contain an error message
125
126 tool := NewLsTool()
127 params := LSParams{
128 Path: "",
129 }
130
131 paramsJSON, err := json.Marshal(params)
132 require.NoError(t, err)
133
134 call := ToolCall{
135 Name: LSToolName,
136 Input: string(paramsJSON),
137 }
138
139 response, err := tool.Run(context.Background(), call)
140 require.NoError(t, err)
141
142 // The response should either contain a valid directory listing or an error
143 // We'll just check that it's not empty
144 assert.NotEmpty(t, response.Content)
145 })
146
147 t.Run("handles invalid parameters", func(t *testing.T) {
148 tool := NewLsTool()
149 call := ToolCall{
150 Name: LSToolName,
151 Input: "invalid json",
152 }
153
154 response, err := tool.Run(context.Background(), call)
155 require.NoError(t, err)
156 assert.Contains(t, response.Content, "error parsing parameters")
157 })
158
159 t.Run("respects ignore patterns", func(t *testing.T) {
160 tool := NewLsTool()
161 params := LSParams{
162 Path: tempDir,
163 Ignore: []string{"file1.txt", "dir1"},
164 }
165
166 paramsJSON, err := json.Marshal(params)
167 require.NoError(t, err)
168
169 call := ToolCall{
170 Name: LSToolName,
171 Input: string(paramsJSON),
172 }
173
174 response, err := tool.Run(context.Background(), call)
175 require.NoError(t, err)
176
177 // The output format is a tree, so we need to check for specific patterns
178 // Check that file1.txt is not directly mentioned
179 assert.NotContains(t, response.Content, "- file1.txt")
180
181 // Check that dir1/ is not directly mentioned
182 assert.NotContains(t, response.Content, "- dir1/")
183 })
184
185 t.Run("handles relative path", func(t *testing.T) {
186 // Save original working directory
187 origWd, err := os.Getwd()
188 require.NoError(t, err)
189 defer func() {
190 os.Chdir(origWd)
191 }()
192
193 // Change to a directory above the temp directory
194 parentDir := filepath.Dir(tempDir)
195 err = os.Chdir(parentDir)
196 require.NoError(t, err)
197
198 tool := NewLsTool()
199 params := LSParams{
200 Path: filepath.Base(tempDir),
201 }
202
203 paramsJSON, err := json.Marshal(params)
204 require.NoError(t, err)
205
206 call := ToolCall{
207 Name: LSToolName,
208 Input: string(paramsJSON),
209 }
210
211 response, err := tool.Run(context.Background(), call)
212 require.NoError(t, err)
213
214 // Should list the temp directory contents
215 assert.Contains(t, response.Content, "dir1")
216 assert.Contains(t, response.Content, "file1.txt")
217 })
218}
219
220func TestShouldSkip(t *testing.T) {
221 testCases := []struct {
222 name string
223 path string
224 ignorePatterns []string
225 expected bool
226 }{
227 {
228 name: "hidden file",
229 path: "/path/to/.hidden_file",
230 ignorePatterns: []string{},
231 expected: true,
232 },
233 {
234 name: "hidden directory",
235 path: "/path/to/.hidden_dir",
236 ignorePatterns: []string{},
237 expected: true,
238 },
239 {
240 name: "pycache directory",
241 path: "/path/to/__pycache__/file.pyc",
242 ignorePatterns: []string{},
243 expected: true,
244 },
245 {
246 name: "node_modules directory",
247 path: "/path/to/node_modules/package",
248 ignorePatterns: []string{},
249 expected: false, // The shouldSkip function doesn't directly check for node_modules in the path
250 },
251 {
252 name: "normal file",
253 path: "/path/to/normal_file.txt",
254 ignorePatterns: []string{},
255 expected: false,
256 },
257 {
258 name: "normal directory",
259 path: "/path/to/normal_dir",
260 ignorePatterns: []string{},
261 expected: false,
262 },
263 {
264 name: "ignored by pattern",
265 path: "/path/to/ignore_me.txt",
266 ignorePatterns: []string{"ignore_*.txt"},
267 expected: true,
268 },
269 {
270 name: "not ignored by pattern",
271 path: "/path/to/keep_me.txt",
272 ignorePatterns: []string{"ignore_*.txt"},
273 expected: false,
274 },
275 }
276
277 for _, tc := range testCases {
278 t.Run(tc.name, func(t *testing.T) {
279 result := shouldSkip(tc.path, tc.ignorePatterns)
280 assert.Equal(t, tc.expected, result)
281 })
282 }
283}
284
285func TestCreateFileTree(t *testing.T) {
286 paths := []string{
287 "/path/to/file1.txt",
288 "/path/to/dir1/file2.txt",
289 "/path/to/dir1/subdir/file3.txt",
290 "/path/to/dir2/file4.txt",
291 }
292
293 tree := createFileTree(paths)
294
295 // Check the structure of the tree
296 assert.Len(t, tree, 1) // Should have one root node
297
298 // Check the root node
299 rootNode := tree[0]
300 assert.Equal(t, "path", rootNode.Name)
301 assert.Equal(t, "directory", rootNode.Type)
302 assert.Len(t, rootNode.Children, 1)
303
304 // Check the "to" node
305 toNode := rootNode.Children[0]
306 assert.Equal(t, "to", toNode.Name)
307 assert.Equal(t, "directory", toNode.Type)
308 assert.Len(t, toNode.Children, 3) // file1.txt, dir1, dir2
309
310 // Find the dir1 node
311 var dir1Node *TreeNode
312 for _, child := range toNode.Children {
313 if child.Name == "dir1" {
314 dir1Node = child
315 break
316 }
317 }
318
319 require.NotNil(t, dir1Node)
320 assert.Equal(t, "directory", dir1Node.Type)
321 assert.Len(t, dir1Node.Children, 2) // file2.txt and subdir
322}
323
324func TestPrintTree(t *testing.T) {
325 // Create a simple tree
326 tree := []*TreeNode{
327 {
328 Name: "dir1",
329 Path: "dir1",
330 Type: "directory",
331 Children: []*TreeNode{
332 {
333 Name: "file1.txt",
334 Path: "dir1/file1.txt",
335 Type: "file",
336 },
337 {
338 Name: "subdir",
339 Path: "dir1/subdir",
340 Type: "directory",
341 Children: []*TreeNode{
342 {
343 Name: "file2.txt",
344 Path: "dir1/subdir/file2.txt",
345 Type: "file",
346 },
347 },
348 },
349 },
350 },
351 {
352 Name: "file3.txt",
353 Path: "file3.txt",
354 Type: "file",
355 },
356 }
357
358 result := printTree(tree, "/root")
359
360 // Check the output format
361 assert.Contains(t, result, "- /root/")
362 assert.Contains(t, result, " - dir1/")
363 assert.Contains(t, result, " - file1.txt")
364 assert.Contains(t, result, " - subdir/")
365 assert.Contains(t, result, " - file2.txt")
366 assert.Contains(t, result, " - file3.txt")
367}
368
369func TestListDirectory(t *testing.T) {
370 // Create a temporary directory for testing
371 tempDir, err := os.MkdirTemp("", "list_directory_test")
372 require.NoError(t, err)
373 defer os.RemoveAll(tempDir)
374
375 // Create a test directory structure
376 testDirs := []string{
377 "dir1",
378 "dir1/subdir1",
379 ".hidden_dir",
380 }
381
382 testFiles := []string{
383 "file1.txt",
384 "file2.txt",
385 "dir1/file3.txt",
386 "dir1/subdir1/file4.txt",
387 ".hidden_file.txt",
388 }
389
390 // Create directories
391 for _, dir := range testDirs {
392 dirPath := filepath.Join(tempDir, dir)
393 err := os.MkdirAll(dirPath, 0755)
394 require.NoError(t, err)
395 }
396
397 // Create files
398 for _, file := range testFiles {
399 filePath := filepath.Join(tempDir, file)
400 err := os.WriteFile(filePath, []byte("test content"), 0644)
401 require.NoError(t, err)
402 }
403
404 t.Run("lists files with no limit", func(t *testing.T) {
405 files, truncated, err := listDirectory(tempDir, []string{}, 1000)
406 require.NoError(t, err)
407 assert.False(t, truncated)
408
409 // Check that visible files and directories are included
410 containsPath := func(paths []string, target string) bool {
411 targetPath := filepath.Join(tempDir, target)
412 for _, path := range paths {
413 if strings.HasPrefix(path, targetPath) {
414 return true
415 }
416 }
417 return false
418 }
419
420 assert.True(t, containsPath(files, "dir1"))
421 assert.True(t, containsPath(files, "file1.txt"))
422 assert.True(t, containsPath(files, "file2.txt"))
423 assert.True(t, containsPath(files, "dir1/file3.txt"))
424
425 // Check that hidden files and directories are not included
426 assert.False(t, containsPath(files, ".hidden_dir"))
427 assert.False(t, containsPath(files, ".hidden_file.txt"))
428 })
429
430 t.Run("respects limit and returns truncated flag", func(t *testing.T) {
431 files, truncated, err := listDirectory(tempDir, []string{}, 2)
432 require.NoError(t, err)
433 assert.True(t, truncated)
434 assert.Len(t, files, 2)
435 })
436
437 t.Run("respects ignore patterns", func(t *testing.T) {
438 files, truncated, err := listDirectory(tempDir, []string{"*.txt"}, 1000)
439 require.NoError(t, err)
440 assert.False(t, truncated)
441
442 // Check that no .txt files are included
443 for _, file := range files {
444 assert.False(t, strings.HasSuffix(file, ".txt"), "Found .txt file: %s", file)
445 }
446
447 // But directories should still be included
448 containsDir := false
449 for _, file := range files {
450 if strings.Contains(file, "dir1") {
451 containsDir = true
452 break
453 }
454 }
455 assert.True(t, containsDir)
456 })
457}