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. */
|
/** Close the spill file (if any) and return the final output. */
|
||||||
finalize(): CollectedOutput {
|
finalize(): CollectedOutput {
|
||||||
if (this.spillFd !== undefined) {
|
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
|
this.spillFd = undefined
|
||||||
}
|
}
|
||||||
return this.snapshot()
|
return this.snapshot()
|
||||||
|
|||||||
@@ -4,6 +4,21 @@ import { dirname, join } from 'node:path'
|
|||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { killGroup, OutputCollector, runBash } from '@deepseek-ai/dsh-bash-local'
|
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-'))
|
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-bash-spec-'))
|
||||||
|
|
||||||
function spec(command: string, overrides: Partial<Parameters<typeof runBash>[0]> = {}) {
|
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.text.length).toBe(500)
|
||||||
expect(result.stdout.spillPath).toBeUndefined()
|
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', () => {
|
describe('OutputCollector', () => {
|
||||||
@@ -200,6 +228,22 @@ describe('OutputCollector', () => {
|
|||||||
expect(collector.totalBytes).toBe(8)
|
expect(collector.totalBytes).toBe(8)
|
||||||
expect(collector.finalize().text).toBe('bbbb')
|
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', () => {
|
describe('killGroup', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user