@@ -1,8 +1,14 @@
11package tools
22
33import (
4+ "context"
5+ "encoding/json"
6+ "os"
7+ "path/filepath"
48 "regexp"
59 "testing"
10+
11+ "github.com/stretchr/testify/require"
612)
713
814func TestRegexCache(t *testing.T) {
@@ -52,6 +58,114 @@ func TestGlobToRegexCaching(t *testing.T) {
5258 }
5359}
5460
61+func TestGrepWithIgnoreFiles(t *testing.T) {
62+ tempDir := t.TempDir()
63+
64+ // Create test files
65+ testFiles := map[string]string{
66+ "file1.txt": "hello world",
67+ "file2.txt": "hello world",
68+ "ignored/file3.txt": "hello world",
69+ "node_modules/lib.js": "hello world",
70+ "secret.key": "hello world",
71+ }
72+
73+ for path, content := range testFiles {
74+ fullPath := filepath.Join(tempDir, path)
75+ require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755))
76+ require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644))
77+ }
78+
79+ // Create .gitignore file
80+ gitignoreContent := "ignored/\n*.key\n"
81+ require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".gitignore"), []byte(gitignoreContent), 0o644))
82+
83+ // Create .crushignore file
84+ crushignoreContent := "node_modules/\n"
85+ require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".crushignore"), []byte(crushignoreContent), 0o644))
86+
87+ // Create grep tool
88+ grepTool := NewGrepTool(tempDir)
89+
90+ // Create grep parameters
91+ params := GrepParams{
92+ Pattern: "hello world",
93+ Path: tempDir,
94+ }
95+ paramsJSON, err := json.Marshal(params)
96+ require.NoError(t, err)
97+
98+ // Run grep
99+ call := ToolCall{Input: string(paramsJSON)}
100+ response, err := grepTool.Run(context.Background(), call)
101+ require.NoError(t, err)
102+
103+ // Check results - should only find file1.txt and file2.txt
104+ // ignored/file3.txt should be ignored by .gitignore
105+ // node_modules/lib.js should be ignored by .crushignore
106+ // secret.key should be ignored by .gitignore
107+ result := response.Content
108+ require.Contains(t, result, "file1.txt")
109+ require.Contains(t, result, "file2.txt")
110+ require.NotContains(t, result, "file3.txt")
111+ require.NotContains(t, result, "lib.js")
112+ require.NotContains(t, result, "secret.key")
113+}
114+
115+func TestSearchImplementations(t *testing.T) {
116+ t.Parallel()
117+ tempDir := t.TempDir()
118+
119+ for path, content := range map[string]string{
120+ "file1.go": "package main\nfunc main() {\n\tfmt.Println(\"hello world\")\n}",
121+ "file2.js": "console.log('hello world');",
122+ "file3.txt": "hello world from text file",
123+ "binary.exe": "\x00\x01\x02\x03",
124+ "empty.txt": "",
125+ "subdir/nested.go": "package nested\n// hello world comment",
126+ ".hidden.txt": "hello world in hidden file",
127+ "file4.txt": "hello world from a banana",
128+ "file5.txt": "hello world from a grape",
129+ } {
130+ fullPath := filepath.Join(tempDir, path)
131+ require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755))
132+ require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644))
133+ }
134+
135+ require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".gitignore"), []byte("file4.txt\n"), 0o644))
136+ require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".crushignore"), []byte("file5.txt\n"), 0o644))
137+
138+ for name, fn := range map[string]func(pattern, path, include string) ([]grepMatch, error){
139+ "regex": searchFilesWithRegex,
140+ "rg": func(pattern, path, include string) ([]grepMatch, error) {
141+ return searchWithRipgrep(t.Context(), pattern, path, include)
142+ },
143+ } {
144+ t.Run(name, func(t *testing.T) {
145+ t.Parallel()
146+
147+ if name == "rg" && getRg() == "" {
148+ t.Skip("rg is not in $PATH")
149+ }
150+
151+ matches, err := fn("hello world", tempDir, "")
152+ require.NoError(t, err)
153+
154+ require.Equal(t, len(matches), 4)
155+ for _, match := range matches {
156+ require.NotEmpty(t, match.path)
157+ require.NotZero(t, match.lineNum)
158+ require.NotEmpty(t, match.lineText)
159+ require.NotZero(t, match.modTime)
160+ require.NotContains(t, match.path, ".hidden.txt")
161+ require.NotContains(t, match.path, "file4.txt")
162+ require.NotContains(t, match.path, "file5.txt")
163+ require.NotContains(t, match.path, "binary.exe")
164+ }
165+ })
166+ }
167+}
168+
55169// Benchmark to show performance improvement
56170func BenchmarkRegexCacheVsCompile(b *testing.B) {
57171 cache := newRegexCache()