package tools import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "time" ) // searchRepo builds a small tree with the shapes that matter: nesting, a // pruned directory, and a binary file. func searchRepo(t *testing.T) string { t.Helper() dir := t.TempDir() writeFile(t, dir, "go.mod", "module codey\n") writeFile(t, dir, "cmd/codey/main.go", "package main\n\nfunc main() { run() }\n") writeFile(t, dir, "internal/agent/loop.go", "package agent\n\nfunc Run() {}\n") writeFile(t, dir, "internal/agent/loop_test.go", "package agent\n\nfunc TestRun(t *testing.T) {}\n") writeFile(t, dir, "README.md", "# codey\n\nRun it.\n") writeFile(t, dir, "node_modules/junk/index.js", "function run() {}\n") writeFile(t, dir, ".git/config", "[core]\n\trun = true\n") if err := os.WriteFile(filepath.Join(dir, "bin.dat"), []byte{0x00, 0x01, 'r', 'u', 'n'}, 0o644); err != nil { t.Fatal(err) } return dir } func call(t *testing.T, tool Tool, args any) string { t.Helper() raw, err := json.Marshal(args) if err != nil { t.Fatal(err) } out, err := tool.Execute(context.Background(), raw) if err != nil { t.Fatalf("%s: %v", tool.Name(), err) } return out } func TestGlob(t *testing.T) { dir := searchRepo(t) tool := NewGlob(dir) tests := []struct { name string args GlobArgs want []string unwanted []string }{ { name: "bare pattern matches at any depth", args: GlobArgs{Pattern: "*.go"}, want: []string{"cmd/codey/main.go", "internal/agent/loop.go", "internal/agent/loop_test.go"}, unwanted: []string{"go.mod", "README.md"}, }, { name: "pattern with a slash is anchored to the subtree", args: GlobArgs{Pattern: "internal/**/*_test.go"}, want: []string{"internal/agent/loop_test.go"}, unwanted: []string{"cmd/codey/main.go"}, }, { name: "path narrows the search", args: GlobArgs{Pattern: "*.go", Path: "cmd"}, want: []string{"cmd/codey/main.go"}, unwanted: []string{"internal/agent/loop.go"}, }, { name: "noise directories are pruned", args: GlobArgs{Pattern: "*.js"}, unwanted: []string{"node_modules"}, }, { name: "exact name", args: GlobArgs{Pattern: "go.mod"}, want: []string{"go.mod"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { out := call(t, tool, tc.args) for _, w := range tc.want { if !strings.Contains(out, w) { t.Errorf("missing %q in:\n%s", w, out) } } for _, u := range tc.unwanted { if strings.Contains(out, u) { t.Errorf("unexpected %q in:\n%s", u, out) } } }) } } func TestGlobNoMatchesSaysSoUsefully(t *testing.T) { out := call(t, NewGlob(searchRepo(t)), GlobArgs{Pattern: "*.rs"}) if !strings.Contains(out, "No files match") || !strings.Contains(out, "files scanned") { t.Errorf("a miss should report the scan, got: %s", out) } } func TestGlobSortsNewestFirst(t *testing.T) { dir := searchRepo(t) newest := filepath.Join(dir, "internal/agent/loop.go") if err := os.Chtimes(newest, timeNow(), timeNow()); err != nil { t.Fatal(err) } out := call(t, NewGlob(dir), GlobArgs{Pattern: "*.go"}) if first := strings.SplitN(out, "\n", 2)[0]; first != "internal/agent/loop.go" { t.Errorf("first result = %q, want the most recently modified file", first) } } func TestGlobErrors(t *testing.T) { tool := NewGlob(searchRepo(t)) for _, tc := range []struct { name, want string args GlobArgs }{ {name: "no pattern", want: "pattern is required", args: GlobArgs{}}, {name: "bad path", want: "no such directory", args: GlobArgs{Pattern: "*.go", Path: "nowhere"}}, } { t.Run(tc.name, func(t *testing.T) { raw, _ := json.Marshal(tc.args) if _, err := tool.Execute(context.Background(), raw); err == nil || !strings.Contains(err.Error(), tc.want) { t.Errorf("error = %v, want %q", err, tc.want) } }) } } func TestGrep(t *testing.T) { dir := searchRepo(t) tests := []struct { name string args GrepArgs want []string unwanted []string }{ { name: "finds a symbol with path and line number", args: GrepArgs{Pattern: `func Run\(\)`}, want: []string{"internal/agent/loop.go:3: func Run() {}"}, }, { name: "glob narrows to a file type", args: GrepArgs{Pattern: "Run", Glob: "*.md"}, want: []string{"README.md"}, unwanted: []string{".go:"}, }, { name: "case sensitive by default", args: GrepArgs{Pattern: "RUN"}, unwanted: []string{".go:", ".md:"}, }, { name: "ignore_case", args: GrepArgs{Pattern: "RUN", IgnoreCase: true}, want: []string{"README.md"}, }, { name: "noise directories and binaries are skipped", args: GrepArgs{Pattern: "run"}, unwanted: []string{"node_modules", ".git/config", "bin.dat"}, }, { name: "path narrows the search", args: GrepArgs{Pattern: "package", Path: "cmd"}, want: []string{"cmd/codey/main.go"}, }, } // Both engines must produce identical output, or eval numbers would depend // on whether the machine has ripgrep installed. for _, engine := range []string{"ripgrep", "walk"} { t.Run(engine, func(t *testing.T) { if engine == "walk" { saved := ripgrep ripgrep = "" t.Cleanup(func() { ripgrep = saved }) } else if ripgrep == "" { t.Skip("ripgrep not installed") } tool := NewGrep(dir) for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { out := call(t, tool, tc.args) for _, w := range tc.want { if !strings.Contains(out, w) { t.Errorf("missing %q in:\n%s", w, out) } } for _, u := range tc.unwanted { if strings.Contains(out, u) { t.Errorf("unexpected %q in:\n%s", u, out) } } }) } }) } } func TestGrepEnginesAgree(t *testing.T) { if ripgrep == "" { t.Skip("ripgrep not installed") } dir := searchRepo(t) withRG := call(t, NewGrep(dir), GrepArgs{Pattern: "func"}) saved := ripgrep ripgrep = "" defer func() { ripgrep = saved }() withWalk := call(t, NewGrep(dir), GrepArgs{Pattern: "func"}) if withRG != withWalk { t.Errorf("engines disagree:\nripgrep:\n%s\nwalk:\n%s", withRG, withWalk) } } func TestGrepNoMatchIsNotAnError(t *testing.T) { for _, engine := range []string{"ripgrep", "walk"} { t.Run(engine, func(t *testing.T) { if engine == "walk" { saved := ripgrep ripgrep = "" t.Cleanup(func() { ripgrep = saved }) } else if ripgrep == "" { t.Skip("ripgrep not installed") } out := call(t, NewGrep(searchRepo(t)), GrepArgs{Pattern: "zzzznotpresent"}) if !strings.Contains(out, "No matches") { t.Errorf("want an explicit no-match message, got %q", out) } }) } } func TestGrepInvalidRegexIsReported(t *testing.T) { raw, _ := json.Marshal(GrepArgs{Pattern: "func ("}) _, err := NewGrep(searchRepo(t)).Execute(context.Background(), raw) if err == nil || !strings.Contains(err.Error(), "invalid regular expression") { t.Errorf("error = %v, want a regex error the model can fix", err) } } func timeNow() time.Time { return time.Now().Add(time.Hour) }