190 lines
5.2 KiB
TypeScript
190 lines
5.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
|
|
const mocks = vi.hoisted(() => {
|
|
const connect = vi.fn()
|
|
const run = vi.fn()
|
|
const wait = vi.fn()
|
|
const sendStdin = vi.fn()
|
|
const closeStdin = vi.fn()
|
|
const kill = vi.fn()
|
|
const ensureAPIKey = vi.fn(() => 'test-api-key')
|
|
const isPipedStdin = vi.fn()
|
|
const streamStdinChunks = vi.fn()
|
|
const setupSignalHandlers = vi.fn(() => () => {})
|
|
|
|
return {
|
|
connect,
|
|
run,
|
|
wait,
|
|
sendStdin,
|
|
closeStdin,
|
|
kill,
|
|
ensureAPIKey,
|
|
isPipedStdin,
|
|
streamStdinChunks,
|
|
setupSignalHandlers,
|
|
}
|
|
})
|
|
|
|
vi.mock('e2b', () => {
|
|
class CommandExitError extends Error {
|
|
exitCode: number
|
|
constructor(exitCode: number) {
|
|
super(`Command exited with ${exitCode}`)
|
|
this.exitCode = exitCode
|
|
}
|
|
}
|
|
|
|
class NotFoundError extends Error {}
|
|
|
|
return {
|
|
Sandbox: {
|
|
connect: mocks.connect,
|
|
},
|
|
CommandExitError,
|
|
NotFoundError,
|
|
}
|
|
})
|
|
|
|
vi.mock('../../../src/api', () => ({
|
|
ensureAPIKey: mocks.ensureAPIKey,
|
|
}))
|
|
|
|
vi.mock('src/utils/signal', () => ({
|
|
setupSignalHandlers: mocks.setupSignalHandlers,
|
|
}))
|
|
|
|
vi.mock(
|
|
'../../../src/commands/sandbox/exec_helpers',
|
|
async (importOriginal: <T>() => Promise<T>) => {
|
|
const actual =
|
|
await importOriginal<
|
|
typeof import('../../../src/commands/sandbox/exec_helpers')
|
|
>()
|
|
return {
|
|
...actual,
|
|
isPipedStdin: mocks.isPipedStdin,
|
|
streamStdinChunks: mocks.streamStdinChunks,
|
|
}
|
|
}
|
|
)
|
|
|
|
describe('sandbox exec closeStdin handling', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
vi.clearAllMocks()
|
|
|
|
mocks.wait.mockResolvedValue({ exitCode: 0 })
|
|
const handle = {
|
|
pid: 1234,
|
|
error: undefined,
|
|
wait: mocks.wait,
|
|
kill: vi.fn().mockResolvedValue(undefined),
|
|
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
}
|
|
|
|
mocks.run.mockResolvedValue(handle)
|
|
mocks.sendStdin.mockResolvedValue(undefined)
|
|
mocks.closeStdin.mockResolvedValue(undefined)
|
|
mocks.kill.mockResolvedValue(true)
|
|
mocks.isPipedStdin.mockReturnValue(true)
|
|
mocks.streamStdinChunks.mockImplementation(
|
|
async (
|
|
_stream: NodeJS.ReadableStream,
|
|
onChunk: (chunk: Uint8Array) => Promise<void>,
|
|
_maxBytes: number
|
|
) => {
|
|
await onChunk(Buffer.from('hello'))
|
|
}
|
|
)
|
|
mocks.connect.mockResolvedValue({
|
|
commands: {
|
|
run: mocks.run,
|
|
sendStdin: mocks.sendStdin,
|
|
closeStdin: mocks.closeStdin,
|
|
kill: mocks.kill,
|
|
supportsStdinClose: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
test('fails fast and kills remote process when closeStdin throws non-NotFoundError', async () => {
|
|
mocks.closeStdin.mockRejectedValue(new Error('close failed'))
|
|
|
|
const exitSpy = vi
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined) as never)
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
const { execCommand } = await import('../../../src/commands/sandbox/exec')
|
|
await execCommand.parseAsync(['sandbox-id', 'cat'], {
|
|
from: 'user',
|
|
})
|
|
|
|
expect(mocks.closeStdin).toHaveBeenCalledTimes(1)
|
|
expect(mocks.kill).toHaveBeenCalledWith(1234)
|
|
expect(mocks.wait).not.toHaveBeenCalled()
|
|
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
})
|
|
|
|
test('keeps NotFoundError from closeStdin non-fatal', async () => {
|
|
const { NotFoundError } = await import('e2b')
|
|
mocks.closeStdin.mockRejectedValue(new NotFoundError('already exited'))
|
|
|
|
const exitSpy = vi
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined) as never)
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
const { execCommand } = await import('../../../src/commands/sandbox/exec')
|
|
await execCommand.parseAsync(['sandbox-id', 'cat'], {
|
|
from: 'user',
|
|
})
|
|
|
|
expect(mocks.closeStdin).toHaveBeenCalledTimes(1)
|
|
expect(mocks.kill).not.toHaveBeenCalled()
|
|
expect(mocks.wait).toHaveBeenCalledTimes(1)
|
|
expect(exitSpy).toHaveBeenCalledWith(0)
|
|
})
|
|
|
|
test('stops stdin streaming after NotFoundError from sendStdin', async () => {
|
|
const { NotFoundError } = await import('e2b')
|
|
mocks.sendStdin.mockRejectedValueOnce(new NotFoundError('already exited'))
|
|
mocks.streamStdinChunks.mockImplementation(
|
|
async (
|
|
_stream: NodeJS.ReadableStream,
|
|
onChunk: (chunk: Uint8Array) => Promise<void | boolean>,
|
|
_maxBytes: number
|
|
) => {
|
|
const shouldContinue = await onChunk(Buffer.from('first'))
|
|
if (shouldContinue === false) {
|
|
return
|
|
}
|
|
await onChunk(Buffer.from('second'))
|
|
}
|
|
)
|
|
|
|
const exitSpy = vi
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined) as never)
|
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
const { execCommand } = await import('../../../src/commands/sandbox/exec')
|
|
await execCommand.parseAsync(['sandbox-id', 'cat'], {
|
|
from: 'user',
|
|
})
|
|
|
|
expect(mocks.sendStdin).toHaveBeenCalledTimes(1)
|
|
expect(mocks.closeStdin).not.toHaveBeenCalled()
|
|
expect(mocks.wait).toHaveBeenCalledTimes(1)
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
'e2b: Remote command exited before stdin could be delivered.'
|
|
)
|
|
expect(exitSpy).toHaveBeenCalledWith(0)
|
|
})
|
|
})
|