ls_test.go

  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}