package tools import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestLsTool_Info(t *testing.T) { tool := NewLsTool() info := tool.Info() assert.Equal(t, LSToolName, info.Name) assert.NotEmpty(t, info.Description) assert.Contains(t, info.Parameters, "path") assert.Contains(t, info.Parameters, "ignore") assert.Contains(t, info.Required, "path") } func TestLsTool_Run(t *testing.T) { // Create a temporary directory for testing tempDir, err := os.MkdirTemp("", "ls_tool_test") require.NoError(t, err) defer os.RemoveAll(tempDir) // Create a test directory structure testDirs := []string{ "dir1", "dir2", "dir2/subdir1", "dir2/subdir2", "dir3", "dir3/.hidden_dir", "__pycache__", } testFiles := []string{ "file1.txt", "file2.txt", "dir1/file3.txt", "dir2/file4.txt", "dir2/subdir1/file5.txt", "dir2/subdir2/file6.txt", "dir3/file7.txt", "dir3/.hidden_file.txt", "__pycache__/cache.pyc", ".hidden_root_file.txt", } // Create directories for _, dir := range testDirs { dirPath := filepath.Join(tempDir, dir) err := os.MkdirAll(dirPath, 0755) require.NoError(t, err) } // Create files for _, file := range testFiles { filePath := filepath.Join(tempDir, file) err := os.WriteFile(filePath, []byte("test content"), 0644) require.NoError(t, err) } t.Run("lists directory successfully", func(t *testing.T) { tool := NewLsTool() params := LSParams{ Path: tempDir, } paramsJSON, err := json.Marshal(params) require.NoError(t, err) call := ToolCall{ Name: LSToolName, Input: string(paramsJSON), } response, err := tool.Run(context.Background(), call) require.NoError(t, err) // Check that visible directories and files are included assert.Contains(t, response.Content, "dir1") assert.Contains(t, response.Content, "dir2") assert.Contains(t, response.Content, "dir3") assert.Contains(t, response.Content, "file1.txt") assert.Contains(t, response.Content, "file2.txt") // Check that hidden files and directories are not included assert.NotContains(t, response.Content, ".hidden_dir") assert.NotContains(t, response.Content, ".hidden_file.txt") assert.NotContains(t, response.Content, ".hidden_root_file.txt") // Check that __pycache__ is not included assert.NotContains(t, response.Content, "__pycache__") }) t.Run("handles non-existent path", func(t *testing.T) { tool := NewLsTool() params := LSParams{ Path: filepath.Join(tempDir, "non_existent_dir"), } paramsJSON, err := json.Marshal(params) require.NoError(t, err) call := ToolCall{ Name: LSToolName, Input: string(paramsJSON), } response, err := tool.Run(context.Background(), call) require.NoError(t, err) assert.Contains(t, response.Content, "path does not exist") }) t.Run("handles empty path parameter", func(t *testing.T) { // For this test, we need to mock the config.WorkingDirectory function // Since we can't easily do that, we'll just check that the response doesn't contain an error message tool := NewLsTool() params := LSParams{ Path: "", } paramsJSON, err := json.Marshal(params) require.NoError(t, err) call := ToolCall{ Name: LSToolName, Input: string(paramsJSON), } response, err := tool.Run(context.Background(), call) require.NoError(t, err) // The response should either contain a valid directory listing or an error // We'll just check that it's not empty assert.NotEmpty(t, response.Content) }) t.Run("handles invalid parameters", func(t *testing.T) { tool := NewLsTool() call := ToolCall{ Name: LSToolName, Input: "invalid json", } response, err := tool.Run(context.Background(), call) require.NoError(t, err) assert.Contains(t, response.Content, "error parsing parameters") }) t.Run("respects ignore patterns", func(t *testing.T) { tool := NewLsTool() params := LSParams{ Path: tempDir, Ignore: []string{"file1.txt", "dir1"}, } paramsJSON, err := json.Marshal(params) require.NoError(t, err) call := ToolCall{ Name: LSToolName, Input: string(paramsJSON), } response, err := tool.Run(context.Background(), call) require.NoError(t, err) // The output format is a tree, so we need to check for specific patterns // Check that file1.txt is not directly mentioned assert.NotContains(t, response.Content, "- file1.txt") // Check that dir1/ is not directly mentioned assert.NotContains(t, response.Content, "- dir1/") }) t.Run("handles relative path", func(t *testing.T) { // Save original working directory origWd, err := os.Getwd() require.NoError(t, err) defer func() { os.Chdir(origWd) }() // Change to a directory above the temp directory parentDir := filepath.Dir(tempDir) err = os.Chdir(parentDir) require.NoError(t, err) tool := NewLsTool() params := LSParams{ Path: filepath.Base(tempDir), } paramsJSON, err := json.Marshal(params) require.NoError(t, err) call := ToolCall{ Name: LSToolName, Input: string(paramsJSON), } response, err := tool.Run(context.Background(), call) require.NoError(t, err) // Should list the temp directory contents assert.Contains(t, response.Content, "dir1") assert.Contains(t, response.Content, "file1.txt") }) } func TestShouldSkip(t *testing.T) { testCases := []struct { name string path string ignorePatterns []string expected bool }{ { name: "hidden file", path: "/path/to/.hidden_file", ignorePatterns: []string{}, expected: true, }, { name: "hidden directory", path: "/path/to/.hidden_dir", ignorePatterns: []string{}, expected: true, }, { name: "pycache directory", path: "/path/to/__pycache__/file.pyc", ignorePatterns: []string{}, expected: true, }, { name: "node_modules directory", path: "/path/to/node_modules/package", ignorePatterns: []string{}, expected: false, // The shouldSkip function doesn't directly check for node_modules in the path }, { name: "normal file", path: "/path/to/normal_file.txt", ignorePatterns: []string{}, expected: false, }, { name: "normal directory", path: "/path/to/normal_dir", ignorePatterns: []string{}, expected: false, }, { name: "ignored by pattern", path: "/path/to/ignore_me.txt", ignorePatterns: []string{"ignore_*.txt"}, expected: true, }, { name: "not ignored by pattern", path: "/path/to/keep_me.txt", ignorePatterns: []string{"ignore_*.txt"}, expected: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := shouldSkip(tc.path, tc.ignorePatterns) assert.Equal(t, tc.expected, result) }) } } func TestCreateFileTree(t *testing.T) { paths := []string{ "/path/to/file1.txt", "/path/to/dir1/file2.txt", "/path/to/dir1/subdir/file3.txt", "/path/to/dir2/file4.txt", } tree := createFileTree(paths) // Check the structure of the tree assert.Len(t, tree, 1) // Should have one root node // Check the root node rootNode := tree[0] assert.Equal(t, "path", rootNode.Name) assert.Equal(t, "directory", rootNode.Type) assert.Len(t, rootNode.Children, 1) // Check the "to" node toNode := rootNode.Children[0] assert.Equal(t, "to", toNode.Name) assert.Equal(t, "directory", toNode.Type) assert.Len(t, toNode.Children, 3) // file1.txt, dir1, dir2 // Find the dir1 node var dir1Node *TreeNode for _, child := range toNode.Children { if child.Name == "dir1" { dir1Node = child break } } require.NotNil(t, dir1Node) assert.Equal(t, "directory", dir1Node.Type) assert.Len(t, dir1Node.Children, 2) // file2.txt and subdir } func TestPrintTree(t *testing.T) { // Create a simple tree tree := []*TreeNode{ { Name: "dir1", Path: "dir1", Type: "directory", Children: []*TreeNode{ { Name: "file1.txt", Path: "dir1/file1.txt", Type: "file", }, { Name: "subdir", Path: "dir1/subdir", Type: "directory", Children: []*TreeNode{ { Name: "file2.txt", Path: "dir1/subdir/file2.txt", Type: "file", }, }, }, }, }, { Name: "file3.txt", Path: "file3.txt", Type: "file", }, } result := printTree(tree, "/root") // Check the output format assert.Contains(t, result, "- /root/") assert.Contains(t, result, " - dir1/") assert.Contains(t, result, " - file1.txt") assert.Contains(t, result, " - subdir/") assert.Contains(t, result, " - file2.txt") assert.Contains(t, result, " - file3.txt") } func TestListDirectory(t *testing.T) { // Create a temporary directory for testing tempDir, err := os.MkdirTemp("", "list_directory_test") require.NoError(t, err) defer os.RemoveAll(tempDir) // Create a test directory structure testDirs := []string{ "dir1", "dir1/subdir1", ".hidden_dir", } testFiles := []string{ "file1.txt", "file2.txt", "dir1/file3.txt", "dir1/subdir1/file4.txt", ".hidden_file.txt", } // Create directories for _, dir := range testDirs { dirPath := filepath.Join(tempDir, dir) err := os.MkdirAll(dirPath, 0755) require.NoError(t, err) } // Create files for _, file := range testFiles { filePath := filepath.Join(tempDir, file) err := os.WriteFile(filePath, []byte("test content"), 0644) require.NoError(t, err) } t.Run("lists files with no limit", func(t *testing.T) { files, truncated, err := listDirectory(tempDir, []string{}, 1000) require.NoError(t, err) assert.False(t, truncated) // Check that visible files and directories are included containsPath := func(paths []string, target string) bool { targetPath := filepath.Join(tempDir, target) for _, path := range paths { if strings.HasPrefix(path, targetPath) { return true } } return false } assert.True(t, containsPath(files, "dir1")) assert.True(t, containsPath(files, "file1.txt")) assert.True(t, containsPath(files, "file2.txt")) assert.True(t, containsPath(files, "dir1/file3.txt")) // Check that hidden files and directories are not included assert.False(t, containsPath(files, ".hidden_dir")) assert.False(t, containsPath(files, ".hidden_file.txt")) }) t.Run("respects limit and returns truncated flag", func(t *testing.T) { files, truncated, err := listDirectory(tempDir, []string{}, 2) require.NoError(t, err) assert.True(t, truncated) assert.Len(t, files, 2) }) t.Run("respects ignore patterns", func(t *testing.T) { files, truncated, err := listDirectory(tempDir, []string{"*.txt"}, 1000) require.NoError(t, err) assert.False(t, truncated) // Check that no .txt files are included for _, file := range files { assert.False(t, strings.HasSuffix(file, ".txt"), "Found .txt file: %s", file) } // But directories should still be included containsDir := false for _, file := range files { if strings.Contains(file, "dir1") { containsDir = true break } } assert.True(t, containsDir) }) }