fix(e2b): close remote lifecycle gaps

This commit is contained in:
Tianyi Cui
2026-07-28 16:26:20 +08:00
parent e64d40837c
commit 3dea36f1ce
22 changed files with 568 additions and 181 deletions

View File

@@ -199,6 +199,7 @@ export class E2BFileSystem extends FileSystem {
const reader = stream.getReader()
const decoder = new TextDecoder('utf-8', { fatal: true })
let sampledBytes = 0
let completed = false
try {
while (true) {
assertNotAborted(signal, 'read')
@@ -222,9 +223,17 @@ export class E2BFileSystem extends FileSystem {
} catch (error: unknown) {
throw new FsError(`cannot read "${displayPath}": invalid UTF-8 text`, 'FS_NOT_TEXT', { cause: error })
}
completed = true
} catch (error: unknown) {
throw mapError(error, 'read', displayPath, signal)
} finally {
if (!completed) {
try {
await reader.cancel()
} catch (_streamCancellationFailure) {
// The primary read outcome owns the result; cancellation is best-effort after early stop.
}
}
reader.releaseLock()
}
},

View File

@@ -12,7 +12,7 @@ import { FsVersion } from '@deepseek-ai/dsh-fs'
import E2BFileSystem from '@deepseek-ai/dsh-fs-e2b'
import * as E2BFsInvariant from '../src/invariant.ts'
import InvariantService from '@deepseek-ai/dsh-invariants'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
interface RemoteNode {
type: FileType
@@ -38,6 +38,8 @@ class FakeRemote {
readonly removals: string[] = []
readonly commands: string[] = []
streamChunks: Uint8Array[] | undefined
streamKeepOpen = false
readonly streamCancel = vi.fn()
nextCommandError: unknown
nextInfoError: unknown
nextListError: unknown
@@ -148,10 +150,11 @@ class FakeRemote {
if (options.format === 'bytes') return data.slice()
const chunks = this.streamChunks ?? [data.slice()]
return new ReadableStream<Uint8Array>({
start(controller) {
start: (controller) => {
for (const chunk of chunks) controller.enqueue(chunk)
controller.close()
if (!this.streamKeepOpen) controller.close()
},
cancel: () => { this.streamCancel() },
})
},
list: async (path: string, options?: { depth?: number; signal?: AbortSignal }): Promise<EntryInfo[]> => {
@@ -303,6 +306,22 @@ describe('E2BFileSystem identity, metadata, and reads', () => {
expect(initiallyBuffered).toBe('€')
})
it('cancels a remote stream when its consumer stops early', async () => {
const remote = new FakeRemote()
remote.file('/workspace/text.txt', 'ab')
remote.streamChunks = [bytes('a'), bytes('b')]
remote.streamKeepOpen = true
const { fs } = await setup(remote)
const stream = await fs.streamText(await fs.resolve('text.txt'))
for await (const chunk of stream) {
expect(chunk).toBe('a')
break
}
expect(remote.streamCancel).toHaveBeenCalledOnce()
})
it('matches local binary sampling while edits still reject any NUL byte', async () => {
const remote = new FakeRemote()
remote.file('/workspace/late-nul.txt', `${'a'.repeat(8192)}\0tail`)