v0.3.62 8233420

glob_test.go RawHistoryFolder

1package tools
2
3import (
4	"context"
5	"os"
6	"path/filepath"
7	"strings"
8	"testing"
9
10	"github.com/patriceckhart/zot/packages/provider"
11)
12
13func TestGlobBasic(t *testing.T) {
14	dir := t.TempDir()
15	files := []string{
16		"main.go",
17		"README.md",
18		"pkg/util.go",
19		"pkg/util_test.go",
20		"pkg/sub/deep.go",
21		"docs/index.html",
22	}
23	for _, f := range files {
24		p := filepath.Join(dir, f)
25		if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
26			t.Fatal(err)
27		}
28		if err := os.WriteFile(p, []byte("content"), 0o644); err != nil {
29			t.Fatal(err)
30		}
31	}
32
33	tool := &GlobTool{CWD: dir}
34
35	// 1. Simple extension match across tree (no slash in pattern)
36	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.go"}), nil)
37	if err != nil {
38		t.Fatalf("execute error: %v", err)
39	}
40	text := res.Content[0].(provider.TextBlock).Text
41	for _, want := range []string{"main.go", "pkg/util.go", "pkg/util_test.go", "pkg/sub/deep.go"} {
42		if !strings.Contains(text, want) {
43			t.Errorf("expected %q in result, got:\n%s", want, text)
44		}
45	}
46	if strings.Contains(text, "README.md") || strings.Contains(text, "docs/index.html") {
47		t.Errorf("unexpected file in result:\n%s", text)
48	}
49
50	// 2. Globstar match with path prefix
51	res, err = tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "pkg/**/*.go"}), nil)
52	if err != nil {
53		t.Fatalf("execute error: %v", err)
54	}
55	text = res.Content[0].(provider.TextBlock).Text
56	if strings.Contains(text, "main.go") {
57		t.Errorf("did not expect main.go in pkg/**/*.go, got:\n%s", text)
58	}
59	for _, want := range []string{"pkg/util.go", "pkg/util_test.go", "pkg/sub/deep.go"} {
60		if !strings.Contains(text, want) {
61			t.Errorf("expected %q in result, got:\n%s", want, text)
62		}
63	}
64
65	// 3. Specific path segment
66	res, err = tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "pkg/*.go"}), nil)
67	if err != nil {
68		t.Fatalf("execute error: %v", err)
69	}
70	text = res.Content[0].(provider.TextBlock).Text
71	if strings.Contains(text, "pkg/sub/deep.go") {
72		t.Errorf("did not expect deep.go in single-star pkg/*.go, got:\n%s", text)
73	}
74	if !strings.Contains(text, "pkg/util.go") || !strings.Contains(text, "pkg/util_test.go") {
75		t.Errorf("missing pkg/util.go in pkg/*.go, got:\n%s", text)
76	}
77}
78
79func TestGlobBraceExpansion(t *testing.T) {
80	dir := t.TempDir()
81	files := []string{
82		"a.go",
83		"b.json",
84		"c.ts",
85		"d.txt",
86	}
87	for _, f := range files {
88		p := filepath.Join(dir, f)
89		os.WriteFile(p, []byte("content"), 0o644)
90	}
91
92	tool := &GlobTool{CWD: dir}
93	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.{go,json}"}), nil)
94	if err != nil {
95		t.Fatalf("execute error: %v", err)
96	}
97	text := res.Content[0].(provider.TextBlock).Text
98	if !strings.Contains(text, "a.go") || !strings.Contains(text, "b.json") {
99		t.Errorf("expected a.go and b.json, got:\n%s", text)
100	}
101	if strings.Contains(text, "c.ts") || strings.Contains(text, "d.txt") {
102		t.Errorf("unexpected files in result:\n%s", text)
103	}
104}
105
106func TestGlobGitignore(t *testing.T) {
107	dir := t.TempDir()
108	if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("ignored/\n*.log\n"), 0o644); err != nil {
109		t.Fatal(err)
110	}
111
112	files := []string{
113		"app.go",
114		"error.log",
115		"ignored/secret.go",
116		"sub/normal.go",
117		"sub/test.log",
118	}
119	for _, f := range files {
120		p := filepath.Join(dir, f)
121		os.MkdirAll(filepath.Dir(p), 0o755)
122		os.WriteFile(p, []byte("content"), 0o644)
123	}
124
125	tool := &GlobTool{CWD: dir}
126	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "**/*"}), nil)
127	if err != nil {
128		t.Fatalf("execute error: %v", err)
129	}
130	text := res.Content[0].(provider.TextBlock).Text
131	if strings.Contains(text, "secret.go") || strings.Contains(text, "error.log") || strings.Contains(text, "test.log") {
132		t.Errorf("result contains gitignored files:\n%s", text)
133	}
134	if !strings.Contains(text, "app.go") || !strings.Contains(text, "sub/normal.go") {
135		t.Errorf("missing tracked files in:\n%s", text)
136	}
137}
138
139func TestGlobNestedGitignore(t *testing.T) {
140	dir := t.TempDir()
141	nested := filepath.Join(dir, "vendor")
142	os.MkdirAll(nested, 0o755)
143	os.WriteFile(filepath.Join(nested, ".gitignore"), []byte("cached/\n"), 0o644)
144
145	files := []string{
146		"root.go",
147		"vendor/keep.go",
148		"vendor/cached/temp.go",
149	}
150	for _, f := range files {
151		p := filepath.Join(dir, f)
152		os.MkdirAll(filepath.Dir(p), 0o755)
153		os.WriteFile(p, []byte("content"), 0o644)
154	}
155
156	tool := &GlobTool{CWD: dir}
157	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "**/*.go"}), nil)
158	if err != nil {
159		t.Fatal(err)
160	}
161	text := res.Content[0].(provider.TextBlock).Text
162	if strings.Contains(text, "temp.go") {
163		t.Errorf("nested gitignore rule was not respected: %s", text)
164	}
165	if !strings.Contains(text, "root.go") || !strings.Contains(text, "vendor/keep.go") {
166		t.Errorf("expected files missing: %s", text)
167	}
168}
169
170func TestGlobHiddenFiles(t *testing.T) {
171	dir := t.TempDir()
172	files := []string{
173		"visible.txt",
174		".hidden.txt",
175		".config/settings.json",
176	}
177	for _, f := range files {
178		p := filepath.Join(dir, f)
179		os.MkdirAll(filepath.Dir(p), 0o755)
180		os.WriteFile(p, []byte("content"), 0o644)
181	}
182
183	tool := &GlobTool{CWD: dir}
184
185	// Default: skip hidden
186	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.*"}), nil)
187	if err != nil {
188		t.Fatal(err)
189	}
190	text := res.Content[0].(provider.TextBlock).Text
191	if strings.Contains(text, ".hidden.txt") || strings.Contains(text, "settings.json") {
192		t.Errorf("hidden files should be skipped by default: %s", text)
193	}
194	if !strings.Contains(text, "visible.txt") {
195		t.Errorf("visible.txt missing: %s", text)
196	}
197
198	// Hidden: true
199	res, err = tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "**/*", "hidden": true}), nil)
200	if err != nil {
201		t.Fatal(err)
202	}
203	text = res.Content[0].(provider.TextBlock).Text
204	if !strings.Contains(text, ".hidden.txt") || !strings.Contains(text, ".config/settings.json") {
205		t.Errorf("hidden files missing when hidden=true: %s", text)
206	}
207}
208
209func TestGlobSubdirectoryPath(t *testing.T) {
210	dir := t.TempDir()
211	files := []string{
212		"root.txt",
213		"sub/a.txt",
214		"sub/b.txt",
215		"other/c.txt",
216	}
217	for _, f := range files {
218		p := filepath.Join(dir, f)
219		os.MkdirAll(filepath.Dir(p), 0o755)
220		os.WriteFile(p, []byte("content"), 0o644)
221	}
222
223	tool := &GlobTool{CWD: dir}
224	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.txt", "path": "sub"}), nil)
225	if err != nil {
226		t.Fatal(err)
227	}
228	text := res.Content[0].(provider.TextBlock).Text
229	if strings.Contains(text, "root.txt") || strings.Contains(text, "other/c.txt") {
230		t.Errorf("result should only contain files from sub: %s", text)
231	}
232	if !strings.Contains(text, "sub/a.txt") || !strings.Contains(text, "sub/b.txt") {
233		t.Errorf("sub files missing: %s", text)
234	}
235}
236
237func TestGlobSubdirectoryHonorsParentGitignore(t *testing.T) {
238	dir := t.TempDir()
239	if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("sub/ignored.go\n"), 0o644); err != nil {
240		t.Fatal(err)
241	}
242	if err := os.Mkdir(filepath.Join(dir, "sub"), 0o755); err != nil {
243		t.Fatal(err)
244	}
245	for _, name := range []string{"ignored.go", "kept.go"} {
246		if err := os.WriteFile(filepath.Join(dir, "sub", name), []byte("content"), 0o644); err != nil {
247			t.Fatal(err)
248		}
249	}
250
251	tool := &GlobTool{CWD: dir}
252	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.go", "path": "sub"}), nil)
253	if err != nil {
254		t.Fatal(err)
255	}
256	text := res.Content[0].(provider.TextBlock).Text
257	if strings.Contains(text, "ignored.go") {
258		t.Fatalf("parent .gitignore rule was not respected: %s", text)
259	}
260	if !strings.Contains(text, "sub/kept.go") {
261		t.Fatalf("expected kept file in result: %s", text)
262	}
263}
264
265func TestGlobSymlinkSearchRoot(t *testing.T) {
266	dir := t.TempDir()
267	target := filepath.Join(dir, "target")
268	if err := os.Mkdir(target, 0o755); err != nil {
269		t.Fatal(err)
270	}
271	if err := os.WriteFile(filepath.Join(target, "found.go"), []byte("content"), 0o644); err != nil {
272		t.Fatal(err)
273	}
274	if err := os.Symlink(target, filepath.Join(dir, "link")); err != nil {
275		t.Skipf("symlinks unavailable: %v", err)
276	}
277
278	tool := &GlobTool{CWD: dir}
279	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.go", "path": "link"}), nil)
280	if err != nil {
281		t.Fatal(err)
282	}
283	if text := res.Content[0].(provider.TextBlock).Text; text != "link/found.go" {
284		t.Fatalf("unexpected symlink search result: %q", text)
285	}
286}
287
288func TestGlobNoMatches(t *testing.T) {
289	dir := t.TempDir()
290	os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0o644)
291
292	tool := &GlobTool{CWD: dir}
293	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.rs"}), nil)
294	if err != nil {
295		t.Fatal(err)
296	}
297	text := res.Content[0].(provider.TextBlock).Text
298	if text != "No files matched the pattern." {
299		t.Fatalf("expected 'No files matched the pattern.', got: %q", text)
300	}
301	details := res.Details.(map[string]any)
302	if details["matches"] != 0 {
303		t.Fatalf("matches detail want 0, got %v", details["matches"])
304	}
305}
306
307func TestGlobValidationErrors(t *testing.T) {
308	dir := t.TempDir()
309	tool := &GlobTool{CWD: dir}
310
311	// Empty pattern
312	if _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": ""}), nil); err == nil {
313		t.Fatal("expected error for empty pattern")
314	}
315
316	// Non-existent directory
317	if _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.go", "path": "nonexistent"}), nil); err == nil {
318		t.Fatal("expected error for non-existent path")
319	}
320
321	// File instead of directory
322	filePath := filepath.Join(dir, "file.txt")
323	os.WriteFile(filePath, []byte("text"), 0o644)
324	if _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.txt", "path": "file.txt"}), nil); err == nil {
325		t.Fatal("expected error for file passed as path")
326	}
327}
328
329func TestGlobSandboxing(t *testing.T) {
330	dir := t.TempDir()
331	sandboxDir := filepath.Join(dir, "sandbox")
332	outsideDir := filepath.Join(dir, "outside")
333	os.MkdirAll(sandboxDir, 0o755)
334	os.MkdirAll(outsideDir, 0o755)
335
336	s := NewSandbox(sandboxDir)
337	s.Lock()
338
339	tool := &GlobTool{CWD: sandboxDir, Sandbox: s}
340
341	// Inside sandbox should succeed
342	res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.go"}), nil)
343	if err != nil {
344		t.Fatalf("expected success inside sandbox, got %v", err)
345	}
346	if res.Content[0].(provider.TextBlock).Text != "No files matched the pattern." {
347		t.Fatalf("unexpected content: %v", res.Content[0])
348	}
349
350	// Absolute paths inside the sandbox should not leak the sandbox root.
351	insideFile := filepath.Join(sandboxDir, "inside.go")
352	if err := os.WriteFile(insideFile, []byte("content"), 0o644); err != nil {
353		t.Fatal(err)
354	}
355	res, err = tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.go", "path": sandboxDir}), nil)
356	if err != nil {
357		t.Fatalf("expected absolute path inside sandbox to succeed, got %v", err)
358	}
359	if text := res.Content[0].(provider.TextBlock).Text; text != "inside.go" {
360		t.Fatalf("jailed result should be relative, got %q", text)
361	}
362
363	// Outside sandbox should fail.
364	if _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"pattern": "*.go", "path": outsideDir}), nil); err == nil {
365		t.Fatal("expected sandboxing error for path outside sandbox")
366	}
367}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close