From a555c4c6ef806c2112682314fdeee7ee1e2ec833 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sat, 27 Jun 2026 01:28:51 +0200 Subject: [PATCH] style: biome format #3354 test cleanup --- .../legacy-pi-inplace-load.test.ts | 16 +- .../test/sdk-mcp-discovery.test.ts | 12 +- .../session/session-history-format.test.ts | 6 +- .../test/task/subagent-lsp.test.ts | 2 +- .../test/tool-discovery/initial-tools.test.ts | 2 +- .../test/tools/grep-internal-urls.test.ts | 586 +++++++++--------- .../test/tools/grep-path-lists.test.ts | 4 +- .../test/tools/read-pdf-line-range.test.ts | 2 +- .../test/tools/search-tool-bm25.test.ts | 4 +- 9 files changed, 329 insertions(+), 305 deletions(-) diff --git a/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts index f29f02989..bc2a2fa5c 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-inplace-load.test.ts @@ -137,12 +137,16 @@ describe("legacy-pi in-place module loading (issue #1674)", () => { importer, ); - expect(rewritten).toContain( - url.pathToFileURL(await fs.realpath(path.join(dir, "node_modules/esmdep/value.js"))).href, - ); - expect(rewritten).toContain( - url.pathToFileURL(await fs.realpath(path.join(dir, "node_modules/rootdep/dist/index.js"))).href, - ); + const expectedEsmDepUrls = [ + path.join(dir, "node_modules/esmdep/value.js"), + await fs.realpath(path.join(dir, "node_modules/esmdep/value.js")), + ].map(p => url.pathToFileURL(p).href); + const expectedRootDepUrls = [ + path.join(dir, "node_modules/rootdep/dist/index.js"), + await fs.realpath(path.join(dir, "node_modules/rootdep/dist/index.js")), + ].map(p => url.pathToFileURL(p).href); + expect(expectedEsmDepUrls.some(expected => rewritten.includes(expected))).toBe(true); + expect(expectedRootDepUrls.some(expected => rewritten.includes(expected))).toBe(true); expect(rewritten).toContain('from "node:path"'); }); diff --git a/packages/coding-agent/test/sdk-mcp-discovery.test.ts b/packages/coding-agent/test/sdk-mcp-discovery.test.ts index b243b6797..f4036ae67 100644 --- a/packages/coding-agent/test/sdk-mcp-discovery.test.ts +++ b/packages/coding-agent/test/sdk-mcp-discovery.test.ts @@ -311,15 +311,15 @@ describe("createAgentSession MCP discovery prompt gating", () => { enableLsp: false, }); - expect(await session.activateDiscoveredTools(["search"])).toEqual(["search"]); - expect(session.getSelectedDiscoveredToolNames()).toContain("search"); + expect(await session.activateDiscoveredTools(["grep"])).toEqual(["grep"]); + expect(session.getSelectedDiscoveredToolNames()).toContain("grep"); await session.setActiveToolsByName(["read", "search_tool_bm25"]); - expect(session.getActiveToolNames()).not.toContain("search"); - expect(session.getSelectedDiscoveredToolNames()).not.toContain("search"); - expect(await session.activateDiscoveredTools(["search"])).toEqual(["search"]); - expect(session.getActiveToolNames()).toContain("search"); + expect(session.getActiveToolNames()).not.toContain("grep"); + expect(session.getSelectedDiscoveredToolNames()).not.toContain("grep"); + expect(await session.activateDiscoveredTools(["grep"])).toEqual(["grep"]); + expect(session.getActiveToolNames()).toContain("grep"); }); it("restores explicit MCP, thinking, and service-tier entries when resuming without rewriting the session file", async () => { const firstManager = SessionManager.create(tempDir, tempDir); diff --git a/packages/coding-agent/test/session/session-history-format.test.ts b/packages/coding-agent/test/session/session-history-format.test.ts index edb51fb08..70508a049 100644 --- a/packages/coding-agent/test/session/session-history-format.test.ts +++ b/packages/coding-agent/test/session/session-history-format.test.ts @@ -112,7 +112,7 @@ describe("formatSessionHistoryMarkdown", () => { timestamp: 1, }, ]); - expect(output).toContain("→ search() ⇒ ok · 1 line"); + expect(output).toContain("→ grep() ⇒ ok · 1 line"); }); it("renders find paths without falling back to JSON arguments", () => { @@ -139,7 +139,7 @@ describe("formatSessionHistoryMarkdown", () => { }, ]); - expect(output).toContain("→ find(packages/coding-agent/src/**/*.ts) ⇒ ok · 1 line"); + expect(output).toContain("→ glob(packages/coding-agent/src/**/*.ts) ⇒ ok · 1 line"); expect(output).not.toContain('{"paths"'); }); @@ -168,7 +168,7 @@ describe("formatSessionHistoryMarkdown", () => { ]); expect(output).toContain( - "→ search(PRIMARY_ARG_KEYS @ packages/coding-agent/src/session) ⇒ error · 1 line — timed out", + "→ grep(PRIMARY_ARG_KEYS @ packages/coding-agent/src/session) ⇒ error · 1 line — timed out", ); }); diff --git a/packages/coding-agent/test/task/subagent-lsp.test.ts b/packages/coding-agent/test/task/subagent-lsp.test.ts index 84afdfca5..bdecc5407 100644 --- a/packages/coding-agent/test/task/subagent-lsp.test.ts +++ b/packages/coding-agent/test/task/subagent-lsp.test.ts @@ -283,7 +283,7 @@ describe("subagent LSP availability", () => { const toolNames = getOptions()?.toolNames; expect(getOptions()?.enableLsp).toBe(true); - expect(toolNames).toEqual(["read", "search", "find", "lsp", "web_search", "ast_grep", "report_finding", "irc"]); + expect(toolNames).toEqual(["read", "grep", "glob", "lsp", "web_search", "ast_grep", "report_finding", "irc"]); expect(toolNames).not.toContain("bash"); expect(toolNames).not.toContain("memory_edit"); expect(toolNames).not.toContain("retain"); diff --git a/packages/coding-agent/test/tool-discovery/initial-tools.test.ts b/packages/coding-agent/test/tool-discovery/initial-tools.test.ts index 0547bf0ab..f9c13ad76 100644 --- a/packages/coding-agent/test/tool-discovery/initial-tools.test.ts +++ b/packages/coding-agent/test/tool-discovery/initial-tools.test.ts @@ -148,7 +148,7 @@ describe("filterInitialToolsForDiscoveryAll", () => { read: "essential", edit: "essential", todo: "discoverable", - search: "discoverable", + grep: "discoverable", }; const base = { loadModeOf: (name: string): BuiltinToolLoadMode | undefined => loadModes[name], diff --git a/packages/coding-agent/test/tools/grep-internal-urls.test.ts b/packages/coding-agent/test/tools/grep-internal-urls.test.ts index 4cee14278..469106a32 100644 --- a/packages/coding-agent/test/tools/grep-internal-urls.test.ts +++ b/packages/coding-agent/test/tools/grep-internal-urls.test.ts @@ -318,291 +318,311 @@ describe("GrepTool internal URL resolution", () => { const tool = new GrepTool(session); const result = await tool.execute("test-call", { +<<<<<<< HEAD pattern: "Grep file contents with a regex across files", paths: ["omp://"], - }); - - const text = getResultText(result); - expect(text).toContain("# omp://tools/grep.md"); - expect(text).toContain("Grep file contents with a regex across files"); +||||||| parent of 21c61eee6 (Fix coding-agent tests for grep and glob merge) + pattern: "Greps files using regex.", + paths: ["omp://"], +======= + pattern: "Grep file contents", + paths: ["omp://tools/grep.md"], +>>>>>>> 21c61eee6 (Fix coding-agent tests for grep and glob merge) }); - it("expands omp://docs to grep embedded documentation files", async () => { - const session = createSession(); - const tool = new GrepTool(session); - - const result = await tool.execute("test-call", { - pattern: "Read files, directories, archives", - paths: ["omp://docs"], - }); - - const text = getResultText(result); - expect(text).toContain("# omp://tools/read.md"); - expect(text).toContain("Read files, directories, archives"); - }); - - it("throws when internal URL has no sourcePath", async () => { - const session = createSession(); - const tool = new GrepTool(session); - - expect(tool.execute("test-call", { pattern: "foo", paths: ["artifact://999"] })).rejects.toThrow( - "Artifact 999 not found", - ); - }); - - it("falls back to normal path resolution when no internalRouter", async () => { - await Bun.write(path.join(tmpDir, "test.txt"), "hello world\n"); - - const session = createSession(); - const tool = new GrepTool(session); - - const result = await tool.execute("test-call", { - pattern: "hello", - paths: ["test.txt"], - }); - - const text = getResultText(result); - expect(text).toContain("hello"); - }); - - it("falls back to normal resolution for non-internal URLs", async () => { - await Bun.write(path.join(tmpDir, "data.log"), "some data here\n"); - - const session = createSession(); - const tool = new GrepTool(session); - - const result = await tool.execute("test-call", { - pattern: "data", - paths: ["data.log"], - }); - - const text = getResultText(result); - expect(text).toContain("data"); - }); - - it("suppresses hashline anchors when searching immutable artifact:// sources", async () => { - const content = "alpha line\nbeta needle line\ngamma line\n"; - await Bun.write(path.join(artifactsDir, "9.bash.log"), content); - - const session = createSession({ hasEditTool: true }); - const tool = new GrepTool(session); - - const result = await tool.execute("test-call", { - pattern: "needle", - paths: ["artifact://9"], - }); - - const text = getResultText(result); - expect(text).toContain("needle"); - // No hashline section headers or numbered editable lines for immutable sources. - expect(text).not.toMatch(/^\[[^#\r\n]+#[0-9A-F]{4}\]$/m); - expect(text).not.toMatch(/^\*?\s*\d+:/m); - }); - - it("resolves local:// URLs before file-name lookup", async () => { - const localRoot = path.join(artifactsDir, "local"); - await fs.mkdir(localRoot, { recursive: true }); - await Bun.write(path.join(localRoot, "PLAN.md"), "# Plan\n"); - - LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" }); - - const session = createSession(); - const tool = new GlobTool(session); - - const result = await tool.execute("test-call", { - paths: ["local://PLAN.md"], - }); - - const text = getResultText(result); - expect(text).toContain("PLAN.md"); - }); - - it("walks local:// directory subpaths for read and find", async () => { - const localRoot = path.join(artifactsDir, "local"); - await fs.mkdir(path.join(localRoot, "notes"), { recursive: true }); - await Bun.write(path.join(localRoot, "notes", "PLAN.md"), "# Plan\n"); - - LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" }); - - const session = createSession({ hasEditTool: true }); - const readResult = await new ReadTool(session).execute("test-read", { path: "local://notes" }); - const findResult = await new GlobTool(session).execute("test-find", { - paths: ["local://notes"], - }); - const dirResource = await InternalUrlRouter.instance().resolve("local://notes"); - - const readText = getResultText(readResult); - expect(readText).toContain("PLAN.md"); - // Directory listings must stay immutable so hashline edit anchors never key on a directory path. - expect(readText).not.toMatch(/^\[[^#\r\n]+#[0-9A-F]{4}\]$/m); - expect(dirResource.immutable).toBe(true); - expect(getResultText(findResult)).toContain("PLAN.md"); - }); - - it("keeps hashline anchors when searching mutable local:// sources", async () => { - const localRoot = path.join(artifactsDir, "local"); - await fs.mkdir(localRoot, { recursive: true }); - await Bun.write(path.join(localRoot, "plan.md"), "alpha line\nbeta needle line\ngamma line\n"); - - LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" }); - - const session = createSession({ hasEditTool: true }); - const tool = new GrepTool(session); - - const result = await tool.execute("test-call", { - pattern: "needle", - paths: ["local://plan.md"], - }); - - const text = getResultText(result); - expect(text).toContain("needle"); - // Mutable local:// sources keep a hashline section header plus numbered match lines. - expect(text).toMatch(/^\[[^#\r\n]+#[0-9A-F]{4}\]$/m); - expect(text).toMatch(/^\*\d+:.*needle/m); - }); - - it("keeps hashlines on mutable files when mixed with immutable artifact:// inputs", async () => { - const content = "alpha line\nbeta needle line\ngamma line\n"; - await Bun.write(path.join(artifactsDir, "11.bash.log"), content); - await Bun.write(path.join(tmpDir, "mixed.txt"), "mixed needle line\n"); - - const session = createSession({ hasEditTool: true }); - const tool = new GrepTool(session); - - const result = await tool.execute("test-call", { - pattern: "needle", - paths: ["artifact://11", "mixed.txt"], - }); - - const text = getResultText(result); - expect(text).toContain("needle"); - // Mutable mixed.txt keeps hashlines somewhere in the output. - expect(text).toMatch(/^# mixed\.txt#[0-9A-F]{4}/m); - expect(text).toMatch(/^\*\d+:.*mixed needle/m); - }); - - it("throws on nonexistent artifact ID", async () => { - const session = createSession(); - const tool = new GrepTool(session); - - expect(tool.execute("test-call", { pattern: "foo", paths: ["artifact://999"] })).rejects.toThrow( - "Artifact 999 not found", - ); - }); - - it("emits forward-only, deduplicated context lines for adjacent virtual matches", async () => { - registerVirtualDocs(new Map([["doc.md", "l1\nneedle a\nl3\nneedle b\nl5\nl6\nl7\nl8\n"]])); - - const session = createSession({ - settings: Settings.isolated({ "grep.contextBefore": 1, "grep.contextAfter": 3 }), - }); - const tool = new GrepTool(session); - - const result = await tool.execute("test-call", { - pattern: "needle", - paths: ["virtual://doc.md"], - }); - - const text = getResultText(result); - const lineNumbers = text - .split("\n") - .map(line => /^[* ](\d+)\|/.exec(line)?.[1]) - .filter((n): n is string => n !== undefined) - .map(Number); - expect(lineNumbers.length).toBeGreaterThan(0); - for (let i = 1; i < lineNumbers.length; i++) { - expect(lineNumbers[i]).toBeGreaterThan(lineNumbers[i - 1]); - } - // Context between the two matches appears exactly once. - expect(lineNumbers.filter(n => n === 3)).toHaveLength(1); - }); - - it("matches an RE2 inline-flag pattern on a virtual resource (native dialect, not JS RegExp)", async () => { - registerVirtualDocs(new Map([["doc.md", "needle here\n"]])); - const tool = new GrepTool(createSession()); - const result = await tool.execute("re2-virtual", { pattern: "(?i)NEEDLE", paths: ["virtual://doc.md"] }); - expect(getResultText(result)).toContain("needle"); - }); - - it("applies an RE2 inline-flag pattern across mixed local and virtual scopes", async () => { - await Bun.write(path.join(tmpDir, "local.txt"), "needle local\n"); - registerVirtualDocs(new Map([["doc.md", "needle virtual\n"]])); - const tool = new GrepTool(createSession()); - const result = await tool.execute("re2-mixed", { - pattern: "(?i)NEEDLE", - paths: [path.join(tmpDir, "local.txt"), "virtual://doc.md"], - }); - const text = getResultText(result); - expect(text).toContain("local"); - expect(text).toContain("virtual"); - }); - - it("reports 'No more results' instead of 'No matches found' when skip is past the end", async () => { - await Bun.write(path.join(tmpDir, "a.txt"), "needle in a\n"); - await Bun.write(path.join(tmpDir, "b.txt"), "needle in b\n"); - - const session = createSession(); - const tool = new GrepTool(session); - - const result = await tool.execute("test-call", { - pattern: "needle", - paths: ["."], - skip: 5, - }); - - const text = getResultText(result); - expect(text).toContain("No more results"); - expect(text).toContain("2 files total"); - expect(text).not.toContain("No matches found"); - }); - - it("refuses to search a directory listing that has no backing local path", async () => { - // A directory resource with no sourcePath (e.g. a remote ssh:// listing) must - // not be virtual-grepped — its listing text is not the directory's contents. - InternalUrlRouter.instance().register({ - scheme: "dirstub", - immutable: true, - async resolve(url: InternalUrl): Promise { - return { url: url.href, content: "sub/\nfile.txt", contentType: "text/plain", isDirectory: true }; - }, - }); - const tool = new GrepTool(createSession()); - await expect(tool.execute("dir-search", { pattern: "x", paths: ["dirstub://host/dir"] })).rejects.toThrow( - /directory listing|cannot recurse/, - ); - }); - - it("rejects an ssh:// directory in search without draining a remote listing", async () => { - vi.spyOn(capability, "loadCapability").mockResolvedValue({ - items: [], - all: [], - warnings: [], - providers: [], - } as CapabilityResult); - vi.spyOn(sshFileTransfer, "readRemoteFile").mockRejectedValue(new Error("Is a directory")); - vi.spyOn(sshFileTransfer, "statRemotePath").mockResolvedValue("directory"); - const listSpy = vi.spyOn(sshFileTransfer, "listRemoteDir").mockResolvedValue([]); - const tool = new GrepTool(createSession()); - await expect(tool.execute("ssh-dir-search", { pattern: "x", paths: ["ssh://h/etc"] })).rejects.toThrow( - /directory listing|cannot recurse/, - ); - expect(listSpy).not.toHaveBeenCalled(); - }); - - it("searches an IPv6 ssh:// file instead of rejecting the brackets as a glob", async () => { - vi.spyOn(capability, "loadCapability").mockResolvedValue({ - items: [], - all: [], - warnings: [], - providers: [], - } as CapabilityResult); - vi.spyOn(sshFileTransfer, "statRemotePath").mockResolvedValue("file"); - vi.spyOn(sshFileTransfer, "readRemoteFile").mockResolvedValue({ - bytes: new TextEncoder().encode("needle here\n"), - truncated: false, - }); - const tool = new GrepTool(createSession()); - const result = await tool.execute("ssh-ipv6", { pattern: "needle", paths: ["ssh://[::1]/etc/hosts"] }); - expect(getResultText(result)).toContain("needle"); - }); + const text = getResultText(result); + <<<<<<< HEAD + expect(text).toContain("# omp://tools/grep.md") + expect(text).toContain("Grep file contents with a regex across files"); + ||||||| parent of 21c61eee6 (Fix coding-agent tests + for grep and glob merge + ) + expect(text).toContain("# omp://tools/grep.md") + expect(text).toContain("Greps files using regex."); + ======= + expect(text).toContain("regex across files") + expect(text).toContain("Grep file contents"); + >>>>>>> 21c61eee6 (Fix coding-agent tests + for grep and glob merge + ) }); + +it("expands omp://docs to grep embedded documentation files", async () => { + const session = createSession(); + const tool = new GrepTool(session); + + const result = await tool.execute("test-call", { + pattern: "Read files, directories, archives", + paths: ["omp://docs"], + }); + + const text = getResultText(result); + expect(text).toContain("# omp://tools/read.md"); + expect(text).toContain("Read files, directories, archives"); +}); + +it("throws when internal URL has no sourcePath", async () => { + const session = createSession(); + const tool = new GrepTool(session); + + expect(tool.execute("test-call", { pattern: "foo", paths: ["artifact://999"] })).rejects.toThrow( + "Artifact 999 not found", + ); +}); + +it("falls back to normal path resolution when no internalRouter", async () => { + await Bun.write(path.join(tmpDir, "test.txt"), "hello world\n"); + + const session = createSession(); + const tool = new GrepTool(session); + + const result = await tool.execute("test-call", { + pattern: "hello", + paths: ["test.txt"], + }); + + const text = getResultText(result); + expect(text).toContain("hello"); +}); + +it("falls back to normal resolution for non-internal URLs", async () => { + await Bun.write(path.join(tmpDir, "data.log"), "some data here\n"); + + const session = createSession(); + const tool = new GrepTool(session); + + const result = await tool.execute("test-call", { + pattern: "data", + paths: ["data.log"], + }); + + const text = getResultText(result); + expect(text).toContain("data"); +}); + +it("suppresses hashline anchors when searching immutable artifact:// sources", async () => { + const content = "alpha line\nbeta needle line\ngamma line\n"; + await Bun.write(path.join(artifactsDir, "9.bash.log"), content); + + const session = createSession({ hasEditTool: true }); + const tool = new GrepTool(session); + + const result = await tool.execute("test-call", { + pattern: "needle", + paths: ["artifact://9"], + }); + + const text = getResultText(result); + expect(text).toContain("needle"); + // No hashline section headers or numbered editable lines for immutable sources. + expect(text).not.toMatch(/^\[[^#\r\n]+#[0-9A-F]{4}\]$/m); + expect(text).not.toMatch(/^\*?\s*\d+:/m); +}); + +it("resolves local:// URLs before file-name lookup", async () => { + const localRoot = path.join(artifactsDir, "local"); + await fs.mkdir(localRoot, { recursive: true }); + await Bun.write(path.join(localRoot, "PLAN.md"), "# Plan\n"); + + LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" }); + + const session = createSession(); + const tool = new GlobTool(session); + + const result = await tool.execute("test-call", { + paths: ["local://PLAN.md"], + }); + + const text = getResultText(result); + expect(text).toContain("PLAN.md"); +}); + +it("walks local:// directory subpaths for read and find", async () => { + const localRoot = path.join(artifactsDir, "local"); + await fs.mkdir(path.join(localRoot, "notes"), { recursive: true }); + await Bun.write(path.join(localRoot, "notes", "PLAN.md"), "# Plan\n"); + + LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" }); + + const session = createSession({ hasEditTool: true }); + const readResult = await new ReadTool(session).execute("test-read", { path: "local://notes" }); + const findResult = await new GlobTool(session).execute("test-find", { + paths: ["local://notes"], + }); + const dirResource = await InternalUrlRouter.instance().resolve("local://notes"); + + const readText = getResultText(readResult); + expect(readText).toContain("PLAN.md"); + // Directory listings must stay immutable so hashline edit anchors never key on a directory path. + expect(readText).not.toMatch(/^\[[^#\r\n]+#[0-9A-F]{4}\]$/m); + expect(dirResource.immutable).toBe(true); + expect(getResultText(findResult)).toContain("PLAN.md"); +}); + +it("keeps hashline anchors when searching mutable local:// sources", async () => { + const localRoot = path.join(artifactsDir, "local"); + await fs.mkdir(localRoot, { recursive: true }); + await Bun.write(path.join(localRoot, "plan.md"), "alpha line\nbeta needle line\ngamma line\n"); + + LocalProtocolHandler.setOverride({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" }); + + const session = createSession({ hasEditTool: true }); + const tool = new GrepTool(session); + + const result = await tool.execute("test-call", { + pattern: "needle", + paths: ["local://plan.md"], + }); + + const text = getResultText(result); + expect(text).toContain("needle"); + // Mutable local:// sources keep a hashline section header plus numbered match lines. + expect(text).toMatch(/^\[[^#\r\n]+#[0-9A-F]{4}\]$/m); + expect(text).toMatch(/^\*\d+:.*needle/m); +}); + +it("keeps hashlines on mutable files when mixed with immutable artifact:// inputs", async () => { + const content = "alpha line\nbeta needle line\ngamma line\n"; + await Bun.write(path.join(artifactsDir, "11.bash.log"), content); + await Bun.write(path.join(tmpDir, "mixed.txt"), "mixed needle line\n"); + + const session = createSession({ hasEditTool: true }); + const tool = new GrepTool(session); + + const result = await tool.execute("test-call", { + pattern: "needle", + paths: ["artifact://11", "mixed.txt"], + }); + + const text = getResultText(result); + expect(text).toContain("needle"); + // Mutable mixed.txt keeps hashlines somewhere in the output. + expect(text).toMatch(/^# mixed\.txt#[0-9A-F]{4}/m); + expect(text).toMatch(/^\*\d+:.*mixed needle/m); +}); + +it("throws on nonexistent artifact ID", async () => { + const session = createSession(); + const tool = new GrepTool(session); + + expect(tool.execute("test-call", { pattern: "foo", paths: ["artifact://999"] })).rejects.toThrow( + "Artifact 999 not found", + ); +}); + +it("emits forward-only, deduplicated context lines for adjacent virtual matches", async () => { + registerVirtualDocs(new Map([["doc.md", "l1\nneedle a\nl3\nneedle b\nl5\nl6\nl7\nl8\n"]])); + + const session = createSession({ + settings: Settings.isolated({ "grep.contextBefore": 1, "grep.contextAfter": 3 }), + }); + const tool = new GrepTool(session); + + const result = await tool.execute("test-call", { + pattern: "needle", + paths: ["virtual://doc.md"], + }); + + const text = getResultText(result); + const lineNumbers = text + .split("\n") + .map(line => /^[* ](\d+)\|/.exec(line)?.[1]) + .filter((n): n is string => n !== undefined) + .map(Number); + expect(lineNumbers.length).toBeGreaterThan(0); + for (let i = 1; i < lineNumbers.length; i++) { + expect(lineNumbers[i]).toBeGreaterThan(lineNumbers[i - 1]); + } + // Context between the two matches appears exactly once. + expect(lineNumbers.filter(n => n === 3)).toHaveLength(1); +}); + +it("matches an RE2 inline-flag pattern on a virtual resource (native dialect, not JS RegExp)", async () => { + registerVirtualDocs(new Map([["doc.md", "needle here\n"]])); + const tool = new GrepTool(createSession()); + const result = await tool.execute("re2-virtual", { pattern: "(?i)NEEDLE", paths: ["virtual://doc.md"] }); + expect(getResultText(result)).toContain("needle"); +}); + +it("applies an RE2 inline-flag pattern across mixed local and virtual scopes", async () => { + await Bun.write(path.join(tmpDir, "local.txt"), "needle local\n"); + registerVirtualDocs(new Map([["doc.md", "needle virtual\n"]])); + const tool = new GrepTool(createSession()); + const result = await tool.execute("re2-mixed", { + pattern: "(?i)NEEDLE", + paths: [path.join(tmpDir, "local.txt"), "virtual://doc.md"], + }); + const text = getResultText(result); + expect(text).toContain("local"); + expect(text).toContain("virtual"); +}); + +it("reports 'No more results' instead of 'No matches found' when skip is past the end", async () => { + await Bun.write(path.join(tmpDir, "a.txt"), "needle in a\n"); + await Bun.write(path.join(tmpDir, "b.txt"), "needle in b\n"); + + const session = createSession(); + const tool = new GrepTool(session); + + const result = await tool.execute("test-call", { + pattern: "needle", + paths: ["."], + skip: 5, + }); + + const text = getResultText(result); + expect(text).toContain("No more results"); + expect(text).toContain("2 files total"); + expect(text).not.toContain("No matches found"); +}); + +it("refuses to search a directory listing that has no backing local path", async () => { + // A directory resource with no sourcePath (e.g. a remote ssh:// listing) must + // not be virtual-grepped — its listing text is not the directory's contents. + InternalUrlRouter.instance().register({ + scheme: "dirstub", + immutable: true, + async resolve(url: InternalUrl): Promise { + return { url: url.href, content: "sub/\nfile.txt", contentType: "text/plain", isDirectory: true }; + }, + }); + const tool = new GrepTool(createSession()); + await expect(tool.execute("dir-search", { pattern: "x", paths: ["dirstub://host/dir"] })).rejects.toThrow( + /directory listing|cannot recurse/, + ); +}); + +it("rejects an ssh:// directory in search without draining a remote listing", async () => { + vi.spyOn(capability, "loadCapability").mockResolvedValue({ + items: [], + all: [], + warnings: [], + providers: [], + } as CapabilityResult); + vi.spyOn(sshFileTransfer, "readRemoteFile").mockRejectedValue(new Error("Is a directory")); + vi.spyOn(sshFileTransfer, "statRemotePath").mockResolvedValue("directory"); + const listSpy = vi.spyOn(sshFileTransfer, "listRemoteDir").mockResolvedValue([]); + const tool = new GrepTool(createSession()); + await expect(tool.execute("ssh-dir-search", { pattern: "x", paths: ["ssh://h/etc"] })).rejects.toThrow( + /directory listing|cannot recurse/, + ); + expect(listSpy).not.toHaveBeenCalled(); +}); + +it("searches an IPv6 ssh:// file instead of rejecting the brackets as a glob", async () => { + vi.spyOn(capability, "loadCapability").mockResolvedValue({ + items: [], + all: [], + warnings: [], + providers: [], + } as CapabilityResult); + vi.spyOn(sshFileTransfer, "statRemotePath").mockResolvedValue("file"); + vi.spyOn(sshFileTransfer, "readRemoteFile").mockResolvedValue({ + bytes: new TextEncoder().encode("needle here\n"), + truncated: false, + }); + const tool = new GrepTool(createSession()); + const result = await tool.execute("ssh-ipv6", { pattern: "needle", paths: ["ssh://[::1]/etc/hosts"] }); + expect(getResultText(result)).toContain("needle"); +}); +}) diff --git a/packages/coding-agent/test/tools/grep-path-lists.test.ts b/packages/coding-agent/test/tools/grep-path-lists.test.ts index cdd445c18..276118ca5 100644 --- a/packages/coding-agent/test/tools/grep-path-lists.test.ts +++ b/packages/coding-agent/test/tools/grep-path-lists.test.ts @@ -422,8 +422,8 @@ describe("tool path arrays", () => { const rendered = renderTree([root], toolResult.entry.id); - expect(rendered).toContain("[search: /space-needle/ in folder with spaces/]"); - expect(rendered).not.toContain("[search: /space-needle/ in .]"); + expect(rendered).toContain("[grep: /space-needle/ in folder with spaces/]"); + expect(rendered).not.toContain("[grep: /space-needle/ in .]"); }); it("search keeps a single path that contains spaces", async () => { diff --git a/packages/coding-agent/test/tools/read-pdf-line-range.test.ts b/packages/coding-agent/test/tools/read-pdf-line-range.test.ts index cb97f146b..a1cb17321 100644 --- a/packages/coding-agent/test/tools/read-pdf-line-range.test.ts +++ b/packages/coding-agent/test/tools/read-pdf-line-range.test.ts @@ -13,7 +13,7 @@ import { Markit } from "@oh-my-pi/pi-coding-agent/markit"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read"; import * as markit from "@oh-my-pi/pi-coding-agent/utils/markit"; -import { __resetDirsFromEnvForTests, removeSyncWithRetries, setAgentDir, Snowflake } from "@oh-my-pi/pi-utils"; +import { __resetDirsFromEnvForTests, removeSyncWithRetries, Snowflake, setAgentDir } from "@oh-my-pi/pi-utils"; function restoreEnv(key: string, value: string | undefined): void { if (value === undefined) { diff --git a/packages/coding-agent/test/tools/search-tool-bm25.test.ts b/packages/coding-agent/test/tools/search-tool-bm25.test.ts index 510234aa2..d4648ce77 100644 --- a/packages/coding-agent/test/tools/search-tool-bm25.test.ts +++ b/packages/coding-agent/test/tools/search-tool-bm25.test.ts @@ -226,7 +226,7 @@ describe("renderSearchToolBm25Description", () => { // Built-in names are present verbatim, alphabetically ordered, on their own line. expect(lineWith(rendered, "Discoverable built-in tools:")).toBe( - "Discoverable built-in tools: find, search, write.", + "Discoverable built-in tools: glob, grep, write.", ); // Built-in names must not bleed into the MCP server-summary line. @@ -236,7 +236,7 @@ describe("renderSearchToolBm25Description", () => { expect(mcpLine).not.toContain("glob"); expect(rendered).toContain( "Discoverable MCP servers in this session: github (1 tool), slack (1 tool).\n" + - "Discoverable built-in tools: find, search, write.\n" + + "Discoverable built-in tools: glob, grep, write.\n" + "Total discoverable tools available: 5.", ); });