1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
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)
}
}
|