From c5451af749126387f1d677baad8d7f3a09199411 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 16 Apr 2026 22:53:45 +0200 Subject: [PATCH] fix(coding-agent): make find tool match path-based glob patterns fd --glob matches against the basename unless --full-path is set, so patterns containing '/' (e.g. 'src/**/*.spec.ts') silently returned no results. When the pattern contains '/', switch fd into --full-path mode and prepend '**/' unless the pattern already starts with '/', '**/', or is '**'. Basename patterns keep the default matcher. closes #3302 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/tools/find.ts | 13 +++- .../regressions/3302-find-path-glob.test.ts | 72 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index eea5c9e3..60183d2f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed `--verbose` startup output to begin with expanded startup help and loaded resource listings after the compact startup header change ([#3147](https://github.com/badlogic/pi-mono/issues/3147)) +- Fixed `find` tool returning no results for path-based glob patterns such as `src/**/*.spec.ts` or `some/parent/child/**` by switching fd into full-path mode and normalizing the pattern when it contains a `/` ([#3302](https://github.com/badlogic/pi-mono/issues/3302)) ## [0.67.5] - 2026-04-16 diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index 9e9e95b2..92afc275 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -258,7 +258,18 @@ export function createFindToolDefinition( return; } for (const gitignorePath of gitignoreFiles) args.push("--ignore-file", gitignorePath); - args.push(pattern, searchPath); + + // fd --glob matches against the basename unless --full-path is set; in --full-path + // mode it matches against the absolute candidate path, so a path-containing + // pattern like 'src/**/*.spec.ts' needs a leading '**/' to match anything. + let effectivePattern = pattern; + if (pattern.includes("/")) { + args.push("--full-path"); + if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") { + effectivePattern = `**/${pattern}`; + } + } + args.push(effectivePattern, searchPath); const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"] }); const rl = createInterface({ input: child.stdout }); diff --git a/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts b/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts new file mode 100644 index 00000000..0f19cb14 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts @@ -0,0 +1,72 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { createFindToolDefinition } from "../../../src/core/tools/find.js"; + +/** + * Regression test for https://github.com/badlogic/pi-mono/issues/3302 + * + * The `find` tool advertises glob patterns like `src/**\/*.spec.ts`, but the + * default fd-backed implementation used `fd --glob ` without + * `--full-path`, which makes fd match only against the basename. Any pattern + * containing a `/` therefore silently returned no matches. + * + * The fix switches fd into full-path mode when the pattern contains a `/` + * and prepends `**\/` so the pattern can match against the absolute candidate + * path that fd feeds to the matcher. + */ +describe("issue #3302 find returns no results for path-based glob patterns", () => { + let tempRoot: string; + + beforeEach(() => { + tempRoot = mkdtempSync(join(tmpdir(), "pi-3302-")); + mkdirSync(join(tempRoot, "some", "parent", "child"), { recursive: true }); + mkdirSync(join(tempRoot, "src", "foo", "bar"), { recursive: true }); + writeFileSync(join(tempRoot, "some", "parent", "child", "file.ext"), ""); + writeFileSync(join(tempRoot, "some", "parent", "child", "test.spec.ts"), ""); + writeFileSync(join(tempRoot, "src", "foo", "bar", "example.spec.ts"), ""); + }); + + afterEach(() => { + rmSync(tempRoot, { recursive: true, force: true }); + }); + + async function runFind(pattern: string): Promise { + const def = createFindToolDefinition(tempRoot); + // The find tool implementation does not touch ctx; pass a minimal stub. + const ctx = {} as Parameters[4]; + const result = (await def.execute("call-1", { pattern }, undefined, undefined, ctx)) as { + content: Array<{ type: string; text?: string }>; + }; + const text = result.content[0]?.text ?? ""; + if (text === "No files found matching pattern") return []; + return text + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith("[")); + } + + it("basename pattern still matches (regression-safe)", async () => { + const files = await runFind("*.spec.ts"); + expect(files.sort()).toEqual(["some/parent/child/test.spec.ts", "src/foo/bar/example.spec.ts"]); + }); + + it("directory-prefixed pattern with ** tail matches subtree", async () => { + const files = await runFind("some/parent/child/**"); + // Matches files (and possibly directories) under the subtree. Assert the two files are present. + expect(files).toContain("some/parent/child/file.ext"); + expect(files).toContain("some/parent/child/test.spec.ts"); + }); + + it("leading ** wildcard with path segments matches", async () => { + const files = await runFind("**/parent/child/*"); + expect(files.sort()).toContain("some/parent/child/file.ext"); + expect(files.sort()).toContain("some/parent/child/test.spec.ts"); + }); + + it("src/**/*.spec.ts matches nested spec file", async () => { + const files = await runFind("src/**/*.spec.ts"); + expect(files).toEqual(["src/foo/bar/example.spec.ts"]); + }); +});