chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { assert, test, describe } from 'vitest'
|
||||
import { handleApiError } from '../../src/api'
|
||||
import {
|
||||
AuthenticationError,
|
||||
RateLimitError,
|
||||
SandboxError,
|
||||
} from '../../src/errors'
|
||||
|
||||
function createMockResponse(
|
||||
status: number,
|
||||
error: unknown,
|
||||
data?: unknown
|
||||
): {
|
||||
response: { status: number; ok: boolean }
|
||||
error: unknown
|
||||
data: unknown
|
||||
} {
|
||||
return {
|
||||
response: { status, ok: status >= 200 && status < 300 },
|
||||
error,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
describe('handleApiError', () => {
|
||||
describe('without content', () => {
|
||||
// openapi-fetch leaves `error` undefined for non-2xx responses with
|
||||
// Content-Length: 0
|
||||
test('catches 404 with undefined error', () => {
|
||||
const res = createMockResponse(404, undefined)
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, '404')
|
||||
})
|
||||
|
||||
test('catches 500 with undefined error', () => {
|
||||
const res = createMockResponse(500, undefined)
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, '500')
|
||||
})
|
||||
|
||||
test('returns AuthenticationError for 401 with undefined error', () => {
|
||||
const res = createMockResponse(401, undefined)
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, AuthenticationError)
|
||||
assert.include(err?.message, 'Unauthorized')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with empty error body', () => {
|
||||
test('catches 404 with empty string error', () => {
|
||||
const res = createMockResponse(404, '')
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, '404')
|
||||
})
|
||||
|
||||
test('catches 400 with empty string error', () => {
|
||||
const res = createMockResponse(400, '')
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, '400')
|
||||
})
|
||||
|
||||
test('catches 500 with empty string error', () => {
|
||||
const res = createMockResponse(500, '')
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, '500')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with JSON error body', () => {
|
||||
test('catches 404 with message', () => {
|
||||
const res = createMockResponse(404, { code: 404, message: 'Not found' })
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, 'Not found')
|
||||
})
|
||||
|
||||
test('catches 400 with message', () => {
|
||||
const res = createMockResponse(400, { code: 400, message: 'Bad request' })
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, 'Bad request')
|
||||
})
|
||||
})
|
||||
|
||||
describe('special status codes', () => {
|
||||
test('returns AuthenticationError for 401', () => {
|
||||
const res = createMockResponse(401, { message: 'Invalid token' })
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, AuthenticationError)
|
||||
assert.include(err?.message, 'Unauthorized')
|
||||
})
|
||||
|
||||
test('returns AuthenticationError for 401 with empty body', () => {
|
||||
const res = createMockResponse(401, '')
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, AuthenticationError)
|
||||
assert.include(err?.message, 'Unauthorized')
|
||||
})
|
||||
|
||||
test('returns RateLimitError for 429', () => {
|
||||
const res = createMockResponse(429, { message: 'Too many requests' })
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, RateLimitError)
|
||||
assert.include(err?.message, 'Rate limit')
|
||||
})
|
||||
|
||||
test('returns RateLimitError for 429 with empty body', () => {
|
||||
const res = createMockResponse(429, '')
|
||||
const err = handleApiError(res as any)
|
||||
assert.instanceOf(err, RateLimitError)
|
||||
assert.include(err?.message, 'Rate limit')
|
||||
})
|
||||
})
|
||||
|
||||
describe('success responses', () => {
|
||||
test('returns undefined for 200 success', () => {
|
||||
const res = createMockResponse(200, undefined, { id: '123' })
|
||||
const err = handleApiError(res as any)
|
||||
assert.isUndefined(err)
|
||||
})
|
||||
|
||||
test('returns undefined for 201 success', () => {
|
||||
const res = createMockResponse(201, undefined, { id: '123' })
|
||||
const err = handleApiError(res as any)
|
||||
assert.isUndefined(err)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
import { afterEach, expect, test, vi } from 'vitest'
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.resetModules()
|
||||
vi.doUnmock('undici')
|
||||
vi.doUnmock('../../src/utils')
|
||||
delete process.env.E2B_API_CONNECTIONS
|
||||
delete process.env.E2B_API_INFLIGHT_REQUESTS
|
||||
})
|
||||
|
||||
test('uses undici with a bounded HTTP/2 dispatcher for API requests', async () => {
|
||||
const agents: Array<{ allowH2?: boolean; connections?: number }> = []
|
||||
const requests: Array<{ init?: RequestInit & { dispatcher?: unknown } }> = []
|
||||
|
||||
class Agent {
|
||||
constructor(options: { allowH2?: boolean; connections?: number }) {
|
||||
agents.push(options)
|
||||
}
|
||||
}
|
||||
|
||||
const undiciFetch = vi.fn((input, init) => {
|
||||
requests.push({ init })
|
||||
return Promise.resolve(new Response('ok'))
|
||||
})
|
||||
const loadUndici = vi.fn(() => Promise.resolve({ Agent, fetch: undiciFetch }))
|
||||
|
||||
const { createApiFetchForRuntime } = await import('../../src/api/http2')
|
||||
|
||||
const fetcher = createApiFetchForRuntime('node', {
|
||||
connectionLimit: 100,
|
||||
loadUndici,
|
||||
})
|
||||
await fetcher('https://example.com/sandboxes')
|
||||
|
||||
expect(loadUndici).toHaveBeenCalledOnce()
|
||||
expect(agents).toEqual([{ allowH2: true, connections: 100 }])
|
||||
expect(requests[0].init?.dispatcher).toBeInstanceOf(Agent)
|
||||
})
|
||||
|
||||
test('uses a ProxyAgent dispatcher when a proxy is configured', async () => {
|
||||
const proxyAgents: Array<{
|
||||
uri?: string
|
||||
allowH2?: boolean
|
||||
connections?: number
|
||||
}> = []
|
||||
const agents: Array<unknown> = []
|
||||
const requests: Array<{ init?: RequestInit & { dispatcher?: unknown } }> = []
|
||||
|
||||
class Agent {
|
||||
constructor() {
|
||||
agents.push(this)
|
||||
}
|
||||
}
|
||||
|
||||
class ProxyAgent {
|
||||
constructor(options: {
|
||||
uri?: string
|
||||
allowH2?: boolean
|
||||
connections?: number
|
||||
}) {
|
||||
proxyAgents.push(options)
|
||||
}
|
||||
}
|
||||
|
||||
const undiciFetch = vi.fn((input, init) => {
|
||||
requests.push({ init })
|
||||
return Promise.resolve(new Response('ok'))
|
||||
})
|
||||
const loadUndici = vi.fn(() =>
|
||||
Promise.resolve({ Agent, ProxyAgent, fetch: undiciFetch })
|
||||
)
|
||||
|
||||
const { createApiFetchForRuntime } = await import('../../src/api/http2')
|
||||
|
||||
const fetcher = createApiFetchForRuntime('node', {
|
||||
connectionLimit: 100,
|
||||
proxy: 'http://user:pass@127.0.0.1:8080',
|
||||
loadUndici,
|
||||
})
|
||||
await fetcher('https://example.com/sandboxes')
|
||||
|
||||
expect(agents).toHaveLength(0)
|
||||
expect(proxyAgents).toEqual([
|
||||
{
|
||||
uri: 'http://user:pass@127.0.0.1:8080',
|
||||
allowH2: true,
|
||||
connections: 100,
|
||||
},
|
||||
])
|
||||
expect(requests[0].init?.dispatcher).toBeInstanceOf(ProxyAgent)
|
||||
})
|
||||
|
||||
test('caches API fetchers per proxy', async () => {
|
||||
const { createApiFetch } = await import('../../src/api/http2')
|
||||
|
||||
const noProxy = createApiFetch()
|
||||
const proxyA = createApiFetch('http://127.0.0.1:8080')
|
||||
const proxyB = createApiFetch('http://127.0.0.1:9090')
|
||||
|
||||
expect(createApiFetch()).toBe(noProxy)
|
||||
expect(createApiFetch('http://127.0.0.1:8080')).toBe(proxyA)
|
||||
expect(proxyA).not.toBe(noProxy)
|
||||
expect(proxyA).not.toBe(proxyB)
|
||||
})
|
||||
|
||||
test('getApiConnectionLimit throws on a malformed env value', async () => {
|
||||
process.env.E2B_API_CONNECTIONS = 'not-a-number'
|
||||
|
||||
const { getApiConnectionLimit } = await import('../../src/api/http2')
|
||||
|
||||
expect(() => getApiConnectionLimit()).toThrow(/E2B_API_CONNECTIONS/)
|
||||
})
|
||||
|
||||
test('getApiInflightLimit throws on a malformed env value', async () => {
|
||||
process.env.E2B_API_INFLIGHT_REQUESTS = 'not-a-number'
|
||||
|
||||
const { getApiInflightLimit } = await import('../../src/api/http2')
|
||||
|
||||
expect(() => getApiInflightLimit()).toThrow(/E2B_API_INFLIGHT_REQUESTS/)
|
||||
})
|
||||
|
||||
test('getApiInflightLimit returns 0 when explicitly disabled', async () => {
|
||||
process.env.E2B_API_INFLIGHT_REQUESTS = '0'
|
||||
|
||||
const { getApiInflightLimit } = await import('../../src/api/http2')
|
||||
|
||||
expect(getApiInflightLimit()).toBe(0)
|
||||
})
|
||||
|
||||
test('getApiInflightLimit throws on negative env value', async () => {
|
||||
process.env.E2B_API_INFLIGHT_REQUESTS = '-5'
|
||||
|
||||
const { getApiInflightLimit } = await import('../../src/api/http2')
|
||||
|
||||
expect(() => getApiInflightLimit()).toThrow(/E2B_API_INFLIGHT_REQUESTS=-5/)
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { expect, test, vi } from 'vitest'
|
||||
|
||||
import { limitConcurrency } from '../../src/api/inflight'
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res
|
||||
reject = rej
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
test('limitConcurrency queues requests over the cap and releases on response', async () => {
|
||||
const gate = deferred<Response>()
|
||||
let secondStarted = false
|
||||
const inner = vi.fn(async (input: RequestInfo | URL) => {
|
||||
if (String(input).endsWith('/first')) return gate.promise
|
||||
secondStarted = true
|
||||
return new Response('second')
|
||||
}) as unknown as typeof fetch
|
||||
|
||||
const limited = limitConcurrency(inner, 1)
|
||||
const first = limited('https://example.com/first')
|
||||
const second = limited('https://example.com/second')
|
||||
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(secondStarted).toBe(false)
|
||||
|
||||
gate.resolve(new Response('first'))
|
||||
expect(await (await first).text()).toBe('first')
|
||||
expect(await (await second).text()).toBe('second')
|
||||
expect(secondStarted).toBe(true)
|
||||
})
|
||||
|
||||
test('limitConcurrency releases when the underlying fetch rejects', async () => {
|
||||
let calls = 0
|
||||
const inner = vi.fn(async () => {
|
||||
calls++
|
||||
if (calls === 1) throw new Error('boom')
|
||||
return new Response('ok')
|
||||
}) as unknown as typeof fetch
|
||||
|
||||
const limited = limitConcurrency(inner, 1)
|
||||
await expect(limited('https://example.com/a')).rejects.toThrow('boom')
|
||||
|
||||
// Slot should be free for the next request.
|
||||
const res = await limited('https://example.com/b')
|
||||
expect(await res.text()).toBe('ok')
|
||||
})
|
||||
|
||||
test('limitConcurrency aborts queued requests when their signal fires', async () => {
|
||||
const gate = deferred<Response>()
|
||||
const inner = vi.fn(async () => gate.promise) as unknown as typeof fetch
|
||||
const limited = limitConcurrency(inner, 1)
|
||||
|
||||
// Occupy the only slot.
|
||||
const first = limited('https://example.com/first')
|
||||
|
||||
const controller = new AbortController()
|
||||
const queued = limited('https://example.com/queued', {
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
// Abort the queued request before the slot frees.
|
||||
controller.abort()
|
||||
await expect(queued).rejects.toMatchObject({ name: 'AbortError' })
|
||||
|
||||
// Release the first request to make sure cleanup did not break the slot.
|
||||
gate.resolve(new Response('done'))
|
||||
const resp = await first
|
||||
expect(await resp.text()).toBe('done')
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { sandboxTest, isDebug } from '../setup.js'
|
||||
import { Sandbox } from '../../src'
|
||||
|
||||
sandboxTest.skipIf(isDebug)('get sandbox info', async ({ sandbox }) => {
|
||||
const info = await Sandbox.getInfo(sandbox.sandboxId)
|
||||
expect(info).toBeDefined()
|
||||
expect(info.sandboxId).toBe(sandbox.sandboxId)
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { sandboxTest, isDebug } from '../setup.js'
|
||||
import { Sandbox } from '../../src'
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'kill existing sandbox',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
await Sandbox.kill(sandbox.sandboxId)
|
||||
|
||||
const paginator = Sandbox.list({
|
||||
query: { state: ['running'], metadata: { sandboxTestId } },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
expect(sandboxes.map((s) => s.sandboxId)).not.toContain(sandbox.sandboxId)
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)('kill non-existing sandbox', async () => {
|
||||
await expect(Sandbox.kill('nonexistingsandbox')).resolves.toBe(false)
|
||||
})
|
||||
@@ -0,0 +1,440 @@
|
||||
import { assert } from 'vitest'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
import { Sandbox, SandboxInfo } from '../../src'
|
||||
import { sandboxTest, isDebug } from '../setup.js'
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'list sandboxes',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { sandboxTestId } },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
assert.isAtLeast(sandboxes.length, 1)
|
||||
|
||||
const found = sandboxes.some((s) => s.sandboxId === sandbox.sandboxId)
|
||||
assert.isTrue(found)
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)('list sandboxes with filter', async () => {
|
||||
const uniqueId = randomUUID()
|
||||
const extraSbx = await Sandbox.create({ metadata: { uniqueId } })
|
||||
|
||||
try {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { uniqueId } },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
assert.equal(sandboxes.length, 1)
|
||||
assert.equal(sandboxes[0].sandboxId, extraSbx.sandboxId)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
})
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'list running sandboxes',
|
||||
async ({ sandboxTestId }) => {
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
|
||||
try {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { sandboxTestId }, state: ['running'] },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
assert.isAtLeast(sandboxes.length, 1)
|
||||
|
||||
// Verify our running sandbox is in the list
|
||||
const found = sandboxes.some(
|
||||
(s) => s.sandboxId === extraSbx.sandboxId && s.state === 'running'
|
||||
)
|
||||
assert.isTrue(found)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'list paused sandboxes',
|
||||
async ({ sandboxTestId }) => {
|
||||
// Create and pause a sandbox
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
await extraSbx.betaPause()
|
||||
|
||||
try {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { sandboxTestId }, state: ['paused'] },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
assert.isAtLeast(sandboxes.length, 1)
|
||||
|
||||
// Verify our paused sandbox is in the list
|
||||
const found = sandboxes.some(
|
||||
(s) => s.sandboxId === extraSbx.sandboxId && s.state === 'paused'
|
||||
)
|
||||
assert.isTrue(found)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'paginate running sandboxes',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
// Create extra sandboxes
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
|
||||
try {
|
||||
// Test pagination with limit
|
||||
const paginator = Sandbox.list({
|
||||
limit: 1,
|
||||
query: { metadata: { sandboxTestId }, state: ['running'] },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
// Check first page
|
||||
assert.equal(sandboxes.length, 1)
|
||||
assert.equal(sandboxes[0].state, 'running')
|
||||
assert.isTrue(paginator.hasNext)
|
||||
assert.notEqual(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes[0].sandboxId, extraSbx.sandboxId)
|
||||
|
||||
// Get second page
|
||||
const sandboxes2 = await paginator.nextItems()
|
||||
|
||||
// Check second page
|
||||
assert.equal(sandboxes2.length, 1)
|
||||
assert.equal(sandboxes2[0].state, 'running')
|
||||
assert.isFalse(paginator.hasNext)
|
||||
assert.equal(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes2[0].sandboxId, sandbox.sandboxId)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'paginate paused sandboxes',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
await sandbox.betaPause()
|
||||
|
||||
// Create extra paused sandbox
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
await extraSbx.betaPause()
|
||||
|
||||
try {
|
||||
// Test pagination with limit
|
||||
const paginator = Sandbox.list({
|
||||
limit: 1,
|
||||
query: { metadata: { sandboxTestId }, state: ['paused'] },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
// Check first page
|
||||
assert.equal(sandboxes.length, 1)
|
||||
assert.equal(sandboxes[0].state, 'paused')
|
||||
assert.isTrue(paginator.hasNext)
|
||||
assert.notEqual(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes[0].sandboxId, extraSbx.sandboxId)
|
||||
|
||||
// Get second page
|
||||
const sandboxes2 = await paginator.nextItems()
|
||||
|
||||
// Check second page
|
||||
assert.equal(sandboxes2.length, 1)
|
||||
assert.equal(sandboxes2[0].state, 'paused')
|
||||
assert.isFalse(paginator.hasNext)
|
||||
assert.equal(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes2[0].sandboxId, sandbox.sandboxId)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'paginate running and paused sandboxes',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
// Create extra sandbox
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
|
||||
// Pause the extra sandbox
|
||||
await extraSbx.betaPause()
|
||||
|
||||
try {
|
||||
// Test pagination with limit
|
||||
const paginator = Sandbox.list({
|
||||
limit: 1,
|
||||
query: {
|
||||
metadata: { sandboxTestId },
|
||||
state: ['running', 'paused'],
|
||||
},
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
// Check first page
|
||||
assert.equal(sandboxes.length, 1)
|
||||
assert.equal(sandboxes[0].state, 'paused')
|
||||
assert.isTrue(paginator.hasNext)
|
||||
assert.notEqual(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes[0].sandboxId, extraSbx.sandboxId)
|
||||
|
||||
// Get second page
|
||||
const sandboxes2 = await paginator.nextItems()
|
||||
|
||||
// Check second page
|
||||
assert.equal(sandboxes2.length, 1)
|
||||
assert.equal(sandboxes2[0].state, 'running')
|
||||
assert.isFalse(paginator.hasNext)
|
||||
assert.equal(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes2[0].sandboxId, sandbox.sandboxId)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'paginate iterator',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { sandboxTestId } },
|
||||
})
|
||||
const sandboxes: SandboxInfo[] = []
|
||||
|
||||
while (paginator.hasNext) {
|
||||
const sbxs = await paginator.nextItems()
|
||||
sandboxes.push(...sbxs)
|
||||
}
|
||||
|
||||
assert.isAtLeast(sandboxes.length, 1)
|
||||
assert.isTrue(sandboxes.some((s) => s.sandboxId === sandbox.sandboxId))
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'list sandboxes',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { sandboxTestId } },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
assert.isAtLeast(sandboxes.length, 1)
|
||||
|
||||
const found = sandboxes.some((s) => s.sandboxId === sandbox.sandboxId)
|
||||
assert.isTrue(found)
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)('list sandboxes with filter', async () => {
|
||||
const uniqueId = randomUUID()
|
||||
const extraSbx = await Sandbox.create({ metadata: { uniqueId } })
|
||||
|
||||
try {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { uniqueId } },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
assert.equal(sandboxes.length, 1)
|
||||
assert.equal(sandboxes[0].sandboxId, extraSbx.sandboxId)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
})
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'list running sandboxes',
|
||||
async ({ sandboxTestId }) => {
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
|
||||
try {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { sandboxTestId }, state: ['running'] },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
assert.isAtLeast(sandboxes.length, 1)
|
||||
|
||||
// Verify our running sandbox is in the list
|
||||
const found = sandboxes.some(
|
||||
(s) => s.sandboxId === extraSbx.sandboxId && s.state === 'running'
|
||||
)
|
||||
assert.isTrue(found)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'list paused sandboxes',
|
||||
async ({ sandboxTestId }) => {
|
||||
// Create and pause a sandbox
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
await Sandbox.betaPause(extraSbx.sandboxId)
|
||||
|
||||
try {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { sandboxTestId }, state: ['paused'] },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
assert.isAtLeast(sandboxes.length, 1)
|
||||
|
||||
// Verify our paused sandbox is in the list
|
||||
const found = sandboxes.some(
|
||||
(s) => s.sandboxId === extraSbx.sandboxId && s.state === 'paused'
|
||||
)
|
||||
assert.isTrue(found)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'paginate running sandboxes',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
// Create extra sandboxes
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
|
||||
try {
|
||||
// Test pagination with limit
|
||||
const paginator = Sandbox.list({
|
||||
limit: 1,
|
||||
query: { metadata: { sandboxTestId }, state: ['running'] },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
// Check first page
|
||||
assert.equal(sandboxes.length, 1)
|
||||
assert.equal(sandboxes[0].state, 'running')
|
||||
assert.isTrue(paginator.hasNext)
|
||||
assert.notEqual(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes[0].sandboxId, extraSbx.sandboxId)
|
||||
|
||||
// Get second page
|
||||
const sandboxes2 = await paginator.nextItems()
|
||||
|
||||
// Check second page
|
||||
assert.equal(sandboxes2.length, 1)
|
||||
assert.equal(sandboxes2[0].state, 'running')
|
||||
assert.isFalse(paginator.hasNext)
|
||||
assert.equal(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes2[0].sandboxId, sandbox.sandboxId)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'paginate paused sandboxes',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
await Sandbox.betaPause(sandbox.sandboxId)
|
||||
|
||||
// Create extra paused sandbox
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
await Sandbox.betaPause(extraSbx.sandboxId)
|
||||
|
||||
try {
|
||||
// Test pagination with limit
|
||||
const paginator = Sandbox.list({
|
||||
limit: 1,
|
||||
query: { metadata: { sandboxTestId }, state: ['paused'] },
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
// Check first page
|
||||
assert.equal(sandboxes.length, 1)
|
||||
assert.equal(sandboxes[0].state, 'paused')
|
||||
assert.isTrue(paginator.hasNext)
|
||||
assert.notEqual(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes[0].sandboxId, extraSbx.sandboxId)
|
||||
|
||||
// Get second page
|
||||
const sandboxes2 = await paginator.nextItems()
|
||||
|
||||
// Check second page
|
||||
assert.equal(sandboxes2.length, 1)
|
||||
assert.equal(sandboxes2[0].state, 'paused')
|
||||
assert.isFalse(paginator.hasNext)
|
||||
assert.equal(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes2[0].sandboxId, sandbox.sandboxId)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'paginate running and paused sandboxes',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
// Create extra sandbox
|
||||
const extraSbx = await Sandbox.create({ metadata: { sandboxTestId } })
|
||||
|
||||
// Pause the extra sandbox
|
||||
await Sandbox.betaPause(sandbox.sandboxId)
|
||||
|
||||
try {
|
||||
// Test pagination with limit
|
||||
const paginator = Sandbox.list({
|
||||
limit: 1,
|
||||
query: {
|
||||
metadata: { sandboxTestId },
|
||||
state: ['running', 'paused'],
|
||||
},
|
||||
})
|
||||
const sandboxes = await paginator.nextItems()
|
||||
|
||||
// Check first page
|
||||
assert.equal(sandboxes.length, 1)
|
||||
assert.equal(sandboxes[0].state, 'running')
|
||||
|
||||
assert.isTrue(paginator.hasNext)
|
||||
assert.notEqual(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes[0].sandboxId, extraSbx.sandboxId)
|
||||
|
||||
// Get second page
|
||||
const sandboxes2 = await paginator.nextItems()
|
||||
|
||||
// Check second page
|
||||
assert.equal(sandboxes2.length, 1)
|
||||
assert.equal(sandboxes2[0].state, 'paused')
|
||||
assert.isFalse(paginator.hasNext)
|
||||
assert.equal(paginator.nextToken, undefined)
|
||||
assert.equal(sandboxes2[0].sandboxId, sandbox.sandboxId)
|
||||
} finally {
|
||||
await extraSbx.kill()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sandboxTest.skipIf(isDebug)(
|
||||
'paginate iterator',
|
||||
async ({ sandbox, sandboxTestId }) => {
|
||||
const paginator = Sandbox.list({
|
||||
query: { metadata: { sandboxTestId } },
|
||||
})
|
||||
const sandboxes: SandboxInfo[] = []
|
||||
|
||||
while (paginator.hasNext) {
|
||||
const sbxs = await paginator.nextItems()
|
||||
sandboxes.push(...sbxs)
|
||||
}
|
||||
|
||||
assert.isAtLeast(sandboxes.length, 1)
|
||||
assert.isTrue(sandboxes.some((s) => s.sandboxId === sandbox.sandboxId))
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
import { assert } from 'vitest'
|
||||
|
||||
import { sandboxTest, isDebug } from '../setup.js'
|
||||
import { Sandbox } from '../../src'
|
||||
|
||||
sandboxTest.skipIf(isDebug)('pause sandbox', async ({ sandbox }) => {
|
||||
await Sandbox.pause(sandbox.sandboxId)
|
||||
assert.isFalse(
|
||||
await sandbox.isRunning(),
|
||||
'Sandbox should not be running after pause'
|
||||
)
|
||||
})
|
||||
|
||||
sandboxTest.skipIf(isDebug)('resume sandbox', async ({ sandbox }) => {
|
||||
await Sandbox.pause(sandbox.sandboxId)
|
||||
assert.isFalse(
|
||||
await sandbox.isRunning(),
|
||||
'Sandbox should not be running after pause'
|
||||
)
|
||||
|
||||
await Sandbox.connect(sandbox.sandboxId)
|
||||
assert.isTrue(
|
||||
await sandbox.isRunning(),
|
||||
'Sandbox should be running after resume'
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { assert, test, describe, vi, afterEach } from 'vitest'
|
||||
import { ApiClient, validateApiKey } from '../../src/api'
|
||||
import { ConnectionConfig } from '../../src/connectionConfig'
|
||||
import { AuthenticationError } from '../../src/errors'
|
||||
|
||||
describe('validateApiKey', () => {
|
||||
const validKey = 'e2b_' + '0123456789abcdef'.repeat(2) + '01234567'
|
||||
|
||||
test('accepts a well-formed key', () => {
|
||||
assert.doesNotThrow(() => validateApiKey(validKey))
|
||||
})
|
||||
|
||||
test('rejects a key without the e2b_ prefix', () => {
|
||||
assert.throws(
|
||||
() => validateApiKey('sk_' + '0'.repeat(40)),
|
||||
AuthenticationError,
|
||||
/Invalid API key format/
|
||||
)
|
||||
})
|
||||
|
||||
test('accepts a key with a non-default body length', () => {
|
||||
assert.doesNotThrow(() => validateApiKey('e2b_' + '0'.repeat(20)))
|
||||
})
|
||||
|
||||
test('rejects an empty body after the prefix', () => {
|
||||
assert.throws(
|
||||
() => validateApiKey('e2b_'),
|
||||
AuthenticationError,
|
||||
/Invalid API key format/
|
||||
)
|
||||
})
|
||||
|
||||
test('rejects a key with non-hex characters in the body', () => {
|
||||
assert.throws(
|
||||
() => validateApiKey('e2b_' + 'z'.repeat(40)),
|
||||
AuthenticationError,
|
||||
/Invalid API key format/
|
||||
)
|
||||
})
|
||||
|
||||
test('error message includes an example token', () => {
|
||||
try {
|
||||
validateApiKey('nope')
|
||||
assert.fail('expected validateApiKey to throw')
|
||||
} catch (err) {
|
||||
assert.instanceOf(err, AuthenticationError)
|
||||
assert.match(
|
||||
(err as Error).message,
|
||||
/e2b_0{40}/,
|
||||
'expected example token in error message'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('ApiClient API key validation', () => {
|
||||
test('throws on a malformed key by default', () => {
|
||||
const config = new ConnectionConfig({ apiKey: 'not-a-valid-key' })
|
||||
assert.throws(() => new ApiClient(config), AuthenticationError)
|
||||
})
|
||||
|
||||
test('skips validation when validateApiKey is false', () => {
|
||||
const config = new ConnectionConfig({
|
||||
apiKey: 'not-a-valid-key',
|
||||
validateApiKey: false,
|
||||
})
|
||||
assert.doesNotThrow(() => new ApiClient(config))
|
||||
})
|
||||
})
|
||||
|
||||
describe('ApiClient API key requirement', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
test('throws when no API key is supplied', () => {
|
||||
vi.stubEnv('E2B_API_KEY', '')
|
||||
const config = new ConnectionConfig({})
|
||||
assert.throws(
|
||||
() => new ApiClient(config),
|
||||
AuthenticationError,
|
||||
/API key is required/
|
||||
)
|
||||
})
|
||||
|
||||
test('does not require an API key when requireApiKey is false', () => {
|
||||
vi.stubEnv('E2B_API_KEY', '')
|
||||
const config = new ConnectionConfig({})
|
||||
assert.doesNotThrow(() => new ApiClient(config, { requireApiKey: false }))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user