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"]); }); });