fix(glob,grep): tolerate broken symlinks and non-fatal I/O warnings

Closes #3726.

## Root cause

Two coupled bugs in the ripgrep integration caused the glob and grep
tools to fail outright when the search path contained a broken
(dangling) symlink:

1. **Missing `--no-messages`** in `RG_FILES_FLAGS` (glob) and
   `RG_SAFETY_FLAGS` (grep). ripgrep prints a stderr warning for every
   broken symlink:
   `rg: /path/to/broken-link: No such file or directory (os error 2)`

2. **Overly strict exit-code gate** in both `glob/cli.ts` and
   `grep/cli.ts`: `if (exitCode > 1 && stderr.trim())` treated exit
   code 2 as fatal and discarded the stdout, even though ripgrep exits
   2 on *non-fatal* I/O issues (broken symlinks, permission denied)
   while still printing valid results to stdout.

Combined effect: a single broken symlink anywhere in the search path
turned all subsequent file matches into an empty result with an error.

## Fix

- Append `--no-messages` to `RG_FILES_FLAGS` (`src/tools/glob/constants.ts`)
  and to `RG_SAFETY_FLAGS` (`src/tools/grep/constants.ts`). This silences
  ripgrep's non-fatal stderr warnings without changing match behavior.
- Relax the exit-code gate in both `glob/cli.ts` and `grep/cli.ts` from
  `> 1` to `> 2`, matching ripgrep's documented contract:
  - 0 = matches found
  - 1 = no matches (success, just nothing matched)
  - 2 = non-fatal I/O issues (partial success — stdout is still valid)
  - >2 = fatal error

## Test changes

- New regression assertion in `src/tools/glob/cli.test.ts` for
  `buildRgArgs` confirms `--no-messages` is included in the args.
- The grep change is symmetric (same flag, same rationale, same
  exit-code constant) and rides on the parallel structure.

## Verification

- `bun test src/tools/glob/` → all pass (19 tests in cli.test.ts)
- `bun test src/tools/grep/` → all pre-existing tests still pass
- `bunx tsc --noEmit` → clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZeyuFu
2026-05-17 10:59:41 -04:00
parent babee921b2
commit e195a47205
5 changed files with 25 additions and 2 deletions
+9
View File
@@ -54,6 +54,15 @@ describe("buildRgArgs", () => {
const args = buildRgArgs({ pattern: "**/*.tsx" })
expect(args).toContain("--glob=**/*.tsx")
})
// Regression for #3726: broken/dangling symlinks should not surface as
// tool errors. --no-messages silences ripgrep's non-fatal stderr warnings
// so the downstream "exit code > 2 && stderr.trim()" gate sees a clean
// stream when only I/O warnings were emitted.
it("includes --no-messages so broken symlinks do not error the tool (#3726)", () => {
const args = buildRgArgs({ pattern: "*.ts" })
expect(args).toContain("--no-messages")
})
})
describe("buildFindArgs", () => {
+4 -1
View File
@@ -152,7 +152,10 @@ async function runRgFilesInternal(
const stderr = await new Response(proc.stderr).text()
const exitCode = await proc.exited
if (exitCode > 1 && stderr.trim()) {
// ripgrep exits 2 on non-fatal I/O errors (broken symlinks, permission
// denied) even when stdout still contains valid file matches; treat only
// exit codes >2 as fatal so partial results survive. See #3726.
if (exitCode > 2 && stderr.trim()) {
return {
files: [],
totalFiles: 0,
+4
View File
@@ -9,4 +9,8 @@ export const RG_FILES_FLAGS = [
"--files",
"--color=never",
"--glob=!.git/*",
// Suppress stderr warnings on broken/dangling symlinks and similar
// non-fatal I/O issues so they don't tip the tool into the error branch.
// See #3726.
"--no-messages",
] as const
+4 -1
View File
@@ -197,7 +197,10 @@ async function runRgInternal(options: GrepOptions, resolvedCli?: ResolvedCli): P
const truncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES
const outputToProcess = truncated ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES) : stdout
if (exitCode > 1 && stderr.trim()) {
// ripgrep exits 2 on non-fatal I/O errors (broken symlinks, permission
// denied) even when stdout still contains valid matches; treat only
// exit codes >2 as fatal so partial results survive. See #3726.
if (exitCode > 2 && stderr.trim()) {
return {
matches: [],
totalMatches: 0,
+4
View File
@@ -12,6 +12,10 @@ export const RG_SAFETY_FLAGS = [
"--no-heading",
"--line-number",
"--with-filename",
// Suppress stderr warnings on broken/dangling symlinks and similar
// non-fatal I/O issues so they don't tip the tool into the error branch.
// See #3726.
"--no-messages",
] as const
export const GREP_SAFETY_FLAGS = ["-n", "-H", "--color=never"] as const