fix(bash-local): contain spill close failures
This commit is contained in:
@@ -195,7 +195,15 @@ export class OutputCollector {
|
||||
/** Close the spill file (if any) and return the final output. */
|
||||
finalize(): CollectedOutput {
|
||||
if (this.spillFd !== undefined) {
|
||||
closeSync(this.spillFd)
|
||||
try {
|
||||
closeSync(this.spillFd)
|
||||
} catch {
|
||||
// close can surface delayed writeback failures (for example EIO/ENOSPC)
|
||||
// after writeSync appeared to succeed. Keep finalize total so runBash's
|
||||
// close handler still resolves, but stop advertising a spill file that
|
||||
// may be missing its tail.
|
||||
this.spillFile = undefined
|
||||
}
|
||||
this.spillFd = undefined
|
||||
}
|
||||
return this.snapshot()
|
||||
|
||||
@@ -4,6 +4,21 @@ import { dirname, join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { killGroup, OutputCollector, runBash } from '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
const { failNextClose } = vi.hoisted(() => ({ failNextClose: { value: false } }))
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs')>()
|
||||
return {
|
||||
...actual,
|
||||
closeSync(fd: number): void {
|
||||
if (failNextClose.value) {
|
||||
failNextClose.value = false
|
||||
throw Object.assign(new Error('simulated EIO on close'), { code: 'EIO' })
|
||||
}
|
||||
actual.closeSync(fd)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-bash-spec-'))
|
||||
|
||||
function spec(command: string, overrides: Partial<Parameters<typeof runBash>[0]> = {}) {
|
||||
@@ -160,6 +175,19 @@ describe('output truncation and spill', () => {
|
||||
expect(result.stdout.text.length).toBe(500)
|
||||
expect(result.stdout.spillPath).toBeUndefined()
|
||||
})
|
||||
|
||||
it('settles with the tail and no spill path when final spill close fails', async () => {
|
||||
failNextClose.value = true
|
||||
const result = await runBash(
|
||||
spec('for i in $(seq 1 200); do printf "line-%04d\\n" $i; done', { maxOutputBytes: 500 }),
|
||||
{ spillDir },
|
||||
).done
|
||||
expect(failNextClose.value).toBe(false)
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout.truncated).toBe(true)
|
||||
expect(result.stdout.text).toContain('line-0200')
|
||||
expect(result.stdout.spillPath).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('OutputCollector', () => {
|
||||
@@ -200,6 +228,22 @@ describe('OutputCollector', () => {
|
||||
expect(collector.totalBytes).toBe(8)
|
||||
expect(collector.finalize().text).toBe('bbbb')
|
||||
})
|
||||
|
||||
it('contains close failures and drops the spill path', () => {
|
||||
const collector = new OutputCollector(4, 'closefail', spillDir)
|
||||
collector.push(Buffer.from('aaaa'))
|
||||
collector.push(Buffer.from('bbbb'))
|
||||
expect(collector.snapshot().spillPath).toBeDefined()
|
||||
|
||||
failNextClose.value = true
|
||||
let out: ReturnType<typeof collector.finalize>
|
||||
expect(() => { out = collector.finalize() }).not.toThrow()
|
||||
|
||||
expect(failNextClose.value).toBe(false)
|
||||
expect(out!.text).toBe('bbbb')
|
||||
expect(out!.truncated).toBe(true)
|
||||
expect(out!.spillPath).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('killGroup', () => {
|
||||
|
||||
Reference in New Issue
Block a user