refactor(e2b): group remote providers

This commit is contained in:
Tianyi Cui
2026-07-28 14:52:37 +08:00
parent 6667102890
commit e64d40837c
81 changed files with 171 additions and 249 deletions
+397
View File
@@ -0,0 +1,397 @@
import { PassThrough } from 'node:stream'
import { Context } from 'cordis'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
FileType,
type Sandbox,
} from '@deepseek-ai/dsh-e2b'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import E2BSubprocessService from '@deepseek-ai/dsh-subprocess-e2b'
const mockedLsp = vi.hoisted(() => {
interface Plan {
query?: (...args: unknown[]) => unknown
transportFailure?: unknown
dead?: boolean
deadAfterQuery?: boolean
disposeError?: unknown
}
class FakeLspInstance {
static readonly instances: FakeLspInstance[] = []
static readonly plans: Plan[] = []
readonly plan: Plan
readonly transport: unknown
readonly queries: unknown[][] = []
dead: boolean
disposals = 0
constructor(
readonly spec: Record<string, unknown>,
spawner: (spec: SubprocessSpawnSpec) => unknown,
) {
this.plan = FakeLspInstance.plans.shift() ?? {}
this.dead = this.plan.dead === true
this.transport = spawner({
argv: [String(spec.command), ...(spec.args as string[])],
cwd: String(spec.cwd),
env: spec.env as Record<string, string>,
graceMs: Number(spec.killGraceMs),
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: { maxBytes: Number(spec.maxStderrBytes) } },
})
FakeLspInstance.instances.push(this)
}
async query(...args: unknown[]): Promise<unknown> {
this.queries.push(args)
const result = await Promise.resolve(this.plan.query?.(...args) ?? { kind: 'hover', hover: null })
if (this.plan.deadAfterQuery === true) this.dead = true
return result
}
isTransportFailure(error: unknown): boolean {
return error === this.plan.transportFailure
}
async dispose(): Promise<void> {
this.disposals += 1
this.dead = true
if (this.plan.disposeError !== undefined) throw this.plan.disposeError
}
}
return { FakeLspInstance }
})
vi.mock('@deepseek-ai/dsh-lsp-local', () => ({ LspInstance: mockedLsp.FakeLspInstance }))
import {
E2BLspProvider,
apply,
canonicalizeE2BWorkspace,
readE2BSource,
} from '@deepseek-ai/dsh-lsp-e2b'
import type { LspE2BServerConfig } from '@deepseek-ai/dsh-lsp-e2b'
import * as E2BLspInvariant from '../src/invariant.ts'
import InvariantService from '@deepseek-ai/dsh-invariants'
class FakeInnerHandle implements SubprocessHandle {
readonly pid = 777
readonly stdin = new PassThrough()
readonly stdout = new PassThrough()
readonly stderr = undefined
readonly collected = { stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) } }
readonly done = Promise.resolve({ exitCode: 0, signal: null })
terminate(): void {}
async waitForExit(): Promise<boolean> { return true }
}
class FakeRemote {
readonly writes: Array<Array<{ path: string; data: string }>> = []
readonly commands: string[] = []
readonly infos = new Map<string, { type: FileType; size: number }>()
readonly contents = new Map<string, Uint8Array>()
readonly realpaths = new Map<string, string>()
forcedRealpath: string | undefined
constructor() {
this.infos.set('/workspace', { type: FileType.DIR, size: 0 })
this.infos.set('/workspace/file.ts', { type: FileType.FILE, size: 12 })
this.contents.set('/workspace/file.ts', Buffer.from('const x = 1'))
}
readonly sandbox = {
commands: {
run: async (command: string) => {
this.commands.push(command)
if (command.startsWith('realpath ')) {
const match = /'([^']*)'$/.exec(command)
const requested = match?.[1] ?? ''
return { exitCode: 0, stdout: `${this.forcedRealpath ?? this.realpaths.get(requested) ?? requested}\n`, stderr: '' }
}
if (command.startsWith('command -v')) return { exitCode: 0, stdout: '/usr/bin/node\n', stderr: '' }
return { exitCode: 0, stdout: '', stderr: '' }
},
},
files: {
write: async (files: Array<{ path: string; data: string }>) => {
this.writes.push(files)
return files.map(() => ({}))
},
getInfo: async (path: string) => {
const info = this.infos.get(path)
if (info === undefined) throw new Error(`missing info for ${path}`)
return info
},
read: async (path: string) => this.contents.get(path) ?? new Uint8Array(),
},
} as unknown as Sandbox
}
function subprocess(spawn = vi.fn((_spec: SubprocessSpawnSpec) => new FakeInnerHandle())): E2BSubprocessService {
const service = Object.create(E2BSubprocessService.prototype) as E2BSubprocessService
Object.defineProperty(service, 'spawn', { value: spawn })
return service
}
function server(overrides: Partial<LspE2BServerConfig> = {}): Required<LspE2BServerConfig> {
return {
command: '/usr/bin/server', args: ['--stdio'], env: {},
extensionToLanguage: { '.ts': 'typescript' },
initializationOptions: null, configuration: null,
maxMessageBytes: 1_024, maxStderrBytes: 128, maxDocumentBytes: 1_024,
shutdownTimeoutMs: 100, killGraceMs: 50,
...overrides,
}
}
function provider(remote = new FakeRemote(), service = subprocess()): E2BLspProvider {
return new E2BLspProvider(
'fixture', remote.sandbox, service, server(),
'/usr/bin/server', '/usr/bin/node', '/workspace/.dsh-e2b/lsp-proxy.mjs',
)
}
function query(workspaceRoot = '/workspace') {
return {
operation: 'hover' as const,
filePath: 'file.ts',
position: { line: 0, character: 1 },
workspaceRoot,
languageId: 'typescript',
}
}
beforeEach(() => {
mockedLsp.FakeLspInstance.instances.length = 0
mockedLsp.FakeLspInstance.plans.length = 0
})
describe('E2B LSP filesystem boundary', () => {
it('canonicalizes a directory and reads a contained UTF-8 source', async () => {
const remote = new FakeRemote()
await expect(canonicalizeE2BWorkspace(remote.sandbox, '/workspace')).resolves.toBe('/workspace')
await expect(readE2BSource(remote.sandbox, 'file.ts', '/workspace', 1_024)).resolves.toEqual({
canonicalPath: '/workspace/file.ts',
text: 'const x = 1',
})
await expect(readE2BSource(remote.sandbox, '/workspace/file.ts', '/workspace', 1_024)).resolves.toMatchObject({
canonicalPath: '/workspace/file.ts',
})
const signal = new AbortController().signal
await expect(canonicalizeE2BWorkspace(remote.sandbox, '/workspace', signal)).resolves.toBe('/workspace')
await expect(readE2BSource(remote.sandbox, 'file.ts', '/workspace', 1_024, signal)).resolves.toMatchObject({
canonicalPath: '/workspace/file.ts',
})
})
it('rejects malformed workspaces and source containment/type/size/encoding failures', async () => {
const malformed = new FakeRemote()
malformed.forcedRealpath = 'relative'
await expect(canonicalizeE2BWorkspace(malformed.sandbox, '/workspace')).rejects.toThrow('did not resolve canonically')
malformed.forcedRealpath = '/workspace\nother'
await expect(canonicalizeE2BWorkspace(malformed.sandbox, '/workspace')).rejects.toThrow('did not resolve canonically')
const notDirectory = new FakeRemote()
notDirectory.infos.set('/workspace', { type: FileType.FILE, size: 0 })
await expect(canonicalizeE2BWorkspace(notDirectory.sandbox, '/workspace')).rejects.toThrow('not a directory')
const outside = new FakeRemote()
outside.realpaths.set('/workspace/file.ts', '/outside/file.ts')
await expect(readE2BSource(outside.sandbox, 'file.ts', '/workspace', 20)).rejects.toThrow('outside the workspace')
const notFile = new FakeRemote()
notFile.infos.set('/workspace/file.ts', { type: FileType.DIR, size: 0 })
await expect(readE2BSource(notFile.sandbox, 'file.ts', '/workspace', 20)).rejects.toThrow('not a regular file')
const tooLarge = new FakeRemote()
tooLarge.infos.set('/workspace/file.ts', { type: FileType.FILE, size: 21 })
await expect(readE2BSource(tooLarge.sandbox, 'file.ts', '/workspace', 20)).rejects.toThrow('over the 20-byte limit')
const grew = new FakeRemote()
grew.infos.set('/workspace/file.ts', { type: FileType.FILE, size: 1 })
grew.contents.set('/workspace/file.ts', Buffer.alloc(21))
await expect(readE2BSource(grew.sandbox, 'file.ts', '/workspace', 20)).rejects.toThrow('grew past')
const invalid = new FakeRemote()
invalid.infos.set('/workspace/file.ts', { type: FileType.FILE, size: 1 })
invalid.contents.set('/workspace/file.ts', Uint8Array.from([0xff]))
await expect(readE2BSource(invalid.sandbox, 'file.ts', '/workspace', 20)).rejects.toThrow('not valid UTF-8')
await expect(canonicalizeE2BWorkspace(new FakeRemote().sandbox, '/workspace', AbortSignal.abort('stop')))
.rejects.toBe('stop')
})
})
describe('E2BLspProvider pooling and lifecycle', () => {
it('reuses one canonical-workspace instance and constructs the remote proxy transport', async () => {
const spawn = vi.fn((_spec: SubprocessSpawnSpec) => new FakeInnerHandle())
const remote = new FakeRemote()
mockedLsp.FakeLspInstance.plans.push({ query: async () => ({ kind: 'hover', hover: { contents: 'ok' } }) })
const current = provider(remote, subprocess(spawn))
await expect(current.query(query())).resolves.toEqual({ kind: 'hover', hover: { contents: 'ok' } })
await expect(current.query(query())).resolves.toEqual({ kind: 'hover', hover: { contents: 'ok' } })
expect(mockedLsp.FakeLspInstance.instances).toHaveLength(1)
expect(mockedLsp.FakeLspInstance.instances[0]?.spec).toMatchObject({ clientProcessId: null, cwd: '/workspace' })
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({
argv: ['/usr/bin/node', '/workspace/.dsh-e2b/lsp-proxy.mjs', expect.any(String)],
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: { maxBytes: 128 } },
}))
expect(current.id).toBe('fixture')
expect(current.extensionToLanguage).toEqual({ '.ts': 'typescript' })
await current.disposeAll()
expect(mockedLsp.FakeLspInstance.instances[0]?.disposals).toBe(1)
})
it('replaces one transport failure, but preserves ordinary query errors', async () => {
const transportFailure = new Error('transport failed')
mockedLsp.FakeLspInstance.plans.push(
{ transportFailure, query: async () => { throw transportFailure } },
{ query: async () => ({ kind: 'hover', hover: { contents: 'retried' } }) },
)
const retried = provider()
await expect(retried.query(query())).resolves.toMatchObject({ hover: { contents: 'retried' } })
expect(mockedLsp.FakeLspInstance.instances[0]?.disposals).toBe(1)
expect(mockedLsp.FakeLspInstance.instances).toHaveLength(2)
const ordinary = new Error('ordinary failure')
mockedLsp.FakeLspInstance.plans.push(
{ query: async () => { throw ordinary }, dead: true },
{ query: async () => ({ kind: 'hover', hover: null }) },
)
const failed = provider()
await expect(failed.query(query())).rejects.toBe(ordinary)
await expect(failed.query(query())).resolves.toMatchObject({ kind: 'hover' })
})
it('evicts a server that dies after a successful query', async () => {
mockedLsp.FakeLspInstance.plans.push(
{ deadAfterQuery: true, query: async () => ({ kind: 'hover', hover: null }) },
{ query: async () => ({ kind: 'hover', hover: null }) },
)
const current = provider()
await current.query(query())
await current.query(query())
expect(mockedLsp.FakeLspInstance.instances).toHaveLength(2)
expect(mockedLsp.FakeLspInstance.instances[0]?.disposals).toBe(1)
})
it('serializes a workspace queue, observes queued abort, and awaits work on disposal', async () => {
const first = Promise.withResolvers<unknown>()
mockedLsp.FakeLspInstance.plans.push({ query: () => first.promise })
const current = provider()
const running = current.query(query())
const controller = new AbortController()
const queued = current.query(query(), controller.signal)
await new Promise(resolve => setImmediate(resolve))
controller.abort('queued stop')
await expect(queued).rejects.toBe('queued stop')
const disposing = current.disposeAll()
first.resolve({ kind: 'hover', hover: null })
await expect(running).resolves.toMatchObject({ kind: 'hover' })
await disposing
await expect(current.query(query())).rejects.toMatchObject({ code: 'LSP_DISPOSED' })
})
it('covers pre-abort, synthetic abort, resolve, and rejection in the queue race', async () => {
const current = provider()
const internal = current as unknown as {
queues: Map<string, Promise<void>>
enqueue<T>(workspace: string, signal: AbortSignal | undefined, run: () => Promise<T>): Promise<T>
}
await expect(internal.enqueue('pre', AbortSignal.abort('pre-stop'), async () => 'unused')).rejects.toBe('pre-stop')
const signal = new AbortController().signal
await expect(internal.enqueue('resolve', signal, async () => 'ok')).resolves.toBe('ok')
const failure = new Error('queue failed')
const rejected = Promise.reject<undefined>(failure)
void rejected.catch(() => {})
internal.queues.set('reject', rejected)
await expect(internal.enqueue('reject', signal, async () => 'unused')).rejects.toBe(failure)
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- Exercise normalization at the promise boundary.
const opaque = Promise.reject<undefined>('opaque queue failure')
void opaque.catch(() => {})
internal.queues.set('opaque', opaque)
await expect(internal.enqueue('opaque', signal, async () => 'unused')).rejects.toEqual(new Error('opaque queue failure'))
const synthetic = {
aborted: false,
reason: undefined,
throwIfAborted() {},
addEventListener(_type: string, listener: () => void) { listener() },
removeEventListener() {},
} as unknown as AbortSignal
await expect(internal.enqueue('synthetic', synthetic, async () => 'unused')).rejects.toMatchObject({ name: 'AbortError' })
await current.disposeAll()
})
})
describe('lsp-e2b plugin composition', () => {
function pluginContext(
remote: FakeRemote,
service: E2BSubprocessService,
registerProvider = vi.fn(() => vi.fn()),
) {
const effects: Array<() => void | Promise<void>> = []
const ctx = {
subprocess: service,
e2b: {
runtimeRoot: '/workspace/.dsh-e2b',
getSandbox: async () => remote.sandbox,
},
lsp: { registerProvider },
effect: (callback: () => (() => void | Promise<void>)) => { effects.push(callback()) },
} as unknown as Context
return { ctx, effects, registerProvider }
}
it('installs one proxy, resolves commands, registers providers, and disposes them', async () => {
const remote = new FakeRemote()
const fixture = pluginContext(remote, subprocess())
await apply(fixture.ctx, { servers: { one: server(), two: server({ command: 'server-two' }) } })
expect(remote.writes).toHaveLength(1)
expect(remote.writes[0]?.[0]?.path).toBe('/workspace/.dsh-e2b/lsp-stdio-proxy.mjs')
expect(remote.commands).toContain("chmod 600 -- '/workspace/.dsh-e2b/lsp-stdio-proxy.mjs'")
expect(fixture.registerProvider).toHaveBeenCalledTimes(2)
await fixture.effects[0]?.()
})
it('rolls back partial registration and rejects invalid composition/configuration', async () => {
const remote = new FakeRemote()
const firstDispose = vi.fn()
const register = vi.fn()
.mockReturnValueOnce(firstDispose)
.mockImplementationOnce(() => { throw new Error('duplicate provider') })
const rollback = pluginContext(remote, subprocess(), register)
await expect(apply(rollback.ctx, { servers: { one: server(), two: server() } })).rejects.toThrow('duplicate provider')
expect(firstDispose).toHaveBeenCalledOnce()
const wrong = pluginContext(remote, {} as E2BSubprocessService)
await expect(apply(wrong.ctx, { servers: { one: server() } })).rejects.toThrow('dsh-subprocess-e2b')
const empty = pluginContext(remote, subprocess())
await expect(apply(empty.ctx, { servers: {} })).rejects.toThrow('at least one server')
for (const [id, config] of [
['', server()],
['one', server({ command: '' })],
['one', server({ maxMessageBytes: 0 })],
['one', server({ maxStderrBytes: 1.5 })],
['one', server({ shutdownTimeoutMs: 2_147_483_648 })],
] as const) {
const fixture = pluginContext(new FakeRemote(), subprocess())
await expect(apply(fixture.ctx, { servers: { [id]: config } })).rejects.toThrow()
}
})
it('registers the package-owned invariant companion', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
const fiber = await ctx.plugin(E2BLspInvariant).await()
await fiber.dispose()
})
})
@@ -0,0 +1,181 @@
import { once } from 'node:events'
import { PassThrough } from 'node:stream'
import { describe, expect, it } from 'vitest'
import { encodeE2BFrame } from '@deepseek-ai/dsh-e2b'
import type { SubprocessHandle, SubprocessOutcome } from '@deepseek-ai/dsh-subprocess'
import { E2BLspTransport } from '@deepseek-ai/dsh-lsp-e2b'
class FakeHandle implements SubprocessHandle {
readonly pid = 321
readonly stdin: PassThrough | undefined
readonly stdout: PassThrough | undefined
readonly stderr = undefined
readonly collected: SubprocessHandle['collected']
readonly result = Promise.withResolvers<SubprocessOutcome>()
readonly done = this.result.promise
terminated = 0
waitResult = true
constructor(options: { stdin?: boolean; stdout?: boolean; diagnostic?: string } = {}) {
this.stdin = options.stdin === false ? undefined : new PassThrough()
this.stdout = options.stdout === false ? undefined : new PassThrough()
this.collected = options.diagnostic === undefined
? {}
: { stderr: { readFrom: () => ({ text: options.diagnostic as string, nextOffset: 0, lossy: false }) } }
}
terminate(): void {
this.terminated += 1
}
async waitForExit(): Promise<boolean> {
return this.waitResult
}
resolve(outcome: SubprocessOutcome = { exitCode: 0, signal: null }): void {
this.stdout?.end()
this.result.resolve(outcome)
}
reject(error: unknown): void {
this.stdout?.end()
this.result.reject(error)
}
}
function proxyFrame(type: 'stdout' | 'stderr', data: string | Buffer): string {
return encodeE2BFrame({ type, data: Buffer.from(data).toString('base64') })
}
describe('E2BLspTransport', () => {
it('frames stdin, decodes split byte output, and exposes handle lifecycle', async () => {
const inner = new FakeHandle({ diagnostic: 'proxy diagnostic' })
const transport = new E2BLspTransport(inner, 1_024, 64)
let stdin = ''
inner.stdin?.on('data', (chunk) => { stdin += String(chunk) })
let stdout = Buffer.alloc(0)
transport.stdout.on('data', (chunk) => { stdout = Buffer.concat([stdout, chunk]) })
transport.stdin.write(Buffer.from([0, 0xff]))
await new Promise(resolve => setImmediate(resolve))
const encodedInput = stdin.trim()
const input = JSON.parse(Buffer.from(encodedInput, 'base64').toString('utf8')) as Record<string, string>
expect(input).toEqual({ type: 'stdin', data: 'AP8=' })
const frames = proxyFrame('stdout', Buffer.from([0, 0xff]))
+ proxyFrame('stderr', 'server diagnostic')
+ encodeE2BFrame({ type: 'exit', code: 0, signal: null })
inner.stdout?.write(frames.slice(0, 7))
inner.stdout?.write(frames.slice(7))
inner.stdout?.write(proxyFrame('stderr', ''))
inner.stdout?.write(encodeE2BFrame({ type: 'exit', code: null, signal: 'SIGTERM' }))
inner.resolve()
await expect(transport.done).resolves.toEqual({ exitCode: 0, signal: null })
expect(stdout).toEqual(Buffer.from([0, 0xff]))
expect(transport.collected.stderr?.readFrom(0).text).toBe('server diagnosticproxy diagnostic')
expect(transport.pid).toBe(321)
inner.waitResult = false
await expect(transport.waitForExit()).resolves.toBe(false)
transport.terminate()
expect(inner.terminated).toBe(1)
})
it('ends the inner stdin and retains a bounded byte tail with independent offsets', async () => {
const inner = new FakeHandle()
const transport = new E2BLspTransport(inner, 1_024, 4)
const finished = once(inner.stdin!, 'finish')
transport.stdin.end()
await finished
inner.stdout?.write(proxyFrame('stderr', 'ab'))
inner.stdout?.write(proxyFrame('stderr', 'cdef'))
const reader = transport.collected.stderr!
expect(reader.readFrom(0)).toEqual({ text: 'cdef', nextOffset: 6, lossy: true })
expect(reader.readFrom(2)).toEqual({ text: 'cdef', nextOffset: 6, lossy: false })
expect(reader.readFrom(5)).toEqual({ text: 'f', nextOffset: 6, lossy: false })
expect(reader.readFrom(99)).toEqual({ text: '', nextOffset: 6, lossy: false })
expect(() => reader.readFrom(-1)).toThrow('non-negative safe integer')
expect(() => reader.readFrom(1.5)).toThrow('non-negative safe integer')
inner.resolve()
await transport.done
const partialInner = new FakeHandle()
const partial = new E2BLspTransport(partialInner, 1_024, 4)
partialInner.stdout?.write(proxyFrame('stderr', 'abcdef'))
expect(partial.collected.stderr?.readFrom(0)).toEqual({ text: 'cdef', nextOffset: 6, lossy: true })
partialInner.resolve()
await partial.done
})
it.each([
['non-object', encodeE2BFrame(null), 'malformed frame'],
['wrong type', encodeE2BFrame({ type: 'other', data: '' }), 'malformed frame'],
['invalid exit', encodeE2BFrame({ type: 'exit', code: 'zero', signal: null }), 'malformed frame'],
['missing data', encodeE2BFrame({ type: 'stdout' }), 'malformed frame'],
['invalid base64', encodeE2BFrame({ type: 'stdout', data: 'abc' }), 'invalid base64'],
['invalid outer frame', 'not-base64\n', 'invalid base64'],
['non-ASCII outer frame', 'é', 'non-ASCII'],
])('fails %s proxy traffic and terminates the group', async (_name, frame, message) => {
const inner = new FakeHandle()
const transport = new E2BLspTransport(inner, 1_024, 32)
inner.stdout?.write(frame)
inner.stdout?.write(proxyFrame('stdout', 'ignored'))
inner.stdout?.emit('error', new Error('second failure'))
inner.resolve()
await expect(transport.done).rejects.toThrow(message)
expect(inner.terminated).toBe(1)
})
it('fails truncated frames and preserves inner spawn rejection', async () => {
const truncatedInner = new FakeHandle()
const truncated = new E2BLspTransport(truncatedInner, 1_024, 32)
truncatedInner.stdout?.write('YQ==')
truncatedInner.resolve()
await expect(truncated.done).rejects.toThrow('mid-frame')
const rejectedInner = new FakeHandle({ diagnostic: 'tail' })
const rejected = new E2BLspTransport(rejectedInner, 1_024, 32)
rejectedInner.reject(new Error('spawn failed'))
await expect(rejected.done).rejects.toThrow('spawn failed')
expect(rejected.collected.stderr?.readFrom(0).text).toBe('tail')
})
it('forwards output and input stream errors without an unhandled inner error', async () => {
const outputInner = new FakeHandle()
const output = new E2BLspTransport(outputInner, 1_024, 32)
outputInner.stdout?.emit('error', new Error('proxy stdout failed'))
outputInner.resolve()
await expect(output.done).rejects.toThrow('proxy stdout failed')
const inputInner = new FakeHandle()
const input = new E2BLspTransport(inputInner, 1_024, 32)
const outerError = once(input.stdin, 'error')
inputInner.stdin?.emit('error', new Error('proxy stdin failed'))
await expect(outerError).resolves.toMatchObject([{ message: 'proxy stdin failed' }])
inputInner.resolve()
await input.done
})
it('normalizes a non-Error decoder throw', async () => {
const inner = new FakeHandle()
const transport = new E2BLspTransport(inner, 1_024, 32)
const internal = transport as unknown as {
decoder: { push(chunk: string): unknown[] }
onProxyData(chunk: Buffer): void
}
internal.decoder = { push: () => { throw 'raw decoder failure' } }
internal.onProxyData(Buffer.from('x'))
inner.resolve()
await expect(transport.done).rejects.toThrow('raw decoder failure')
})
it('rejects a subprocess that drops either required pipe', () => {
const missingStdin = new FakeHandle({ stdin: false })
const missingStdout = new FakeHandle({ stdout: false })
expect(() => new E2BLspTransport(missingStdin, 10, 10)).toThrow('dropped a piped stream')
expect(() => new E2BLspTransport(missingStdout, 10, 10)).toThrow('dropped a piped stream')
expect(missingStdin.terminated).toBe(1)
expect(missingStdout.terminated).toBe(1)
})
})