fix(e2b): close adapter lifecycle review gaps

This commit is contained in:
Tianyi Cui
2026-07-29 09:19:46 +08:00
parent f5306f6e89
commit 219999cd2f
14 changed files with 375 additions and 103 deletions

View File

@@ -111,18 +111,7 @@ class FakeTerminalSandbox {
resolvedExecutable = '/usr/bin/node\n'
requestedOutput = 'requested-shell$ '
emitOutputMarker = true
private createGate: Promise<void> | undefined
private releaseCreateGate: (() => void) | undefined
deferCreate(): void {
const gate = Promise.withResolvers<undefined>()
this.createGate = gate.promise
this.releaseCreateGate = () => { gate.resolve(undefined) }
}
releaseCreate(): void {
this.releaseCreateGate?.()
}
afterSessionLookup: (() => void) | undefined
readonly sandbox = {
files: {
@@ -163,7 +152,10 @@ class FakeTerminalSandbox {
if (command.includes('command -v -- ')) {
return { exitCode: 0, stdout: this.resolvedExecutable, stderr: '' }
}
if (command.startsWith('ps -o sid=')) return { exitCode: 0, stdout: this.sessionId, stderr: '' }
if (command.startsWith('ps -o sid=')) {
this.afterSessionLookup?.()
return { exitCode: 0, stdout: this.sessionId, stderr: '' }
}
if (command.startsWith('ps -o tpgid=')) {
if (this.foregroundFailure !== undefined) throw this.foregroundFailure
return { exitCode: 0, stdout: this.foreground, stderr: '' }
@@ -186,7 +178,6 @@ class FakeTerminalSandbox {
pty: {
create: async (options: Parameters<Sandbox['pty']['create']>[0]): Promise<CommandHandle> => {
this.createOptions = options
await this.createGate
if (this.createError !== undefined) throw this.createError
await options.onData(Buffer.from('buffered banner\n'))
return this.handle.asHandle()
@@ -691,7 +682,7 @@ describe('E2B subprocess terminal service', () => {
it('owns live terminals through service disposal', async () => {
const { ctx, fiber, fake } = await service()
const terminal = await ctx.subprocess.spawnTerminal(spec())
const terminal = await ctx.subprocess.spawnTerminal(spec({ signal: new AbortController().signal }))
await fiber.dispose()
await expect(terminal.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
expect(fake.handle.disconnects).toBe(1)
@@ -699,19 +690,19 @@ describe('E2B subprocess terminal service', () => {
it('joins and rejects terminal setup that completes during service disposal', async () => {
const fake = new FakeTerminalSandbox()
fake.deferCreate()
const { ctx, fiber } = await service(fake)
let disposing: Promise<void> | undefined
fake.afterSessionLookup = () => {
fake.afterSessionLookup = undefined
queueMicrotask(() => {
queueMicrotask(() => { disposing = fiber.dispose() })
})
}
const subprocess = ctx.subprocess
const spawning = ctx.subprocess.spawnTerminal(spec())
const rejected = expect(spawning).rejects.toThrow('service disposed during terminal setup')
await vi.waitFor(() => { expect(fake.createOptions).toBeDefined() })
let disposed = false
const subprocess = ctx.subprocess
const disposing = fiber.dispose().then(() => { disposed = true })
await new Promise(resolve => setTimeout(resolve, 0))
expect(disposed).toBe(false)
await vi.waitFor(() => { expect(disposing).toBeDefined() })
await expect(subprocess.spawnTerminal(spec())).rejects.toThrow('service is disposing')
fake.releaseCreate()
await rejected
await disposing
@@ -720,6 +711,37 @@ describe('E2B subprocess terminal service', () => {
expect(fake.removed.some(path => path.includes('/terminals/'))).toBe(true)
})
it('aborts and rolls back terminal setup that cannot publish readiness during disposal', async () => {
const fake = new FakeTerminalSandbox()
fake.ready = new FileNotFoundError('not ready')
const { ctx, fiber } = await service(fake)
const spawning = ctx.subprocess.spawnTerminal(spec())
const rejected = expect(spawning).rejects.toThrow('service disposed during terminal setup')
await vi.waitFor(() => { expect(fake.readyReads).toBeGreaterThan(0) })
await fiber.dispose()
await rejected
expect(fake.groups).toEqual([])
expect(fake.handle.disconnects).toBe(1)
})
it('retains failed terminal setup cleanup for disposal retry', async () => {
const fake = new FakeTerminalSandbox()
fake.sendError = new Error('bootstrap failed')
fake.clearOnTerm = false
fake.clearOnKill = false
const { ctx, fiber } = await service(fake)
await expect(ctx.subprocess.spawnTerminal(spec({ graceMs: 1 }))).rejects.toThrow('bootstrap failed')
expect(fake.groups).toEqual([123])
expect(fake.handle.disconnects).toBe(0)
fake.clearOnKill = true
await fiber.dispose()
expect(fake.groups).toEqual([])
expect(fake.handle.disconnects).toBe(1)
})
it('releases naturally settled terminals and validates terminal requests', async () => {
const { ctx, fiber, fake } = await service()
for (const request of [