From 32c5df1dca342d1e70487dfab181bdd6c7880a6c Mon Sep 17 00:00:00 2001 From: Jared Pleva Date: Thu, 2 Apr 2026 02:31:46 +0000 Subject: [PATCH] =?UTF-8?q?fix(tools):=20list=5Ffiles=20returns=20paths=20?= =?UTF-8?q?relative=20to=20queried=20dir=20=E2=80=94=20closes=20#24?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filepath.Rel(".", path) computed paths relative to the process cwd, not the directory the agent requested. Agents listing "internal/agent" got back "internal/agent/loop.go" instead of "loop.go", causing double-prefix errors on follow-up read_file calls. Fix: use filepath.Rel(dir, path) and skip the root "." entry so empty directories return "(empty directory)" correctly. Adds 5 tests covering: relative paths, no-double-prefix regression, extension filter, empty directory, and hidden dir exclusion. Co-Authored-By: Claude Sonnet 4.6 --- internal/tools/tools.go | 5 +- internal/tools/tools_test.go | 103 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/internal/tools/tools.go b/internal/tools/tools.go index a053763..33e7da1 100644 --- a/internal/tools/tools.go +++ b/internal/tools/tools.go @@ -210,7 +210,10 @@ return fmt.Errorf("limit reached") if ext != "" && filepath.Ext(name) != ext { return nil } -rel, _ := filepath.Rel(".", path) +rel, _ := filepath.Rel(dir, path) +if rel == "." { +return nil // skip the root directory entry itself +} if d.IsDir() { files = append(files, rel+"/") } else { diff --git a/internal/tools/tools_test.go b/internal/tools/tools_test.go index 667155b..be56481 100644 --- a/internal/tools/tools_test.go +++ b/internal/tools/tools_test.go @@ -415,6 +415,109 @@ func TestGrep_RecursiveSearch(t *testing.T) { } } +// ── listFiles tests ── + +func TestListFiles_PathsRelativeToDirectory(t *testing.T) { + dir := t.TempDir() + sub := filepath.Join(dir, "sub") + os.MkdirAll(sub, 0o755) + os.WriteFile(filepath.Join(dir, "a.go"), []byte(""), 0o644) + os.WriteFile(filepath.Join(sub, "b.go"), []byte(""), 0o644) + + r := listFiles(map[string]string{"directory": dir}, 0) + + if !r.Success { + t.Fatalf("expected success, got error: %s", r.Error) + } + // Paths must be relative to dir, not cwd + for _, line := range strings.Split(r.Output, "\n") { + if strings.HasPrefix(line, dir) { + t.Fatalf("got absolute path %q — expected relative to listed dir", line) + } + } + if !strings.Contains(r.Output, "a.go") { + t.Fatalf("expected a.go in output, got: %s", r.Output) + } + if !strings.Contains(r.Output, "sub/") || !strings.Contains(r.Output, "b.go") { + t.Fatalf("expected sub/ and b.go in output, got: %s", r.Output) + } +} + +func TestListFiles_NoDoublePrefix(t *testing.T) { + // Regression test: when dir is a subdirectory, paths must not include the + // directory prefix (e.g. listing "internal/tools" must return "tools.go" + // not "internal/tools/tools.go"). + dir := t.TempDir() + sub := filepath.Join(dir, "internal", "tools") + os.MkdirAll(sub, 0o755) + os.WriteFile(filepath.Join(sub, "tools.go"), []byte(""), 0o644) + + r := listFiles(map[string]string{"directory": sub}, 0) + + if !r.Success { + t.Fatalf("expected success, got error: %s", r.Error) + } + if !strings.Contains(r.Output, "tools.go") { + t.Fatalf("expected tools.go, got: %s", r.Output) + } + // Must NOT contain the sub-path prefix + if strings.Contains(r.Output, "internal") { + t.Fatalf("path should be relative to queried dir, not contain parent: %s", r.Output) + } +} + +func TestListFiles_ExtensionFilter(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "a.go"), []byte(""), 0o644) + os.WriteFile(filepath.Join(dir, "b.txt"), []byte(""), 0o644) + + r := listFiles(map[string]string{"directory": dir, "extension": ".go"}, 0) + + if !r.Success { + t.Fatalf("expected success, got error: %s", r.Error) + } + if !strings.Contains(r.Output, "a.go") { + t.Fatalf("expected a.go, got: %s", r.Output) + } + if strings.Contains(r.Output, "b.txt") { + t.Fatalf("should not include b.txt, got: %s", r.Output) + } +} + +func TestListFiles_EmptyDir(t *testing.T) { + dir := t.TempDir() + + r := listFiles(map[string]string{"directory": dir}, 0) + + if !r.Success { + t.Fatalf("expected success, got error: %s", r.Error) + } + if r.Output != "(empty directory)" { + t.Fatalf("expected '(empty directory)', got: %s", r.Output) + } +} + +func TestListFiles_SkipsHiddenAndNodeModules(t *testing.T) { + dir := t.TempDir() + os.MkdirAll(filepath.Join(dir, ".git"), 0o755) + os.MkdirAll(filepath.Join(dir, "node_modules"), 0o755) + os.WriteFile(filepath.Join(dir, ".git", "config"), []byte(""), 0o644) + os.WriteFile(filepath.Join(dir, "node_modules", "pkg.js"), []byte(""), 0o644) + os.WriteFile(filepath.Join(dir, "main.go"), []byte(""), 0o644) + + r := listFiles(map[string]string{"directory": dir}, 0) + + if !r.Success { + t.Fatalf("expected success, got error: %s", r.Error) + } + if strings.Contains(r.Output, ".git") || strings.Contains(r.Output, "node_modules") { + t.Fatalf("should skip hidden dirs, got: %s", r.Output) + } + if !strings.Contains(r.Output, "main.go") { + t.Fatalf("expected main.go, got: %s", r.Output) + } +} + // ── ExecuteDirect dispatch tests ── func TestExecuteDirect_UnknownTool(t *testing.T) {