package files_test import ( "net/http" "os" "path/filepath" "testing" "rickub.com/bots-garden/ori/internal/files" ) // searchWorkspace extends the basic tree with nested and noisy directories. func searchWorkspace(t *testing.T) (string, http.Handler) { t.Helper() root, h := workspace(t) for _, rel := range []string{ "src/server/main_test.go", "docs/guide.md", "node_modules/pkg/index.js", ".git/HEAD", } { full := filepath.Join(root, rel) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte("x"), 0o644); err != nil { t.Fatal(err) } } return root, h } func relPaths(hits []files.Hit) []string { out := make([]string, 0, len(hits)) for _, h := range hits { out = append(out, h.RelPath) } return out } func TestSearchIsRecursiveAndSkipsNoise(t *testing.T) { root, _ := searchWorkspace(t) hits := files.New(root).Search("", 0) got := relPaths(hits) // Ranked: shorter relative paths first, alphabetical among equals. want := []string{"README.md", "src/main.go", "docs/guide.md", "src/server/main_test.go"} if len(got) != len(want) { t.Fatalf("hits = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("hit[%d] = %q, want %q", i, got[i], want[i]) } } if hits[1].Path != filepath.Join(root, "src", "main.go") || hits[1].Name != "main.go" { t.Errorf("hit = %+v, want absolute path and base name", hits[1]) } } func TestSearchFiltersCaseInsensitivelyAndRanksBaseNames(t *testing.T) { root, _ := searchWorkspace(t) cases := []struct { query string want []string }{ {"MAIN", []string{"src/main.go", "src/server/main_test.go"}}, // "src" matches only through the directory: still found. {"src/", []string{"src/main.go", "src/server/main_test.go"}}, {"guide", []string{"docs/guide.md"}}, {"nothing-here", []string{}}, } for _, tc := range cases { got := relPaths(files.New(root).Search(tc.query, 10)) if len(got) != len(tc.want) { t.Errorf("Search(%q) = %v, want %v", tc.query, got, tc.want) continue } for i := range tc.want { if got[i] != tc.want[i] { t.Errorf("Search(%q)[%d] = %q, want %q", tc.query, i, got[i], tc.want[i]) } } } } func TestSearchHonoursLimit(t *testing.T) { root, _ := searchWorkspace(t) if got := files.New(root).Search("", 2); len(got) != 2 { t.Errorf("limit 2 returned %d hits", len(got)) } if got := files.New(root).Search("", 100000); len(got) != 4 { t.Errorf("oversized limit returned %d hits, want all 4", len(got)) } } func TestSearchEndpoint(t *testing.T) { _, h := searchWorkspace(t) rec := do(t, h, http.MethodGet, "/api/files/search?q=main&limit=1", "") if rec.Code != http.StatusOK { t.Fatalf("GET search = %d (%s)", rec.Code, rec.Body.String()) } hits := decodeBody(t, rec)["files"].([]any) if len(hits) != 1 || hits[0].(map[string]any)["relPath"] != "src/main.go" { t.Errorf("files = %v, want [src/main.go]", hits) } rec = do(t, h, http.MethodGet, "/api/files/search?q=zzz", "") if got := decodeBody(t, rec)["files"].([]any); len(got) != 0 { t.Errorf("no-match search = %v, want an empty array", got) } } func TestSearchEndpointOnMissingRootIs404(t *testing.T) { mux := http.NewServeMux() for pattern, handler := range files.New(filepath.Join(t.TempDir(), "gone")).Routes() { mux.Handle(pattern, handler) } if rec := do(t, mux, http.MethodGet, "/api/files/search", ""); rec.Code != http.StatusNotFound { t.Errorf("search on missing root = %d, want 404", rec.Code) } }