diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index ad62621e072..9eccba0131b 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -60,7 +60,10 @@ export const GrepTool = Tool.define("grep", { } } - if (exitCode !== 0) { + // Non-zero exit code means an error occurred, but ripgrep might have + // returned some data regardless. In that case, the error was likely a soft + // error (like a broken symlink), rather than something fatal. + if (exitCode !== 0 && !output.trim()) { throw new Error(`ripgrep failed: ${errorOutput}`) } diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index a79d931575c..98896f4460f 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import path from "path" +import fs from "fs/promises" import { GrepTool } from "../../src/tool/grep" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" @@ -59,6 +60,55 @@ describe("tool.grep", () => { }) }) + test("returns results when broken symlink exists", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "valid.txt"), "hello world") + const subdir = path.join(dir, "subdir") + await fs.mkdir(subdir) + await fs.symlink(path.join(dir, "/nonexistent/path"), path.join(subdir, "broken")) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const grep = await GrepTool.init() + const result = await grep.execute( + { + pattern: "hello", + path: tmp.path, + }, + ctx, + ) + expect(result.metadata.matches).toBe(1) + expect(result.output).toContain("valid.txt") + }, + }) + }) + + test("throws on ripgrep error without stdout", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "test.txt"), "hello world") + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const grep = await GrepTool.init() + expect( + grep.execute( + { + pattern: "[invalid", + path: tmp.path, + }, + ctx, + ), + ).rejects.toThrow("ripgrep failed") + }, + }) + }) + test("handles CRLF line endings in output", async () => { // This test verifies the regex split handles both \n and \r\n await using tmp = await tmpdir({