208 lines
5.2 KiB
TypeScript
208 lines
5.2 KiB
TypeScript
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
|
|
}
|
|
}
|