forked from bots-garden/ori
| ✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme | 1 | package files_test |
| 2 | ||
| 3 | import ( | |
| 4 | "net/http" | |
| 5 | "os" | |
| 6 | "path/filepath" | |
| 7 | "testing" | |
| 8 | ||
| 9 | "rickub.com/bots-garden/ori/internal/files" | |
| 10 | ) | |
| 11 | ||
| 12 | // searchWorkspace extends the basic tree with nested and noisy directories. | |
| 13 | func searchWorkspace(t *testing.T) (string, http.Handler) { | |
| 14 | t.Helper() | |
| 15 | root, h := workspace(t) | |
| 16 | for _, rel := range []string{ | |
| 17 | "src/server/main_test.go", | |
| 18 | "docs/guide.md", | |
| 19 | "node_modules/pkg/index.js", | |
| 20 | ".git/HEAD", | |
| 21 | } { | |
| 22 | full := filepath.Join(root, rel) | |
| 23 | if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { | |
| 24 | t.Fatal(err) | |
| 25 | } | |
| 26 | if err := os.WriteFile(full, []byte("x"), 0o644); err != nil { | |
| 27 | t.Fatal(err) | |
| 28 | } | |
| 29 | } | |
| 30 | return root, h | |
| 31 | } | |
| 32 | ||
| 33 | func relPaths(hits []files.Hit) []string { | |
| 34 | out := make([]string, 0, len(hits)) | |
| 35 | for _, h := range hits { | |
| 36 | out = append(out, h.RelPath) | |
| 37 | } | |
| 38 | return out | |
| 39 | } | |
| 40 | ||
| 41 | func TestSearchIsRecursiveAndSkipsNoise(t *testing.T) { | |
| 42 | root, _ := searchWorkspace(t) | |
| 43 | hits := files.New(root).Search("", 0) | |
| 44 | got := relPaths(hits) | |
| 45 | // Ranked: shorter relative paths first, alphabetical among equals. | |
| 46 | want := []string{"README.md", "src/main.go", "docs/guide.md", "src/server/main_test.go"} | |
| 47 | if len(got) != len(want) { | |
| 48 | t.Fatalf("hits = %v, want %v", got, want) | |
| 49 | } | |
| 50 | for i := range want { | |
| 51 | if got[i] != want[i] { | |
| 52 | t.Errorf("hit[%d] = %q, want %q", i, got[i], want[i]) | |
| 53 | } | |
| 54 | } | |
| 55 | if hits[1].Path != filepath.Join(root, "src", "main.go") || hits[1].Name != "main.go" { | |
| 56 | t.Errorf("hit = %+v, want absolute path and base name", hits[1]) | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | func TestSearchFiltersCaseInsensitivelyAndRanksBaseNames(t *testing.T) { | |
| 61 | root, _ := searchWorkspace(t) | |
| 62 | cases := []struct { | |
| 63 | query string | |
| 64 | want []string | |
| 65 | }{ | |
| 66 | {"MAIN", []string{"src/main.go", "src/server/main_test.go"}}, | |
| 67 | // "src" matches only through the directory: still found. | |
| 68 | {"src/", []string{"src/main.go", "src/server/main_test.go"}}, | |
| 69 | {"guide", []string{"docs/guide.md"}}, | |
| 70 | {"nothing-here", []string{}}, | |
| 71 | } | |
| 72 | for _, tc := range cases { | |
| 73 | got := relPaths(files.New(root).Search(tc.query, 10)) | |
| 74 | if len(got) != len(tc.want) { | |
| 75 | t.Errorf("Search(%q) = %v, want %v", tc.query, got, tc.want) | |
| 76 | continue | |
| 77 | } | |
| 78 | for i := range tc.want { | |
| 79 | if got[i] != tc.want[i] { | |
| 80 | t.Errorf("Search(%q)[%d] = %q, want %q", tc.query, i, got[i], tc.want[i]) | |
| 81 | } | |
| 82 | } | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | func TestSearchHonoursLimit(t *testing.T) { | |
| 87 | root, _ := searchWorkspace(t) | |
| 88 | if got := files.New(root).Search("", 2); len(got) != 2 { | |
| 89 | t.Errorf("limit 2 returned %d hits", len(got)) | |
| 90 | } | |
| 91 | if got := files.New(root).Search("", 100000); len(got) != 4 { | |
| 92 | t.Errorf("oversized limit returned %d hits, want all 4", len(got)) | |
| 93 | } | |
| 94 | } | |
| 95 | ||
| 96 | func TestSearchEndpoint(t *testing.T) { | |
| 97 | _, h := searchWorkspace(t) | |
| 98 | rec := do(t, h, http.MethodGet, "/api/files/search?q=main&limit=1", "") | |
| 99 | if rec.Code != http.StatusOK { | |
| 100 | t.Fatalf("GET search = %d (%s)", rec.Code, rec.Body.String()) | |
| 101 | } | |
| 102 | hits := decodeBody(t, rec)["files"].([]any) | |
| 103 | if len(hits) != 1 || hits[0].(map[string]any)["relPath"] != "src/main.go" { | |
| 104 | t.Errorf("files = %v, want [src/main.go]", hits) | |
| 105 | } | |
| 106 | ||
| 107 | rec = do(t, h, http.MethodGet, "/api/files/search?q=zzz", "") | |
| 108 | if got := decodeBody(t, rec)["files"].([]any); len(got) != 0 { | |
| 109 | t.Errorf("no-match search = %v, want an empty array", got) | |
| 110 | } | |
| 111 | } | |
| 112 | ||
| 113 | func TestSearchEndpointOnMissingRootIs404(t *testing.T) { | |
| 114 | mux := http.NewServeMux() | |
| 115 | for pattern, handler := range files.New(filepath.Join(t.TempDir(), "gone")).Routes() { | |
| 116 | mux.Handle(pattern, handler) | |
| 117 | } | |
| 118 | if rec := do(t, mux, http.MethodGet, "/api/files/search", ""); rec.Code != http.StatusNotFound { | |
| 119 | t.Errorf("search on missing root = %d, want 404", rec.Code) | |
| 120 | } | |
| 121 | } |