mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* 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.
108 lines
3.7 KiB
TypeScript
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)
|
|
}
|
|
})
|
|
})
|