Files
2026-07-13 12:47:58 +08:00

265 lines
7.1 KiB
TypeScript

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)
})
})