diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index b40c34de..204f1c5d 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -5,7 +5,6 @@ * @-mention autocomplete suggestions to the interactive editor. */ -import nodePath from "node:path"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { type AutocompleteItem, @@ -404,15 +403,14 @@ export default function fffExtension(pi: ExtensionAPI) { ): Promise<{ finder: FileFinderApi; query: string; root: string } | null> { const route = routePathConstraint(pathParam, activeCwd); if (!route) return null; - const aux = await auxPool.acquire(route.root); - // A broader covering picker may have been reused; rebase the suffix so the - // constraint stays relative to the picker's actual root. - const rebase = nodePath - .relative(aux.root, route.root) - .replaceAll(nodePath.sep, "/"); - const suffix = [rebase, route.suffix].filter(Boolean).join("/"); - const query = buildQuery(suffix || undefined, pattern, exclude, aux.root); - return { finder: aux.finder, query, root: aux.root }; + const aux = await auxPool.acquire(route.root, { exact: true }); + const query = buildQuery( + route.suffix || undefined, + pattern, + exclude, + route.root, + ); + return { finder: aux.finder, query, root: route.root }; } async function getMentionItems( diff --git a/packages/pi-fff/test/extension.test.ts b/packages/pi-fff/test/extension.test.ts index c0d351c6..c37cebfa 100644 --- a/packages/pi-fff/test/extension.test.ts +++ b/packages/pi-fff/test/extension.test.ts @@ -1,17 +1,22 @@ import { beforeEach, describe, expect, mock, test } from "bun:test"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; type MockFinder = { isDestroyed: boolean; waitForScan: ReturnType; mixedSearch: ReturnType; + grep: ReturnType; destroy: ReturnType; }; const createCalls: unknown[] = []; let finders: MockFinder[] = []; let mixedSearchImpl: ((query: string, options: unknown) => unknown) | undefined; +let grepMatchRoot: string | undefined; -function createMockFinder(): MockFinder { +function createMockFinder(basePath: string): MockFinder { return { isDestroyed: false, waitForScan: mock(async () => undefined), @@ -28,6 +33,20 @@ function createMockFinder(): MockFinder { }, }; }), + grep: mock(() => { + const items = + basePath === grepMatchRoot + ? [ + { + relativePath: "target.ts", + fileName: "target.ts", + lineContent: "issue711Token", + lineNumber: 1, + }, + ] + : []; + return { ok: true, value: { items } }; + }), destroy: mock(function (this: MockFinder) { this.isDestroyed = true; }), @@ -38,7 +57,7 @@ const finderModule = { FileFinder: { create: mock((options: unknown) => { createCalls.push(options); - const finder = createMockFinder(); + const finder = createMockFinder((options as { basePath: string }).basePath); finders.push(finder); return { ok: true, value: finder }; }), @@ -85,6 +104,7 @@ type EventHandler = (...args: any[]) => unknown; function createPi(mode?: string) { const events = new Map(); const commands = new Map(); + const tools = new Map(); const pi = { getFlag: mock((name: string) => (name === "fff-mode" ? mode : undefined)), @@ -95,11 +115,13 @@ function createPi(mode?: string) { commands.set(name, command); }), registerFlag: mock(() => undefined), - registerTool: mock(() => undefined), + registerTool: mock((tool: any) => { + tools.set(tool.name, tool); + }), appendEntry: mock(() => undefined), }; - return { pi, events, commands }; + return { pi, events, commands, tools }; } function createContext() { @@ -143,6 +165,7 @@ beforeEach(() => { createCalls.length = 0; finders = []; mixedSearchImpl = undefined; + grepMatchRoot = undefined; delete process.env.PI_FFF_MODE; }); @@ -322,3 +345,32 @@ describe("pi-fff autocomplete registration", () => { expect(current.shouldTriggerFileCompletion).toHaveBeenCalledTimes(1); }); }); + +describe("pi-fff external paths", () => { + test("uses an exact finder for an explicit external file", async () => { + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "fff-711-")); + const broadRoot = path.join(fixture, ".pi"); + const targetDir = path.join(broadRoot, "node_modules", "pkg"); + const targetPath = path.join(targetDir, "target.ts"); + fs.mkdirSync(targetDir, { recursive: true }); + fs.writeFileSync(targetPath, "issue711Token\n"); + + try { + grepMatchRoot = targetDir; + const { tools } = await start(); + const grep = tools.get("ffgrep"); + await grep.execute("warmup", { pattern: "warmup", path: broadRoot }); + const result = await grep.execute("repro", { + pattern: "issue711Token", + path: targetPath, + }); + + expect(result.content[0].text).toContain("target.ts"); + expect( + createCalls.map((call) => (call as { basePath: string }).basePath), + ).toContain(targetDir); + } finally { + fs.rmSync(fixture, { recursive: true, force: true }); + } + }); +});