Files
orca/src/shared/ripgrep-process-availability.test.ts
T
Jinjing 02bee48e1d Retry transient ripgrep spawn failures instead of demanding install (#15983)
* retry transient ripgrep spawn failures instead of missing binary errors

Fork/exec pressure (EAGAIN, EMFILE, ENFILE, ENOMEM, ETXTBSY) should not
trigger ripgrep-not-found guidance. Add bounded retries (max 2x) for transient
spawn failures in Quick Open and file listing, respecting cancellation signals.
Introduce RipgrepLaunchFailureError to distinguish fork/exec pressure from
unavailable ripgrep installations.

* Handle cancellation during transient spawn failure retry window

When a query is cancelled after a transient ripgrep spawn failure but before
the retry decision resumes, the cancellation must be reported to the caller
rather than proceeding with a retry attempt.
2026-08-22 13:06:38 -07:00

108 lines
3.7 KiB
TypeScript

import { spawn, type ChildProcess } from 'node:child_process'
import { EventEmitter } from 'node:events'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
absorbPendingRipgrepSpawnError,
isRipgrepUnavailableExit,
isRipgrepSpawnCwdUsable,
isTransientRipgrepSpawnError,
killSpawnedRipgrepProcess
} from './ripgrep-process-availability'
function createChild(pid: number | undefined): ChildProcess {
const child = new EventEmitter() as ChildProcess
Object.defineProperty(child, 'pid', { value: pid })
return child
}
describe('ripgrep process availability', () => {
it('recognizes native pre-spawn failures before either event wins the race', () => {
expect(isRipgrepUnavailableExit(createChild(undefined), null, null)).toBe(true)
expect(isRipgrepUnavailableExit(createChild(undefined), -2, null)).toBe(true)
})
it('keeps all post-spawn and signal exits on their existing paths', () => {
for (const code of [0, 1, 2, 127]) {
expect(isRipgrepUnavailableExit(createChild(1), code, null)).toBe(false)
}
expect(isRipgrepUnavailableExit(createChild(1), null, 'SIGTERM')).toBe(false)
})
it('tags only unsupported native launcher exits after spawn', () => {
for (const code of [0, 1, 2]) {
expect(
isRipgrepUnavailableExit(createChild(1), code, null, {
classifyNativeLauncherExit: true
})
).toBe(false)
}
for (const code of [3, 126, 127, 9009]) {
expect(
isRipgrepUnavailableExit(createChild(1), code, null, {
classifyNativeLauncherExit: true
})
).toBe(true)
}
})
it('does not signal a real failed-spawn handle', async () => {
const child = spawn('orca-definitely-missing-rg-admission-test', [])
const error = new Promise<void>((resolve) => child.once('error', () => resolve()))
const close = new Promise<void>((resolve) => child.once('close', () => resolve()))
expect(killSpawnedRipgrepProcess(child)).toBe(false)
await Promise.all([error, close])
})
it('distinguishes a usable root from a missing spawn cwd', async () => {
await expect(isRipgrepSpawnCwdUsable(process.cwd())).resolves.toBe(true)
await expect(
isRipgrepSpawnCwdUsable(join(process.cwd(), 'orca-definitely-missing-rg-cwd'))
).resolves.toBe(false)
})
it('absorbs a queued spawn error when cleanup wins the race', () => {
const child = createChild(undefined)
absorbPendingRipgrepSpawnError(child, {
errorObserved: false,
unavailableExitObserved: false
})
expect(() => child.emit('error', new Error('spawn rg ENOENT'))).not.toThrow()
expect(child.listenerCount('error')).toBe(0)
})
it('does not retain a sink for started or already-observed processes', () => {
const started = createChild(1)
absorbPendingRipgrepSpawnError(started, {
errorObserved: false,
unavailableExitObserved: false
})
const observed = createChild(undefined)
absorbPendingRipgrepSpawnError(observed, {
errorObserved: true,
unavailableExitObserved: true
})
expect(started.listenerCount('error')).toBe(0)
expect(observed.listenerCount('error')).toBe(0)
})
it('separates transient fork/exec pressure from a missing ripgrep binary', () => {
for (const code of ['EAGAIN', 'EMFILE', 'ENFILE', 'ENOMEM', 'ETXTBSY']) {
expect(isTransientRipgrepSpawnError(Object.assign(new Error('spawn rg'), { code }))).toBe(
true
)
}
for (const error of [
Object.assign(new Error('spawn rg ENOENT'), { code: 'ENOENT' }),
Object.assign(new Error('spawn rg EACCES'), { code: 'EACCES' }),
new Error('spawn rg'),
null
]) {
expect(isTransientRipgrepSpawnError(error)).toBe(false)
}
})
})