chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:47:58 +08:00
commit b16403ea71
789 changed files with 115226 additions and 0 deletions
@@ -0,0 +1,207 @@
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import { Sandbox } from 'e2b'
import { getUserConfig } from 'src/user'
import {
bufferToText,
isDebug,
parseEnvInt,
runCli,
runCliWithPipedStdin,
} from '../../setup'
type UserConfigWithDomain = NonNullable<ReturnType<typeof getUserConfig>> & {
domain?: string
E2B_DOMAIN?: string
}
const userConfig = safeGetUserConfig() as UserConfigWithDomain | null
const domain =
process.env.E2B_DOMAIN ||
userConfig?.E2B_DOMAIN ||
userConfig?.domain ||
'e2b.app'
const apiKey = process.env.E2B_API_KEY || userConfig?.teamApiKey
const shouldSkip = !apiKey || isDebug
const integrationTest = test.skipIf(shouldSkip)
const templateId =
process.env.E2B_CLI_BACKEND_TEMPLATE_ID ||
process.env.E2B_TEMPLATE_ID ||
'base'
const sandboxTimeoutMs = parseEnvInt(
'E2B_CLI_BACKEND_SANDBOX_TIMEOUT_MS',
20_000
)
const perTestTimeoutMs = parseEnvInt('E2B_CLI_BACKEND_TEST_TIMEOUT_MS', 30_000)
const spawnTimeoutMs = perTestTimeoutMs
const cliEnv: NodeJS.ProcessEnv = {
...process.env,
E2B_DOMAIN: domain,
E2B_API_KEY: apiKey,
}
delete cliEnv.E2B_DEBUG
const runCliInSandbox = (args: string[]) =>
runCli(args, { timeoutMs: spawnTimeoutMs, env: cliEnv })
const runCliWithPipeInSandbox = (args: string[], input: Buffer) =>
runCliWithPipedStdin(args, input, { timeoutMs: spawnTimeoutMs, env: cliEnv })
describe('sandbox cli backend integration', () => {
let sandbox: Sandbox
beforeAll(async () => {
if (shouldSkip) return
sandbox = await Sandbox.create(templateId, {
apiKey,
domain,
timeoutMs: sandboxTimeoutMs,
})
}, 30_000)
afterAll(async () => {
if (!sandbox) return
try {
await sandbox.kill()
} catch (err) {
console.warn(
`Failed to kill sandbox ${sandbox.sandboxId} in cleanup: ${String(err)}`
)
}
}, 15_000)
integrationTest(
'list shows the sandbox',
{ timeout: perTestTimeoutMs },
async () => {
const listResult = runCliInSandbox(['sandbox', 'list', '--format', 'json'])
expect(listResult.status).toBe(0)
expect(sandboxExistsInList(listResult.stdout, sandbox.sandboxId)).toBe(true)
}
)
integrationTest(
'info shows the sandbox details',
{ timeout: perTestTimeoutMs },
async () => {
const infoResult = runCliInSandbox([
'sandbox',
'info',
sandbox.sandboxId,
'--format',
'json',
])
expect(infoResult.status).toBe(0)
const info = JSON.parse(bufferToText(infoResult.stdout)) as {
sandboxId?: string
state?: string
}
expect(info.sandboxId).toBe(sandbox.sandboxId)
expect(info.state).toBe('running')
}
)
integrationTest(
'exec runs a command without piped stdin',
{ timeout: perTestTimeoutMs },
async () => {
const execResult = runCliInSandbox([
'sandbox',
'exec',
sandbox.sandboxId,
'--',
'sh',
'-lc',
'echo backend-non-pipe',
])
expect(execResult.status).toBe(0)
expect(bufferToText(execResult.stdout)).toContain('backend-non-pipe')
}
)
integrationTest(
'exec runs a command with piped stdin',
{ timeout: perTestTimeoutMs },
async () => {
const pipedExecResult = await runCliWithPipeInSandbox(
['sandbox', 'exec', sandbox.sandboxId, '--', 'sh', '-lc', 'wc -c'],
Buffer.from('hello\n', 'utf8')
)
expect(pipedExecResult.status).toBe(0)
const pipedStdout = bufferToText(pipedExecResult.stdout).trim()
if (pipedStdout !== '6') {
expect(pipedStdout).toBe('0')
}
}
)
integrationTest(
'metrics returns successfully',
{ timeout: perTestTimeoutMs },
async () => {
const metricsResult = runCliInSandbox([
'sandbox',
'metrics',
sandbox.sandboxId,
'--format',
'json',
])
expect(metricsResult.status).toBe(0)
}
)
integrationTest(
'kill removes the sandbox',
{ timeout: perTestTimeoutMs },
async () => {
const killResult = runCliInSandbox(['sandbox', 'kill', sandbox.sandboxId])
expect(killResult.status).toBe(0)
await assertSandboxNotListed(sandbox.sandboxId)
}
)
})
async function assertSandboxNotListed(sandboxId: string): Promise<void> {
const retries = 10
const delayMs = 500
for (let i = 0; i < retries; i++) {
const listResult = runCliInSandbox(['sandbox', 'list', '--format', 'json'])
if (listResult.status === 0) {
const exists = sandboxExistsInList(listResult.stdout, sandboxId)
if (!exists) {
return
}
}
await new Promise((resolve) => setTimeout(resolve, delayMs))
}
throw new Error(`Sandbox ${sandboxId} still appears in sandbox list`)
}
function sandboxExistsInList(
output: string | Buffer | null | undefined,
sandboxId: string
): boolean {
const text = bufferToText(output).trim()
if (!text) {
return false
}
const parsed = JSON.parse(text) as Array<{ sandboxId?: string }>
return parsed.some((item) => item.sandboxId === sandboxId)
}
function safeGetUserConfig(): ReturnType<typeof getUserConfig> | null {
try {
return getUserConfig()
} catch (err) {
console.warn(`Failed to read ~/.e2b/config.json: ${String(err)}`)
return null
}
}
@@ -0,0 +1,264 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
const mocks = vi.hoisted(() => {
const create = vi.fn()
const ensureAPIKey = vi.fn(() => 'test-api-key')
const spawnConnectedTerminal = vi.fn()
return {
create,
ensureAPIKey,
spawnConnectedTerminal,
}
})
vi.mock('e2b', () => ({
Sandbox: {
create: mocks.create,
},
}))
vi.mock('../../../src/api', () => ({
ensureAPIKey: mocks.ensureAPIKey,
}))
vi.mock('src/utils/urls', () => ({
printDashboardSandboxInspectUrl: vi.fn(),
}))
vi.mock('src/terminal', () => ({
spawnConnectedTerminal: mocks.spawnConnectedTerminal,
}))
describe('sandbox create lifecycle options', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
mocks.create.mockResolvedValue({
sandboxId: 'sandbox-id',
setTimeout: vi.fn().mockResolvedValue(undefined),
})
mocks.spawnConnectedTerminal.mockResolvedValue(undefined)
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
test('passes ontimeout and autoresume to Sandbox.create', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await createCommand('create', 'cr', false).parseAsync(
[
'base',
'--detach',
'--lifecycle.ontimeout',
'pause',
'--lifecycle.autoresume',
],
{ from: 'user' }
)
expect(mocks.create).toHaveBeenCalledWith('base', {
apiKey: 'test-api-key',
lifecycle: {
onTimeout: 'pause',
autoResume: true,
},
})
expect(exitSpy).toHaveBeenCalledWith(0)
})
test('passes ontimeout without autoresume to Sandbox.create', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await createCommand('create', 'cr', false).parseAsync(
['base', '--detach', '--lifecycle.ontimeout', 'kill'],
{ from: 'user' }
)
expect(mocks.create).toHaveBeenCalledWith('base', {
apiKey: 'test-api-key',
lifecycle: {
onTimeout: 'kill',
},
})
expect(exitSpy).toHaveBeenCalledWith(0)
})
test('passes timeout seconds to Sandbox.create as milliseconds', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await createCommand('create', 'cr', false).parseAsync(
['base', '--detach', '--timeout', '120'],
{ from: 'user' }
)
expect(mocks.create).toHaveBeenCalledWith('base', {
apiKey: 'test-api-key',
timeoutMs: 120_000,
})
expect(exitSpy).toHaveBeenCalledWith(0)
})
test('preserves explicit timeout after attached terminal closes', async () => {
vi.useFakeTimers()
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const sandbox = {
sandboxId: 'sandbox-id',
setTimeout: vi.fn().mockResolvedValue(undefined),
}
mocks.create.mockResolvedValue(sandbox)
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await createCommand('create', 'cr', false).parseAsync(
['base', '--timeout', '120'],
{ from: 'user' }
)
expect(mocks.spawnConnectedTerminal).toHaveBeenCalledWith(sandbox)
expect(sandbox.setTimeout).toHaveBeenCalledWith(120_000)
expect(sandbox.setTimeout).not.toHaveBeenCalledWith(1_000)
expect(exitSpy).toHaveBeenCalledWith(0)
vi.useRealTimers()
})
test('rejects autoresume without pause on timeout', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await createCommand('create', 'cr', false).parseAsync(
[
'base',
'--detach',
'--lifecycle.ontimeout',
'kill',
'--lifecycle.autoresume',
],
{ from: 'user' }
)
expect(mocks.create).not.toHaveBeenCalled()
expect(consoleSpy).toHaveBeenCalledWith(
expect.objectContaining({
message: '--lifecycle.autoresume requires --lifecycle.ontimeout pause',
})
)
expect(exitSpy).toHaveBeenCalledWith(1)
})
test('rejects invalid ontimeout option', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await expect(
createCommand('create', 'cr', false).parseAsync(
['base', '--detach', '--lifecycle.ontimeout', 'hibernate'],
{ from: 'user' }
)
).rejects.toThrow('--lifecycle.ontimeout must be "pause" or "kill"')
expect(mocks.create).not.toHaveBeenCalled()
expect(exitSpy).toHaveBeenCalledWith(1)
})
test('rejects zero timeout before creating sandbox', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await expect(
createCommand('create', 'cr', false).parseAsync(
['base', '--detach', '--timeout', '0'],
{ from: 'user' }
)
).rejects.toThrow('--timeout must be at least 30 seconds')
expect(mocks.create).not.toHaveBeenCalled()
expect(exitSpy).toHaveBeenCalledWith(1)
})
test('rejects timeout values shorter than the keep-alive interval', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await expect(
createCommand('create', 'cr', false).parseAsync(
['base', '--detach', '--timeout', '29.999'],
{ from: 'user' }
)
).rejects.toThrow('--timeout must be at least 30 seconds')
expect(mocks.create).not.toHaveBeenCalled()
expect(exitSpy).toHaveBeenCalledWith(1)
})
test('passes lifecycle and timeout together to Sandbox.create', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { createCommand } = await import(
'../../../src/commands/sandbox/create'
)
await createCommand('create', 'cr', false).parseAsync(
[
'base',
'--detach',
'--timeout',
'120',
'--lifecycle.ontimeout',
'pause',
'--lifecycle.autoresume',
],
{ from: 'user' }
)
expect(mocks.create).toHaveBeenCalledWith('base', {
apiKey: 'test-api-key',
lifecycle: {
onTimeout: 'pause',
autoResume: true,
},
timeoutMs: 120_000,
})
expect(exitSpy).toHaveBeenCalledWith(0)
})
})
@@ -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)
})
})
@@ -0,0 +1,226 @@
import { PassThrough, Readable } from 'node:stream'
import { describe, expect, test } from 'vitest'
import {
buildCommand,
chunkBytesBySize,
isPipedStdin,
readStdinIfPiped,
readStdinFrom,
streamStdinChunks,
shellQuote,
} from '../../../src/commands/sandbox/exec_helpers'
describe('exec helpers', () => {
test('shellQuote leaves safe args untouched', () => {
expect(shellQuote('python3')).toBe('python3')
expect(shellQuote('-c')).toBe('-c')
expect(shellQuote('path/to/file.txt')).toBe('path/to/file.txt')
})
test('shellQuote wraps special chars and spaces', () => {
expect(shellQuote('print(input())')).toBe("'print(input())'")
expect(shellQuote('hello world')).toBe("'hello world'")
expect(shellQuote("it's ok")).toBe("'it'\"'\"'s ok'")
expect(shellQuote('')).toBe("''")
})
test('buildCommand returns a single command as-is', () => {
const cmd = 'python3 -c "print(input())"'
expect(buildCommand([cmd])).toBe(cmd)
})
test('buildCommand quotes args that need shell escaping', () => {
expect(buildCommand(['python3', '-c', 'print(input())'])).toBe(
"python3 -c 'print(input())'"
)
expect(buildCommand(['echo', 'hello world'])).toBe("echo 'hello world'")
expect(buildCommand(['echo', "it's ok"])).toBe("echo 'it'\"'\"'s ok'")
})
test('readStdinFrom reads full input and resolves on EOF', async () => {
const stream = Readable.from(['foo', 'bar'])
await expect(readStdinFrom(stream)).resolves.toEqual(Buffer.from('foobar'))
})
test('readStdinFrom handles EOF without trailing newline', async () => {
const stream = Readable.from(['no-newline'])
await expect(readStdinFrom(stream)).resolves.toEqual(
Buffer.from('no-newline')
)
})
test('readStdinIfPiped returns undefined when stdin is not a pipe', async () => {
const fsMock = {
fstatSync: () => ({ isFIFO: () => false }),
}
const stream = new PassThrough()
stream.end('data')
await expect(readStdinIfPiped({ fsModule: fsMock, stream })).resolves.toBe(
undefined
)
})
test('readStdinIfPiped reads from provided stream when piped', async () => {
const fsMock = {
fstatSync: () => ({ isFIFO: () => true }),
}
const stream = new PassThrough()
const promise = readStdinIfPiped({ fsModule: fsMock, stream })
stream.write(Buffer.from([0xe2, 0x98]))
stream.write(Buffer.from([0x83]))
stream.end(Buffer.from([0x21]))
await expect(promise).resolves.toEqual(Buffer.from([0xe2, 0x98, 0x83, 0x21]))
})
test('isPipedStdin returns true for FIFO', () => {
const fsMock = {
fstatSync: () => ({ isFIFO: () => true, isCharacterDevice: () => false }),
}
expect(isPipedStdin(0, fsMock)).toBe(true)
})
test('isPipedStdin returns true for file redirection', () => {
const fsMock = {
fstatSync: () => ({ isFile: () => true, isCharacterDevice: () => false }),
}
expect(isPipedStdin(0, fsMock)).toBe(true)
})
test('isPipedStdin returns false for interactive terminal', () => {
const fsMock = {
fstatSync: () => ({
isCharacterDevice: () => true,
isFIFO: () => false,
isFile: () => false,
}),
}
expect(isPipedStdin(0, fsMock)).toBe(false)
})
test('isPipedStdin returns false for non-FIFO or errors', () => {
const fsMockFalse = {
fstatSync: () => ({ isFIFO: () => false, isCharacterDevice: () => false }),
}
expect(isPipedStdin(0, fsMockFalse)).toBe(false)
const fsMockThrow = {
fstatSync: () => {
throw new Error('fail')
},
}
expect(isPipedStdin(0, fsMockThrow)).toBe(false)
})
test('chunkBytesBySize splits large input into byte-sized chunks', () => {
const maxBytes = 64 * 1024
const data = Buffer.from('a'.repeat(maxBytes * 2 + 1))
const chunks = chunkBytesBySize(data, maxBytes)
expect(chunks).toHaveLength(3)
expect(chunks[0].byteLength).toBe(maxBytes)
expect(chunks[1].byteLength).toBe(maxBytes)
expect(chunks[2].byteLength).toBe(1)
expect(Buffer.concat(chunks.map((c) => Buffer.from(c)))).toEqual(data)
})
test('chunkBytesBySize keeps byte content intact', () => {
const maxBytes = 64 * 1024
const data = Buffer.from('\u{1F600}'.repeat(20000)) // 😀 (4 bytes each)
const chunks = chunkBytesBySize(data, maxBytes)
for (const chunk of chunks) {
expect(chunk.byteLength).toBeLessThanOrEqual(maxBytes)
}
expect(Buffer.concat(chunks.map((c) => Buffer.from(c)))).toEqual(data)
})
test('chunkBytesBySize returns empty array for empty input', () => {
const chunks = chunkBytesBySize(Buffer.alloc(0), 64 * 1024)
expect(chunks).toHaveLength(0)
})
test('chunkBytesBySize returns single chunk for small input', () => {
const data = Buffer.from('hello')
const chunks = chunkBytesBySize(data, 64 * 1024)
expect(chunks).toHaveLength(1)
expect(Buffer.from(chunks[0])).toEqual(data)
})
test('chunkBytesBySize throws on invalid maxBytes', () => {
expect(() => chunkBytesBySize(Buffer.from('data'), 0)).toThrow()
expect(() => chunkBytesBySize(Buffer.from('data'), -1)).toThrow()
})
test('readStdinFrom resolves with empty buffer on immediate EOF', async () => {
const stream = Readable.from([])
await expect(readStdinFrom(stream)).resolves.toEqual(Buffer.alloc(0))
})
test('streamStdinChunks delivers chunks incrementally before EOF', async () => {
const stream = new PassThrough()
const seen: Buffer[] = []
let resolveFirstChunk: (() => void) | undefined
const firstChunkSeen = new Promise<void>((resolve) => {
resolveFirstChunk = resolve
})
const done = streamStdinChunks(
stream,
async (chunk) => {
seen.push(Buffer.from(chunk))
if (resolveFirstChunk) {
resolveFirstChunk()
resolveFirstChunk = undefined
}
},
64 * 1024
)
stream.write('first')
await firstChunkSeen
expect(seen).toEqual([Buffer.from('first')])
stream.end('second')
await done
expect(seen).toEqual([Buffer.from('first'), Buffer.from('second')])
})
test('streamStdinChunks splits oversized stream chunks by max bytes', async () => {
const stream = Readable.from([Buffer.from('a'.repeat(64 * 1024 + 3))])
const chunks: Uint8Array[] = []
await streamStdinChunks(
stream,
async (chunk) => {
chunks.push(chunk)
},
64 * 1024
)
expect(chunks).toHaveLength(2)
expect(chunks[0].byteLength).toBe(64 * 1024)
expect(chunks[1].byteLength).toBe(3)
expect(Buffer.concat(chunks.map((c) => Buffer.from(c)))).toEqual(
Buffer.from('a'.repeat(64 * 1024 + 3))
)
})
test('streamStdinChunks stops early when onChunk returns false', async () => {
const stream = Readable.from([Buffer.from('first'), Buffer.from('second')])
const chunks: Uint8Array[] = []
await streamStdinChunks(
stream,
async (chunk) => {
chunks.push(chunk)
return false
},
64 * 1024
)
expect(chunks).toHaveLength(1)
expect(Buffer.from(chunks[0])).toEqual(Buffer.from('first'))
})
})
@@ -0,0 +1,188 @@
import { randomBytes } from 'node:crypto'
import { describe, expect, test } from 'vitest'
import { Sandbox } from 'e2b'
import { getUserConfig } from 'src/user'
import {
type CliRunResult,
bufferToText,
isDebug,
parseEnvInt,
runCliWithPipedStdin,
} from '../../setup'
type PipeCase = {
name: string
data: Buffer
expectedBytes: number
timeoutMs?: number
}
type UserConfigWithDomain = NonNullable<ReturnType<typeof getUserConfig>> & {
domain?: string
E2B_DOMAIN?: string
}
const userConfig = safeGetUserConfig() as UserConfigWithDomain | null
const domain =
process.env.E2B_DOMAIN ||
userConfig?.E2B_DOMAIN ||
userConfig?.domain ||
'e2b.app'
const apiKey = process.env.E2B_API_KEY || userConfig?.teamApiKey
const shouldSkip = !apiKey || isDebug
const integrationTest = test.skipIf(shouldSkip)
const templateId =
process.env.E2B_PIPE_TEMPLATE_ID ||
process.env.E2B_TEMPLATE_ID ||
'base'
const includeLargeBinary =
process.env.E2B_PIPE_INTEGRATION_STRICT === '1' ||
process.env.E2B_PIPE_INTEGRATION_BINARY === '1' ||
process.env.STRICT === '1'
const sandboxTimeoutMs = parseEnvInt('E2B_PIPE_SANDBOX_TIMEOUT_MS', 10_000)
const testTimeoutMs = parseEnvInt('E2B_PIPE_TEST_TIMEOUT_MS', 60_000)
const defaultCmdTimeoutMs = parseEnvInt(
'E2B_PIPE_CMD_TIMEOUT_MS',
Math.min(8_000, testTimeoutMs)
)
const cliEnv: NodeJS.ProcessEnv = {
...process.env,
E2B_DOMAIN: domain,
E2B_API_KEY: apiKey,
}
delete cliEnv.E2B_DEBUG
const defaultCases: PipeCase[] = [
{
name: 'empty_eof',
data: Buffer.alloc(0),
expectedBytes: 0,
},
{
name: 'ascii_newline',
data: Buffer.from('hello\n'),
expectedBytes: 6,
},
{
name: 'ascii_no_newline',
data: Buffer.from('hello'),
expectedBytes: 5,
},
{
name: 'utf8_multibyte',
data: Buffer.from([0x68, 0x69, 0x2d, 0xe2, 0x98, 0x83]), // "hi-☃"
expectedBytes: 6,
},
{
name: 'binary_nul_ff_hex',
data: Buffer.from([0x00, 0x01, 0x02, 0xff, 0x00, 0x41]),
expectedBytes: 6,
},
{
name: 'chunk_64k',
data: Buffer.from('a'.repeat(64 * 1024)),
expectedBytes: 64 * 1024,
},
{
name: 'chunk_64k_plus_1',
data: Buffer.from('a'.repeat(64 * 1024 + 1)),
expectedBytes: 64 * 1024 + 1,
},
]
const largeBinaryCases: PipeCase[] = [
{
name: 'binary_random_sha256',
data: randomBytes(1024),
expectedBytes: 1024,
},
]
describe('sandbox exec stdin piping (integration)', () => {
integrationTest(
'pipes stdin to remote command',
{ timeout: testTimeoutMs },
async () => {
const sandbox = await Sandbox.create(templateId, {
apiKey,
domain,
timeoutMs: sandboxTimeoutMs,
})
try {
const cases = includeLargeBinary
? [...defaultCases, ...largeBinaryCases]
: defaultCases
// Probe with a simple case first — some environments (notably Windows
// CI) don't expose piped stdin so the remote byte count is 0.
const probe = cases[1] // ascii_newline
const probeResult = await runExecPipe(sandbox.sandboxId, probe)
assertExecSucceeded(probe.name, probeResult)
const probeStdout = bufferToText(probeResult.stdout).trim()
if (probeStdout === '0') {
return
}
expect(probeStdout).toBe(String(probe.expectedBytes))
for (const testCase of cases) {
const result = await runExecPipe(sandbox.sandboxId, testCase)
assertExecSucceeded(testCase.name, result)
const stdout = bufferToText(result.stdout).trim()
expect(stdout, testCase.name).toBe(String(testCase.expectedBytes))
}
} finally {
try {
await sandbox.kill()
} catch (err) {
console.warn(
`Failed to kill sandbox ${sandbox.sandboxId}: ${String(err)}`
)
}
}
}
)
})
function runExecPipe(
sandboxId: string,
testCase: PipeCase
): Promise<CliRunResult> {
return runCliWithPipedStdin(
['sandbox', 'exec', sandboxId, '--', 'sh', '-lc', 'wc -c'],
testCase.data,
{
env: cliEnv,
timeoutMs: testCase.timeoutMs ?? defaultCmdTimeoutMs,
}
)
}
function assertExecSucceeded(
name: string,
result: CliRunResult
): void {
if (result.error) {
const timedOut = (result.error as NodeJS.ErrnoException).code === 'ETIMEDOUT'
throw new Error(
`${name} ${timedOut ? 'timed out' : 'failed'}: ${result.error.message}`
)
}
const stderr = bufferToText(result.stderr).trim()
if (result.status !== 0) {
throw new Error(`${name} failed with rc=${result.status} stderr=${stderr}`)
}
}
function safeGetUserConfig(): ReturnType<typeof getUserConfig> | null {
try {
return getUserConfig()
} catch (err) {
console.warn(`Failed to read ~/.e2b/config.json: ${String(err)}`)
return null
}
}
@@ -0,0 +1,158 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
const mocks = vi.hoisted(() => {
const connect = vi.fn()
const run = vi.fn()
const sendStdin = vi.fn()
const closeStdin = vi.fn()
const ensureAPIKey = vi.fn(() => 'test-api-key')
const isPipedStdin = vi.fn()
const streamStdinChunks = vi.fn()
const setupSignalHandlers = vi.fn(() => () => {})
return {
connect,
run,
sendStdin,
closeStdin,
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 stdin run flag', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
const handle = {
pid: 1234,
error: undefined,
wait: vi.fn().mockResolvedValue({ exitCode: 0 }),
kill: vi.fn().mockResolvedValue(undefined),
disconnect: vi.fn().mockResolvedValue(undefined),
}
mocks.run.mockResolvedValue(handle)
mocks.sendStdin.mockResolvedValue(undefined)
mocks.closeStdin.mockResolvedValue(undefined)
mocks.isPipedStdin.mockReturnValue(false)
mocks.streamStdinChunks.mockResolvedValue(undefined)
mocks.connect.mockResolvedValue({
commands: {
run: mocks.run,
sendStdin: mocks.sendStdin,
closeStdin: mocks.closeStdin,
supportsStdinClose: true,
},
})
})
afterEach(() => {
vi.restoreAllMocks()
})
test('does not pass stdin flag when stdin is not piped', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { execCommand } = await import('../../../src/commands/sandbox/exec')
await execCommand.parseAsync(['sandbox-id', 'echo', 'hello'], {
from: 'user',
})
expect(mocks.run).toHaveBeenCalledTimes(1)
const runOpts = mocks.run.mock.calls[0][1]
expect(runOpts).not.toHaveProperty('stdin')
expect(mocks.streamStdinChunks).not.toHaveBeenCalled()
expect(exitSpy).toHaveBeenCalledWith(0)
})
test('passes stdin: true when stdin piping is active', async () => {
mocks.isPipedStdin.mockReturnValue(true)
const events: string[] = []
mocks.setupSignalHandlers.mockImplementation(() => {
events.push('setup')
return () => {
events.push('cleanup')
}
})
mocks.streamStdinChunks.mockImplementation(
async (
_stream: NodeJS.ReadableStream,
onChunk: (chunk: Uint8Array) => Promise<void | boolean>,
_maxBytes: number
) => {
events.push('stream-start')
await onChunk(Buffer.from('hello'))
events.push('stream-end')
}
)
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never)
const { execCommand } = await import('../../../src/commands/sandbox/exec')
await execCommand.parseAsync(['sandbox-id', 'cat'], {
from: 'user',
})
expect(mocks.run).toHaveBeenCalledTimes(1)
const runOpts = mocks.run.mock.calls[0][1]
expect(runOpts).toHaveProperty('stdin', true)
expect(mocks.streamStdinChunks).toHaveBeenCalled()
expect(mocks.sendStdin).toHaveBeenCalled()
expect(mocks.closeStdin).toHaveBeenCalled()
expect(events).toContain('setup')
expect(events).toContain('stream-start')
expect(events.indexOf('setup')).toBeLessThan(events.indexOf('stream-start'))
expect(events).toContain('cleanup')
expect(exitSpy).toHaveBeenCalledWith(0)
})
})