Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/opencode/src/tool/grep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
}

Expand Down
50 changes: 50 additions & 0 deletions packages/opencode/test/tool/grep.test.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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({
Expand Down