Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2814468
cli(exec): drop killProcessBestEffort and fail fast on closeStdin errors
matthewlouisbrockman Feb 13, 2026
f43175f
pipe tests share a helper, consolidate and simplify
matthewlouisbrockman Feb 13, 2026
df838dd
consolidate cli test pattern to the same as regular js sdk
matthewlouisbrockman Feb 13, 2026
045cf19
logs/metrics tests can get consolidated
matthewlouisbrockman Feb 13, 2026
05040d2
cli: add the killProcessBestEffort back in
matthewlouisbrockman Feb 13, 2026
2d3e466
cli: inline killProcessBestEffort
matthewlouisbrockman Feb 13, 2026
a9a7c0c
cli: linter
matthewlouisbrockman Feb 13, 2026
330c5f2
Merge branch 'main' into piping-tone-down-the-tests-configs-eng-3497
matthewlouisbrockman Feb 17, 2026
65fdf5a
Merge branch 'main' into piping-tone-down-the-tests-configs-eng-3497
matthewlouisbrockman Feb 18, 2026
e867d7b
Merge branch 'main' into piping-tone-down-the-tests-configs-eng-3497
matthewlouisbrockman Feb 20, 2026
f352196
Merge branch 'main' into piping-tone-down-the-tests-configs-eng-3497
matthewlouisbrockman Mar 13, 2026
cab6388
Merge branch 'main' of github.com:e2b-dev/E2B into piping-tone-down-t…
matthewlouisbrockman Mar 24, 2026
fc3387b
Revert "Merge branch 'main' of github.com:e2b-dev/E2B into piping-ton…
matthewlouisbrockman Mar 24, 2026
05b85e7
Merge remote-tracking branch 'origin/main' into piping-tone-down-the-…
matthewlouisbrockman Mar 24, 2026
77fda8f
Merge remote-tracking branch 'origin/main' into piping-tone-down-the-…
matthewlouisbrockman Jun 2, 2026
a36022e
test: pass credentials in exec pipe integration
matthewlouisbrockman Jun 2, 2026
2ecd7c4
cli: inline stdin cleanup kill
matthewlouisbrockman Jun 2, 2026
6a77ca8
cli: keep exec behavior unchanged
matthewlouisbrockman Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 1 addition & 16 deletions packages/cli/src/commands/sandbox/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,22 +197,7 @@ async function sendStdin(sandbox: Sandbox, pid: number): Promise<void> {
return
}

// Fail fast instead of leaving a command blocked on stdin forever.
await killProcessBestEffort(sandbox, pid)
// Fail fast on EOF signaling errors.
throw err
Comment thread
matthewlouisbrockman marked this conversation as resolved.
Outdated
}
}

async function killProcessBestEffort(
sandbox: Sandbox,
pid: number
): Promise<void> {
try {
await sandbox.commands.kill(pid)
} catch (killErr) {
console.error(
'e2b: Failed to kill remote process after stdin EOF signaling failed.'
)
console.error(killErr)
}
}
222 changes: 47 additions & 175 deletions packages/cli/tests/commands/sandbox/backend_integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,36 @@
import { spawn, spawnSync } from 'node:child_process'
import path from 'node:path'
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import { Sandbox } from 'e2b'
import { getUserConfig } from 'src/user'

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
import {
bufferToText,
isDebug,
parseEnvInt,
runCli,
runCliWithPipedStdin,
} from '../../setup'

const integrationTest = test.skipIf(isDebug)
Comment thread
matthewlouisbrockman marked this conversation as resolved.
Outdated
const templateId =
process.env.E2B_CLI_BACKEND_TEMPLATE_ID ||
process.env.E2B_TEMPLATE_ID ||
'base'
const isDebug = process.env.E2B_DEBUG !== undefined
const hasCreds = Boolean(apiKey)
const shouldSkip = !hasCreds || isDebug
const testIf = test.skipIf(shouldSkip)
const cliPath = path.join(process.cwd(), 'dist', 'index.js')
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 runCliInSandbox = (args: string[]) =>
runCli(args, { timeoutMs: spawnTimeoutMs })
const runCliWithPipeInSandbox = (args: string[], input: Buffer) =>
runCliWithPipedStdin(args, input, { timeoutMs: spawnTimeoutMs })

describe('sandbox cli backend integration', () => {
let sandbox: Sandbox

beforeAll(async () => {
if (shouldSkip) return
if (isDebug) return

sandbox = await Sandbox.create(templateId, {
apiKey,
domain,
timeoutMs: sandboxTimeoutMs,
})
}, 30_000)
Expand All @@ -57,35 +47,33 @@ describe('sandbox cli backend integration', () => {
}
}, 15_000)

testIf('list shows the sandbox', { timeout: perTestTimeoutMs }, async () => {
const listResult = runCli(['sandbox', 'list', '--format', 'json'])
expect(listResult.status).toBe(0)
expect(sandboxExistsInList(listResult.stdout, sandbox.sandboxId)).toBe(true)
})
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)
}
)

testIf(
integrationTest(
'exec runs a command without piped stdin',
{ timeout: perTestTimeoutMs },
async () => {
const execResult = runCli([
'sandbox',
'exec',
sandbox.sandboxId,
'--',
'sh',
'-lc',
'echo backend-non-pipe',
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')
}
)

testIf(
integrationTest(
'exec runs a command with piped stdin',
{ timeout: perTestTimeoutMs },
async () => {
const pipedExecResult = await runCliWithPipedStdin(
const pipedExecResult = await runCliWithPipeInSandbox(
['sandbox', 'exec', sandbox.sandboxId, '--', 'sh', '-lc', 'wc -c'],
Buffer.from('hello\n', 'utf8')
)
Expand All @@ -97,132 +85,41 @@ describe('sandbox cli backend integration', () => {
}
)

testIf(
'logs returns successfully',
{ timeout: perTestTimeoutMs },
async () => {
const logsResult = runCli([
'sandbox',
'logs',
sandbox.sandboxId,
'--format',
'json',
])
expect(logsResult.status).toBe(0)
}
)

testIf(
'metrics returns successfully',
{ timeout: perTestTimeoutMs },
async () => {
const metricsResult = runCli([
'sandbox',
'metrics',
sandbox.sandboxId,
'--format',
'json',
])
expect(metricsResult.status).toBe(0)
}
)
for (const command of ['logs', 'metrics'] as const) {
integrationTest(
`${command} returns successfully`,
{ timeout: perTestTimeoutMs },
async () => {
const result = runCliInSandbox([
'sandbox',
command,
sandbox.sandboxId,
'--format',
'json',
])
expect(result.status).toBe(0)
}
)
}

testIf(
integrationTest(
'kill removes the sandbox',
{ timeout: perTestTimeoutMs },
async () => {
const killResult = runCli(['sandbox', 'kill', sandbox.sandboxId])
const killResult = runCliInSandbox(['sandbox', 'kill', sandbox.sandboxId])
expect(killResult.status).toBe(0)

await assertSandboxNotListed(sandbox.sandboxId)
}
)
})

function runCli(
args: string[],
opts?: { input?: string | Buffer }
): ReturnType<typeof spawnSync> {
const env: NodeJS.ProcessEnv = {
...process.env,
E2B_DOMAIN: domain,
E2B_API_KEY: apiKey,
}
delete env.E2B_DEBUG

return spawnSync('node', [cliPath, ...args], {
env,
input: opts?.input,
encoding: 'utf8',
timeout: spawnTimeoutMs,
})
}

type PipeRunResult = {
status: number | null
stdout: Buffer
stderr: Buffer
error?: Error
}

function runCliWithPipedStdin(
args: string[],
input: Buffer
): Promise<PipeRunResult> {
const env: NodeJS.ProcessEnv = {
...process.env,
E2B_DOMAIN: domain,
E2B_API_KEY: apiKey,
}
delete env.E2B_DEBUG

return new Promise((resolve) => {
const child = spawn('node', [cliPath, ...args], {
env,
stdio: ['pipe', 'pipe', 'pipe'],
})

const stdoutChunks: Buffer[] = []
const stderrChunks: Buffer[] = []
let childError: Error | undefined
let timedOut = false

const timer = setTimeout(() => {
timedOut = true
child.kill()
}, spawnTimeoutMs)

child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)))
child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)))
child.on('error', (err) => {
childError = err
})
child.on('close', (code) => {
clearTimeout(timer)
const timeoutError = timedOut
? Object.assign(new Error('CLI command timed out'), {
code: 'ETIMEDOUT',
} as NodeJS.ErrnoException)
: undefined
resolve({
status: code,
stdout: Buffer.concat(stdoutChunks),
stderr: Buffer.concat(stderrChunks),
error: childError ?? timeoutError,
})
})

child.stdin.write(input)
child.stdin.end()
})
}

async function assertSandboxNotListed(sandboxId: string): Promise<void> {
const retries = 10
const delayMs = 500

for (let i = 0; i < retries; i++) {
const listResult = runCli(['sandbox', 'list', '--format', 'json'])
const listResult = runCliInSandbox(['sandbox', 'list', '--format', 'json'])
if (listResult.status === 0) {
const exists = sandboxExistsInList(listResult.stdout, sandboxId)
if (!exists) {
Expand All @@ -248,28 +145,3 @@ function sandboxExistsInList(
const parsed = JSON.parse(text) as Array<{ sandboxId?: string }>
return parsed.some((item) => item.sandboxId === sandboxId)
}

function bufferToText(value: Buffer | string | null | undefined): string {
if (!value) {
return ''
}
return typeof value === 'string' ? value : value.toString('utf8')
}

function parseEnvInt(name: string, fallback: number): number {
const raw = process.env[name]
if (!raw) {
return fallback
}
const parsed = Number.parseInt(raw, 10)
return Number.isFinite(parsed) ? parsed : fallback
}

function safeGetUserConfig(): ReturnType<typeof getUserConfig> | null {
try {
return getUserConfig()
} catch (err) {
console.warn(`Failed to read ~/.e2b/config.json: ${String(err)}`)
return null
}
}
4 changes: 2 additions & 2 deletions packages/cli/tests/commands/sandbox/exec_close_stdin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('sandbox exec closeStdin handling', () => {
vi.restoreAllMocks()
})

test('fails fast and kills remote process when closeStdin throws non-NotFoundError', async () => {
test('fails fast when closeStdin throws non-NotFoundError', async () => {
mocks.closeStdin.mockRejectedValue(new Error('close failed'))

const exitSpy = vi
Expand All @@ -121,7 +121,7 @@ describe('sandbox exec closeStdin handling', () => {
})

expect(mocks.closeStdin).toHaveBeenCalledTimes(1)
expect(mocks.kill).toHaveBeenCalledWith(1234)
expect(mocks.kill).not.toHaveBeenCalled()
expect(mocks.wait).not.toHaveBeenCalled()
expect(exitSpy).toHaveBeenCalledWith(1)
})
Expand Down
Loading