chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user