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