1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15)
16
17// Test data structure for benchmark scenarios
18type benchmarkScenario struct {
19 name string
20 pattern string
21 description string
22}
23
24// Common benchmark scenarios
25var benchmarkScenarios = []benchmarkScenario{
26 {"SimpleExtension", "*.go", "Find all Go files in current directory"},
27 {"RecursiveExtension", "**/*.go", "Find all Go files recursively"},
28 {"MultipleExtensions", "*.{go,js,ts}", "Find multiple file types"},
29 {"RecursiveMultiple", "**/*.{go,js,ts,py}", "Find multiple file types recursively"},
30 {"VerySpecific", "internal/llm/tools/*.go", "Very specific path pattern"},
31}
32
33func TestGlobTool_Info(t *testing.T) {
34 tool := NewGlobTool("/tmp")
35 info := tool.Info()
36
37 assert.Equal(t, GlobToolName, info.Name)
38 assert.Contains(t, info.Description, "Fast file pattern matching tool")
39 assert.Contains(t, info.Required, "pattern")
40 assert.Contains(t, info.Parameters, "pattern")
41 assert.Contains(t, info.Parameters, "path")
42}
43
44func TestGlobTool_BasicFunctionality(t *testing.T) {
45 // Create a temporary directory structure for testing
46 tempDir := t.TempDir()
47 createTestFileStructure(t, tempDir)
48
49 tool := NewGlobTool(tempDir)
50 ctx := context.Background()
51
52 tests := []struct {
53 name string
54 pattern string
55 path string
56 expectFiles bool
57 expectError bool
58 expectedCount int
59 shouldContain []string
60 shouldNotContain []string
61 }{
62 {
63 name: "Find Go files",
64 pattern: "*.go",
65 expectFiles: true,
66 expectedCount: 2,
67 shouldContain: []string{"main.go", "test.go"},
68 },
69 {
70 name: "Find JS files recursively",
71 pattern: "**/*.js",
72 expectFiles: true,
73 expectedCount: 2,
74 shouldContain: []string{"app.js", "utils.js"},
75 },
76 {
77 name: "Multiple extensions",
78 pattern: "*.{go,txt}",
79 expectFiles: true,
80 expectedCount: 3,
81 shouldContain: []string{"main.go", "test.go", "readme.txt"},
82 },
83 {
84 name: "No matches",
85 pattern: "*.nonexistent",
86 expectFiles: false,
87 expectedCount: 0,
88 },
89 {
90 name: "Specific directory",
91 pattern: "src/**/*.js",
92 expectFiles: true,
93 expectedCount: 2,
94 shouldContain: []string{"app.js", "utils.js"},
95 },
96 {
97 name: "Empty pattern",
98 pattern: "",
99 expectError: true,
100 },
101 }
102
103 for _, tt := range tests {
104 t.Run(tt.name, func(t *testing.T) {
105 input := fmt.Sprintf(`{"pattern": "%s"`, tt.pattern)
106 if tt.path != "" {
107 input += fmt.Sprintf(`, "path": "%s"`, tt.path)
108 }
109 input += "}"
110
111 call := ToolCall{
112 ID: "test",
113 Name: GlobToolName,
114 Input: input,
115 }
116
117 response, err := tool.Run(ctx, call)
118
119 if tt.expectError {
120 assert.True(t, response.IsError)
121 return
122 }
123
124 require.NoError(t, err)
125 assert.False(t, response.IsError)
126
127 if !tt.expectFiles {
128 assert.Contains(t, response.Content, "No files found")
129 return
130 }
131
132 // Parse metadata
133 var metadata GlobResponseMetadata
134 if response.Metadata != "" {
135 err := json.Unmarshal([]byte(response.Metadata), &metadata)
136 require.NoError(t, err)
137 assert.Equal(t, tt.expectedCount, metadata.NumberOfFiles)
138 }
139
140 // Check file contents
141 files := strings.Split(response.Content, "\n")
142 actualCount := 0
143 for _, file := range files {
144 if strings.TrimSpace(file) != "" && !strings.Contains(file, "Results are truncated") {
145 actualCount++
146 }
147 }
148
149 assert.Equal(t, tt.expectedCount, actualCount)
150
151 // Check specific files are included
152 for _, expected := range tt.shouldContain {
153 assert.Contains(t, response.Content, expected, "Should contain %s", expected)
154 }
155
156 // Check specific files are not included
157 for _, notExpected := range tt.shouldNotContain {
158 assert.NotContains(t, response.Content, notExpected, "Should not contain %s", notExpected)
159 }
160 })
161 }
162}
163
164func TestGlobTool_Truncation(t *testing.T) {
165 // Create a directory with many files to test truncation
166 tempDir := t.TempDir()
167
168 // Create 150 files to exceed the 100 file limit
169 for i := 0; i < 150; i++ {
170 filename := filepath.Join(tempDir, fmt.Sprintf("file%03d.txt", i))
171 err := os.WriteFile(filename, []byte("test"), 0o644)
172 require.NoError(t, err)
173 }
174
175 tool := NewGlobTool(tempDir)
176 ctx := context.Background()
177
178 call := ToolCall{
179 ID: "test",
180 Name: GlobToolName,
181 Input: `{"pattern": "*.txt"}`,
182 }
183
184 response, err := tool.Run(ctx, call)
185 require.NoError(t, err)
186 assert.False(t, response.IsError)
187
188 // Should be truncated
189 assert.Contains(t, response.Content, "Results are truncated")
190
191 // Parse metadata
192 var metadata GlobResponseMetadata
193 err = json.Unmarshal([]byte(response.Metadata), &metadata)
194 require.NoError(t, err)
195 assert.Equal(t, 100, metadata.NumberOfFiles)
196 assert.True(t, metadata.Truncated)
197}
198
199func TestGlobTool_SortingByModTime(t *testing.T) {
200 tempDir := t.TempDir()
201
202 // Create files with different modification times
203 files := []string{"old.txt", "newer.txt", "newest.txt"}
204 basetime := time.Now().Add(-time.Hour)
205
206 for i, filename := range files {
207 path := filepath.Join(tempDir, filename)
208 err := os.WriteFile(path, []byte("test"), 0o644)
209 require.NoError(t, err)
210
211 // Set different modification times
212 modTime := basetime.Add(time.Duration(i) * time.Minute)
213 err = os.Chtimes(path, modTime, modTime)
214 require.NoError(t, err)
215 }
216
217 tool := NewGlobTool(tempDir)
218 ctx := context.Background()
219
220 call := ToolCall{
221 ID: "test",
222 Name: GlobToolName,
223 Input: `{"pattern": "*.txt"}`,
224 }
225
226 response, err := tool.Run(ctx, call)
227 require.NoError(t, err)
228 assert.False(t, response.IsError)
229
230 lines := strings.Split(strings.TrimSpace(response.Content), "\n")
231 require.Len(t, lines, 3)
232
233 // Should be sorted by modification time (newest first)
234 assert.Contains(t, lines[0], "newest.txt")
235 assert.Contains(t, lines[1], "newer.txt")
236 assert.Contains(t, lines[2], "old.txt")
237}
238
239func TestGlobTool_ErrorHandling(t *testing.T) {
240 tool := NewGlobTool("/tmp")
241 ctx := context.Background()
242
243 tests := []struct {
244 name string
245 input string
246 }{
247 {"Invalid JSON", `{"pattern": "*.go"`},
248 {"Missing pattern", `{"path": "/tmp"}`},
249 {"Empty pattern", `{"pattern": ""}`},
250 }
251
252 for _, tt := range tests {
253 t.Run(tt.name, func(t *testing.T) {
254 call := ToolCall{
255 ID: "test",
256 Name: GlobToolName,
257 Input: tt.input,
258 }
259
260 response, err := tool.Run(ctx, call)
261 require.NoError(t, err)
262 assert.True(t, response.IsError)
263 })
264 }
265}
266
267// Benchmark tests for performance comparison
268func BenchmarkGlobTool(b *testing.B) {
269 // Use the current project directory for realistic benchmarks
270 workingDir, err := os.Getwd()
271 require.NoError(b, err)
272
273 // Go up to the project root
274 for !strings.HasSuffix(workingDir, "crush") {
275 parent := filepath.Dir(workingDir)
276 if parent == workingDir {
277 b.Fatal("Could not find project root")
278 }
279 workingDir = parent
280 }
281
282 tool := NewGlobTool(workingDir)
283 ctx := context.Background()
284
285 for _, scenario := range benchmarkScenarios {
286 b.Run(scenario.name, func(b *testing.B) {
287 input := fmt.Sprintf(`{"pattern": "%s"}`, scenario.pattern)
288 call := ToolCall{
289 ID: "bench",
290 Name: GlobToolName,
291 Input: input,
292 }
293
294 b.ResetTimer()
295 for i := 0; i < b.N; i++ {
296 response, err := tool.Run(ctx, call)
297 if err != nil {
298 b.Fatal(err)
299 }
300 if response.IsError {
301 b.Fatal("Unexpected error response:", response.Content)
302 }
303 }
304 })
305 }
306}
307
308// Memory benchmark
309func BenchmarkGlobTool_Memory(b *testing.B) {
310 b.Skip("Skipping memory benchmark due to potential runtime issues")
311
312 workingDir, err := os.Getwd()
313 require.NoError(b, err)
314
315 // Go up to the project root
316 for !strings.HasSuffix(workingDir, "crush") {
317 parent := filepath.Dir(workingDir)
318 if parent == workingDir {
319 b.Fatal("Could not find project root")
320 }
321 workingDir = parent
322 }
323
324 tool := NewGlobTool(workingDir)
325 ctx := context.Background()
326
327 // Test memory usage with moderate search to avoid runtime issues
328 input := `{"pattern": "**/*.go"}`
329 call := ToolCall{
330 ID: "bench",
331 Name: GlobToolName,
332 Input: input,
333 }
334
335 b.ResetTimer()
336 b.ReportAllocs()
337
338 for i := 0; i < b.N; i++ {
339 response, err := tool.Run(ctx, call)
340 if err != nil {
341 b.Fatal(err)
342 }
343 if response.IsError {
344 b.Fatal("Unexpected error response:", response.Content)
345 }
346 }
347}
348
349// Benchmark different pattern complexities
350func BenchmarkGlobPatterns(b *testing.B) {
351 workingDir, err := os.Getwd()
352 require.NoError(b, err)
353
354 // Go up to the project root
355 for !strings.HasSuffix(workingDir, "crush") {
356 parent := filepath.Dir(workingDir)
357 if parent == workingDir {
358 b.Fatal("Could not find project root")
359 }
360 workingDir = parent
361 }
362
363 patterns := map[string]string{
364 "Simple": "*.go",
365 "SingleRecursive": "**/*.go",
366 "MultiExtension": "*.{go,js,ts,py}",
367 "MultiRecursive": "**/*.{go,js,ts,py}",
368 }
369
370 tool := NewGlobTool(workingDir)
371 ctx := context.Background()
372
373 for name, pattern := range patterns {
374 b.Run(name, func(b *testing.B) {
375 input := fmt.Sprintf(`{"pattern": "%s"}`, pattern)
376 call := ToolCall{
377 ID: "bench",
378 Name: GlobToolName,
379 Input: input,
380 }
381
382 b.ResetTimer()
383 for i := 0; i < b.N; i++ {
384 _, err := tool.Run(ctx, call)
385 if err != nil {
386 b.Fatal(err)
387 }
388 }
389 })
390 }
391}
392
393// Benchmark with different directory depths
394func BenchmarkGlobDepth(b *testing.B) {
395 // Create a temporary deep directory structure
396 tempDir := b.TempDir()
397 createDeepTestStructure(b, tempDir, 3, 5) // Reduced depth to avoid issues
398
399 tool := NewGlobTool(tempDir)
400 ctx := context.Background()
401
402 depths := map[string]string{
403 "Depth1": "*.txt",
404 "Depth2": "*/*.txt",
405 "DepthAll": "**/*.txt",
406 }
407
408 for name, pattern := range depths {
409 b.Run(name, func(b *testing.B) {
410 input := fmt.Sprintf(`{"pattern": "%s"}`, pattern)
411 call := ToolCall{
412 ID: "bench",
413 Name: GlobToolName,
414 Input: input,
415 }
416
417 b.ResetTimer()
418 for i := 0; i < b.N; i++ {
419 _, err := tool.Run(ctx, call)
420 if err != nil {
421 b.Fatal(err)
422 }
423 }
424 })
425 }
426}
427
428// Helper function to create test file structure
429func createTestFileStructure(t *testing.T, baseDir string) {
430 files := map[string]string{
431 "main.go": "package main",
432 "test.go": "package main",
433 "readme.txt": "README",
434 "src/app.js": "console.log('app')",
435 "src/utils.js": "console.log('utils')",
436 "docs/guide.md": "# Guide",
437 "config/app.json": "{}",
438 ".hidden/secret": "secret",
439 "node_modules/lib.js": "// lib",
440 }
441
442 for path, content := range files {
443 fullPath := filepath.Join(baseDir, path)
444 dir := filepath.Dir(fullPath)
445
446 err := os.MkdirAll(dir, 0o755)
447 require.NoError(t, err)
448
449 err = os.WriteFile(fullPath, []byte(content), 0o644)
450 require.NoError(t, err)
451 }
452}
453
454// Helper function to create deep directory structure for benchmarking
455func createDeepTestStructure(b *testing.B, baseDir string, depth, filesPerLevel int) {
456 var createLevel func(string, int)
457 createLevel = func(currentDir string, currentDepth int) {
458 if currentDepth >= depth {
459 return
460 }
461
462 // Create files at this level
463 for i := 0; i < filesPerLevel; i++ {
464 filename := filepath.Join(currentDir, fmt.Sprintf("file%d_%d.txt", currentDepth, i))
465 err := os.WriteFile(filename, []byte("test content"), 0o644)
466 require.NoError(b, err)
467 }
468
469 // Create subdirectories
470 for i := 0; i < 2; i++ { // Reduced from 3 to 2 to avoid issues
471 subDir := filepath.Join(currentDir, fmt.Sprintf("subdir%d_%d", currentDepth, i))
472 err := os.MkdirAll(subDir, 0o755)
473 require.NoError(b, err)
474 createLevel(subDir, currentDepth+1)
475 }
476 }
477
478 createLevel(baseDir, 0)
479}
480
481// Test to verify the tool works with the actual project structure
482func TestGlobTool_RealProject(t *testing.T) {
483 workingDir, err := os.Getwd()
484 require.NoError(t, err)
485
486 // Go up to the project root
487 for !strings.HasSuffix(workingDir, "crush") {
488 parent := filepath.Dir(workingDir)
489 if parent == workingDir {
490 t.Skip("Could not find project root")
491 }
492 workingDir = parent
493 }
494
495 tool := NewGlobTool(workingDir)
496 ctx := context.Background()
497
498 tests := []struct {
499 name string
500 pattern string
501 minFiles int // Minimum expected files (project might grow)
502 }{
503 {"Go files", "**/*.go", 10},
504 {"Test files", "**/*_test.go", 5},
505 {"Tool files", "internal/llm/tools/*.go", 5},
506 {"Config files", "*.{json,yaml,yml}", 2},
507 }
508
509 for _, tt := range tests {
510 t.Run(tt.name, func(t *testing.T) {
511 input := fmt.Sprintf(`{"pattern": "%s"}`, tt.pattern)
512 call := ToolCall{
513 ID: "test",
514 Name: GlobToolName,
515 Input: input,
516 }
517
518 response, err := tool.Run(ctx, call)
519 require.NoError(t, err)
520 assert.False(t, response.IsError)
521
522 var metadata GlobResponseMetadata
523 err = json.Unmarshal([]byte(response.Metadata), &metadata)
524 require.NoError(t, err)
525
526 assert.GreaterOrEqual(t, metadata.NumberOfFiles, tt.minFiles,
527 "Expected at least %d files for pattern %s, got %d",
528 tt.minFiles, tt.pattern, metadata.NumberOfFiles)
529 })
530 }
531}