@@ -0,0 +1,531 @@
+package tools
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// Test data structure for benchmark scenarios
+type benchmarkScenario struct {
+ name string
+ pattern string
+ description string
+}
+
+// Common benchmark scenarios
+var benchmarkScenarios = []benchmarkScenario{
+ {"SimpleExtension", "*.go", "Find all Go files in current directory"},
+ {"RecursiveExtension", "**/*.go", "Find all Go files recursively"},
+ {"MultipleExtensions", "*.{go,js,ts}", "Find multiple file types"},
+ {"RecursiveMultiple", "**/*.{go,js,ts,py}", "Find multiple file types recursively"},
+ {"VerySpecific", "internal/llm/tools/*.go", "Very specific path pattern"},
+}
+
+func TestGlobTool_Info(t *testing.T) {
+ tool := NewGlobTool("/tmp")
+ info := tool.Info()
+
+ assert.Equal(t, GlobToolName, info.Name)
+ assert.Contains(t, info.Description, "Fast file pattern matching tool")
+ assert.Contains(t, info.Required, "pattern")
+ assert.Contains(t, info.Parameters, "pattern")
+ assert.Contains(t, info.Parameters, "path")
+}
+
+func TestGlobTool_BasicFunctionality(t *testing.T) {
+ // Create a temporary directory structure for testing
+ tempDir := t.TempDir()
+ createTestFileStructure(t, tempDir)
+
+ tool := NewGlobTool(tempDir)
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ pattern string
+ path string
+ expectFiles bool
+ expectError bool
+ expectedCount int
+ shouldContain []string
+ shouldNotContain []string
+ }{
+ {
+ name: "Find Go files",
+ pattern: "*.go",
+ expectFiles: true,
+ expectedCount: 2,
+ shouldContain: []string{"main.go", "test.go"},
+ },
+ {
+ name: "Find JS files recursively",
+ pattern: "**/*.js",
+ expectFiles: true,
+ expectedCount: 2,
+ shouldContain: []string{"app.js", "utils.js"},
+ },
+ {
+ name: "Multiple extensions",
+ pattern: "*.{go,txt}",
+ expectFiles: true,
+ expectedCount: 3,
+ shouldContain: []string{"main.go", "test.go", "readme.txt"},
+ },
+ {
+ name: "No matches",
+ pattern: "*.nonexistent",
+ expectFiles: false,
+ expectedCount: 0,
+ },
+ {
+ name: "Specific directory",
+ pattern: "src/**/*.js",
+ expectFiles: true,
+ expectedCount: 2,
+ shouldContain: []string{"app.js", "utils.js"},
+ },
+ {
+ name: "Empty pattern",
+ pattern: "",
+ expectError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ input := fmt.Sprintf(`{"pattern": "%s"`, tt.pattern)
+ if tt.path != "" {
+ input += fmt.Sprintf(`, "path": "%s"`, tt.path)
+ }
+ input += "}"
+
+ call := ToolCall{
+ ID: "test",
+ Name: GlobToolName,
+ Input: input,
+ }
+
+ response, err := tool.Run(ctx, call)
+
+ if tt.expectError {
+ assert.True(t, response.IsError)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.False(t, response.IsError)
+
+ if !tt.expectFiles {
+ assert.Contains(t, response.Content, "No files found")
+ return
+ }
+
+ // Parse metadata
+ var metadata GlobResponseMetadata
+ if response.Metadata != "" {
+ err := json.Unmarshal([]byte(response.Metadata), &metadata)
+ require.NoError(t, err)
+ assert.Equal(t, tt.expectedCount, metadata.NumberOfFiles)
+ }
+
+ // Check file contents
+ files := strings.Split(response.Content, "\n")
+ actualCount := 0
+ for _, file := range files {
+ if strings.TrimSpace(file) != "" && !strings.Contains(file, "Results are truncated") {
+ actualCount++
+ }
+ }
+
+ assert.Equal(t, tt.expectedCount, actualCount)
+
+ // Check specific files are included
+ for _, expected := range tt.shouldContain {
+ assert.Contains(t, response.Content, expected, "Should contain %s", expected)
+ }
+
+ // Check specific files are not included
+ for _, notExpected := range tt.shouldNotContain {
+ assert.NotContains(t, response.Content, notExpected, "Should not contain %s", notExpected)
+ }
+ })
+ }
+}
+
+func TestGlobTool_Truncation(t *testing.T) {
+ // Create a directory with many files to test truncation
+ tempDir := t.TempDir()
+
+ // Create 150 files to exceed the 100 file limit
+ for i := 0; i < 150; i++ {
+ filename := filepath.Join(tempDir, fmt.Sprintf("file%03d.txt", i))
+ err := os.WriteFile(filename, []byte("test"), 0o644)
+ require.NoError(t, err)
+ }
+
+ tool := NewGlobTool(tempDir)
+ ctx := context.Background()
+
+ call := ToolCall{
+ ID: "test",
+ Name: GlobToolName,
+ Input: `{"pattern": "*.txt"}`,
+ }
+
+ response, err := tool.Run(ctx, call)
+ require.NoError(t, err)
+ assert.False(t, response.IsError)
+
+ // Should be truncated
+ assert.Contains(t, response.Content, "Results are truncated")
+
+ // Parse metadata
+ var metadata GlobResponseMetadata
+ err = json.Unmarshal([]byte(response.Metadata), &metadata)
+ require.NoError(t, err)
+ assert.Equal(t, 100, metadata.NumberOfFiles)
+ assert.True(t, metadata.Truncated)
+}
+
+func TestGlobTool_SortingByModTime(t *testing.T) {
+ tempDir := t.TempDir()
+
+ // Create files with different modification times
+ files := []string{"old.txt", "newer.txt", "newest.txt"}
+ basetime := time.Now().Add(-time.Hour)
+
+ for i, filename := range files {
+ path := filepath.Join(tempDir, filename)
+ err := os.WriteFile(path, []byte("test"), 0o644)
+ require.NoError(t, err)
+
+ // Set different modification times
+ modTime := basetime.Add(time.Duration(i) * time.Minute)
+ err = os.Chtimes(path, modTime, modTime)
+ require.NoError(t, err)
+ }
+
+ tool := NewGlobTool(tempDir)
+ ctx := context.Background()
+
+ call := ToolCall{
+ ID: "test",
+ Name: GlobToolName,
+ Input: `{"pattern": "*.txt"}`,
+ }
+
+ response, err := tool.Run(ctx, call)
+ require.NoError(t, err)
+ assert.False(t, response.IsError)
+
+ lines := strings.Split(strings.TrimSpace(response.Content), "\n")
+ require.Len(t, lines, 3)
+
+ // Should be sorted by modification time (newest first)
+ assert.Contains(t, lines[0], "newest.txt")
+ assert.Contains(t, lines[1], "newer.txt")
+ assert.Contains(t, lines[2], "old.txt")
+}
+
+func TestGlobTool_ErrorHandling(t *testing.T) {
+ tool := NewGlobTool("/tmp")
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ input string
+ }{
+ {"Invalid JSON", `{"pattern": "*.go"`},
+ {"Missing pattern", `{"path": "/tmp"}`},
+ {"Empty pattern", `{"pattern": ""}`},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ call := ToolCall{
+ ID: "test",
+ Name: GlobToolName,
+ Input: tt.input,
+ }
+
+ response, err := tool.Run(ctx, call)
+ require.NoError(t, err)
+ assert.True(t, response.IsError)
+ })
+ }
+}
+
+// Benchmark tests for performance comparison
+func BenchmarkGlobTool(b *testing.B) {
+ // Use the current project directory for realistic benchmarks
+ workingDir, err := os.Getwd()
+ require.NoError(b, err)
+
+ // Go up to the project root
+ for !strings.HasSuffix(workingDir, "crush") {
+ parent := filepath.Dir(workingDir)
+ if parent == workingDir {
+ b.Fatal("Could not find project root")
+ }
+ workingDir = parent
+ }
+
+ tool := NewGlobTool(workingDir)
+ ctx := context.Background()
+
+ for _, scenario := range benchmarkScenarios {
+ b.Run(scenario.name, func(b *testing.B) {
+ input := fmt.Sprintf(`{"pattern": "%s"}`, scenario.pattern)
+ call := ToolCall{
+ ID: "bench",
+ Name: GlobToolName,
+ Input: input,
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ response, err := tool.Run(ctx, call)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if response.IsError {
+ b.Fatal("Unexpected error response:", response.Content)
+ }
+ }
+ })
+ }
+}
+
+// Memory benchmark
+func BenchmarkGlobTool_Memory(b *testing.B) {
+ b.Skip("Skipping memory benchmark due to potential runtime issues")
+
+ workingDir, err := os.Getwd()
+ require.NoError(b, err)
+
+ // Go up to the project root
+ for !strings.HasSuffix(workingDir, "crush") {
+ parent := filepath.Dir(workingDir)
+ if parent == workingDir {
+ b.Fatal("Could not find project root")
+ }
+ workingDir = parent
+ }
+
+ tool := NewGlobTool(workingDir)
+ ctx := context.Background()
+
+ // Test memory usage with moderate search to avoid runtime issues
+ input := `{"pattern": "**/*.go"}`
+ call := ToolCall{
+ ID: "bench",
+ Name: GlobToolName,
+ Input: input,
+ }
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for i := 0; i < b.N; i++ {
+ response, err := tool.Run(ctx, call)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if response.IsError {
+ b.Fatal("Unexpected error response:", response.Content)
+ }
+ }
+}
+
+// Benchmark different pattern complexities
+func BenchmarkGlobPatterns(b *testing.B) {
+ workingDir, err := os.Getwd()
+ require.NoError(b, err)
+
+ // Go up to the project root
+ for !strings.HasSuffix(workingDir, "crush") {
+ parent := filepath.Dir(workingDir)
+ if parent == workingDir {
+ b.Fatal("Could not find project root")
+ }
+ workingDir = parent
+ }
+
+ patterns := map[string]string{
+ "Simple": "*.go",
+ "SingleRecursive": "**/*.go",
+ "MultiExtension": "*.{go,js,ts,py}",
+ "MultiRecursive": "**/*.{go,js,ts,py}",
+ }
+
+ tool := NewGlobTool(workingDir)
+ ctx := context.Background()
+
+ for name, pattern := range patterns {
+ b.Run(name, func(b *testing.B) {
+ input := fmt.Sprintf(`{"pattern": "%s"}`, pattern)
+ call := ToolCall{
+ ID: "bench",
+ Name: GlobToolName,
+ Input: input,
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := tool.Run(ctx, call)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ }
+}
+
+// Benchmark with different directory depths
+func BenchmarkGlobDepth(b *testing.B) {
+ // Create a temporary deep directory structure
+ tempDir := b.TempDir()
+ createDeepTestStructure(b, tempDir, 3, 5) // Reduced depth to avoid issues
+
+ tool := NewGlobTool(tempDir)
+ ctx := context.Background()
+
+ depths := map[string]string{
+ "Depth1": "*.txt",
+ "Depth2": "*/*.txt",
+ "DepthAll": "**/*.txt",
+ }
+
+ for name, pattern := range depths {
+ b.Run(name, func(b *testing.B) {
+ input := fmt.Sprintf(`{"pattern": "%s"}`, pattern)
+ call := ToolCall{
+ ID: "bench",
+ Name: GlobToolName,
+ Input: input,
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := tool.Run(ctx, call)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ }
+}
+
+// Helper function to create test file structure
+func createTestFileStructure(t *testing.T, baseDir string) {
+ files := map[string]string{
+ "main.go": "package main",
+ "test.go": "package main",
+ "readme.txt": "README",
+ "src/app.js": "console.log('app')",
+ "src/utils.js": "console.log('utils')",
+ "docs/guide.md": "# Guide",
+ "config/app.json": "{}",
+ ".hidden/secret": "secret",
+ "node_modules/lib.js": "// lib",
+ }
+
+ for path, content := range files {
+ fullPath := filepath.Join(baseDir, path)
+ dir := filepath.Dir(fullPath)
+
+ err := os.MkdirAll(dir, 0o755)
+ require.NoError(t, err)
+
+ err = os.WriteFile(fullPath, []byte(content), 0o644)
+ require.NoError(t, err)
+ }
+}
+
+// Helper function to create deep directory structure for benchmarking
+func createDeepTestStructure(b *testing.B, baseDir string, depth, filesPerLevel int) {
+ var createLevel func(string, int)
+ createLevel = func(currentDir string, currentDepth int) {
+ if currentDepth >= depth {
+ return
+ }
+
+ // Create files at this level
+ for i := 0; i < filesPerLevel; i++ {
+ filename := filepath.Join(currentDir, fmt.Sprintf("file%d_%d.txt", currentDepth, i))
+ err := os.WriteFile(filename, []byte("test content"), 0o644)
+ require.NoError(b, err)
+ }
+
+ // Create subdirectories
+ for i := 0; i < 2; i++ { // Reduced from 3 to 2 to avoid issues
+ subDir := filepath.Join(currentDir, fmt.Sprintf("subdir%d_%d", currentDepth, i))
+ err := os.MkdirAll(subDir, 0o755)
+ require.NoError(b, err)
+ createLevel(subDir, currentDepth+1)
+ }
+ }
+
+ createLevel(baseDir, 0)
+}
+
+// Test to verify the tool works with the actual project structure
+func TestGlobTool_RealProject(t *testing.T) {
+ workingDir, err := os.Getwd()
+ require.NoError(t, err)
+
+ // Go up to the project root
+ for !strings.HasSuffix(workingDir, "crush") {
+ parent := filepath.Dir(workingDir)
+ if parent == workingDir {
+ t.Skip("Could not find project root")
+ }
+ workingDir = parent
+ }
+
+ tool := NewGlobTool(workingDir)
+ ctx := context.Background()
+
+ tests := []struct {
+ name string
+ pattern string
+ minFiles int // Minimum expected files (project might grow)
+ }{
+ {"Go files", "**/*.go", 10},
+ {"Test files", "**/*_test.go", 5},
+ {"Tool files", "internal/llm/tools/*.go", 5},
+ {"Config files", "*.{json,yaml,yml}", 2},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ input := fmt.Sprintf(`{"pattern": "%s"}`, tt.pattern)
+ call := ToolCall{
+ ID: "test",
+ Name: GlobToolName,
+ Input: input,
+ }
+
+ response, err := tool.Run(ctx, call)
+ require.NoError(t, err)
+ assert.False(t, response.IsError)
+
+ var metadata GlobResponseMetadata
+ err = json.Unmarshal([]byte(response.Metadata), &metadata)
+ require.NoError(t, err)
+
+ assert.GreaterOrEqual(t, metadata.NumberOfFiles, tt.minFiles,
+ "Expected at least %d files for pattern %s, got %d",
+ tt.minFiles, tt.pattern, metadata.NumberOfFiles)
+ })
+ }
+}