fix(search): publish capped results without waiting for process exit (#34861)

* fix(search): publish capped content results before process exit

* test(search): drop duplicate queued kill-error variant

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-28 12:51:40 -07:00
committed by GitHub
co-authored by OrcaWin
parent 7d29cd0839
commit 4ce1e18930
2 changed files with 58 additions and 6 deletions
@@ -28,6 +28,8 @@ import { stopBundledRipgrep } from './bundled-ripgrep-stop'
// NTFS permits POSIX-created names/types Windows rg cannot open (1/2/1920); skip them without truncating the search.
const WINDOWS_UNOPENABLE_PATH_OS_ERRORS: ReadonlySet<number> = new Set([1, 2, 1920])
function ignoreCompletedSearchTerminationError(): void {}
export function runBundledRipgrepTextSearch({
options,
rootPath,
@@ -110,10 +112,7 @@ export function runBundledRipgrepTextSearch({
}
const rejectUnavailable = (): void => finish(Promise.reject(bundledRipgrepUnavailableError()))
const processLine = (line: string): void => {
const verdict = ingestRgJsonLine(line, resultRootPath, acc, maxResults, transformAbsPath)
if (verdict === 'stop' && child) {
stopBundledRipgrep(child, Boolean(wslDistroForOutput))
}
ingestRgJsonLine(line, resultRootPath, acc, maxResults, transformAbsPath)
}
let nextChild: ReturnType<typeof spawnBundledRipgrep>
@@ -135,12 +134,14 @@ export function runBundledRipgrepTextSearch({
releaseChild = onSpawn(nextChild)
const handleStdoutData = (chunk: string): void => {
if (!lines.push(chunk, processLine)) {
const withinLineLimit = lines.push(chunk, processLine)
if (!withinLineLimit || acc.totalMatches >= maxResults) {
acc.truncated = true
child?.once('error', ignoreCompletedSearchTerminationError)
resolveOnce()
if (child) {
stopBundledRipgrep(child, Boolean(wslDistroForOutput))
}
resolveOnce()
}
}
const handleStderrData = (chunk: Buffer): void => {
@@ -168,6 +168,57 @@ describe('RuntimeFileCommands', () => {
)
})
it.each(['ignored', 'erroring'] as const)(
'publishes capped results and detaches when termination is %s',
async (termination) => {
const { commands } = createRuntimeFileCommands()
const child = createRuntimeSearchChild()
if (termination === 'erroring') {
child.kill.mockImplementation(() => {
child.emit('error', Object.assign(new Error('kill EPERM'), { code: 'EPERM' }))
return false
})
}
resolveAuthorizedPathMock.mockResolvedValue('/repo')
wslAwareSpawnMock.mockReturnValue(child)
const resultPromise = commands.searchRuntimeFiles('id:wt-1', {
query: 'needle',
maxResults: 2
})
let settled = false
void resultPromise.then(() => {
settled = true
})
await flushRuntimeSearchMicrotasks()
const record = JSON.stringify({
type: 'match',
data: {
path: { text: './example.txt' },
lines: { text: 'needle needle\n' },
line_number: 1,
submatches: [
{ start: 0, end: 6 },
{ start: 7, end: 13 }
]
}
})
expect(() => child.stdout.emit('data', `${record}\n{"type":"match"`)).not.toThrow()
await flushRuntimeSearchMicrotasks()
expect(settled).toBe(true)
await expect(resultPromise).resolves.toMatchObject({
totalMatches: 2,
truncated: true,
files: [{ matches: [{ column: 1 }, { column: 8 }] }]
})
expect(child.kill).toHaveBeenCalledOnce()
expect(child.stdout.listenerCount('data')).toBe(0)
expect(child.stderr.listenerCount('data')).toBe(0)
expect(child.listenerCount('close')).toBe(0)
expect(vi.getTimerCount()).toBe(0)
}
)
it.each(['error-first', 'close-first'] as const)(
'rejects with the bundled-ripgrep error when runtime rg native launch failure is %s',
async (order) => {