mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 05:02:11 +00:00
feat: add E2B auto-pause support with pause/kill/reconnect provider lifecycle
Add `pause()`, `kill()`, and `reconnect()` methods to the SandboxProvider interface so providers can support graceful suspension and permanent deletion as distinct operations. The E2B provider now uses `betaCreate` with `autoPause: true` by default, `betaPause()` for suspension, and surfaces `SandboxDestroyedError` on reconnect to a deleted sandbox. SDK exposes `pauseSandbox()` and `killSandbox()` alongside the existing `destroySandbox()`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
32f3c6c3bc
commit
77c8f1e3f3
12 changed files with 416 additions and 13 deletions
|
|
@ -39,7 +39,7 @@ try {
|
|||
}
|
||||
```
|
||||
|
||||
The `e2b` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically.
|
||||
The `e2b` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. Sandboxes pause by default instead of being deleted, and reconnecting with the same `sandboxId` resumes them automatically.
|
||||
|
||||
## Faster cold starts
|
||||
|
||||
|
|
|
|||
|
|
@ -357,10 +357,10 @@ icon: "rocket"
|
|||
|
||||
<Step title="Clean up">
|
||||
```typescript
|
||||
await client.destroySandbox(); // tears down the sandbox and disconnects
|
||||
await client.destroySandbox(); // provider-defined cleanup and disconnect
|
||||
```
|
||||
|
||||
Use `client.dispose()` instead to disconnect without destroying the sandbox (for reconnecting later).
|
||||
Use `client.dispose()` instead to disconnect without changing sandbox state. On E2B, `client.pauseSandbox()` pauses the sandbox and `client.killSandbox()` deletes it permanently.
|
||||
</Step>
|
||||
|
||||
<Step title="Inspect with the UI">
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ const sdk = await SandboxAgent.start({
|
|||
|
||||
// sdk.sandboxId — prefixed provider ID (e.g. "local/127.0.0.1:2468")
|
||||
|
||||
await sdk.destroySandbox(); // tears down sandbox + disposes client
|
||||
await sdk.destroySandbox(); // provider-defined cleanup + disposes client
|
||||
```
|
||||
|
||||
`SandboxAgent.start(...)` requires a `sandbox` provider. Built-in providers:
|
||||
|
|
@ -101,7 +101,7 @@ await sdk.destroySandbox(); // tears down sandbox + disposes client
|
|||
| `sandbox-agent/vercel` | Vercel Sandbox |
|
||||
| `sandbox-agent/cloudflare` | Cloudflare Sandbox |
|
||||
|
||||
Use `sdk.dispose()` to disconnect without destroying the sandbox, or `sdk.destroySandbox()` to tear down both.
|
||||
Use `sdk.dispose()` to disconnect without changing sandbox state, `sdk.pauseSandbox()` for graceful suspension when supported, or `sdk.killSandbox()` for permanent deletion.
|
||||
|
||||
## Session flow
|
||||
|
||||
|
|
|
|||
33
examples/daytona/src/daytona.ts
Normal file
33
examples/daytona/src/daytona.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { daytona } from "sandbox-agent/daytona";
|
||||
|
||||
function collectEnvVars(): Record<string, string> {
|
||||
const envVars: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
return envVars;
|
||||
}
|
||||
|
||||
function inspectorUrlToBaseUrl(inspectorUrl: string): string {
|
||||
return inspectorUrl.replace(/\/ui\/$/, "");
|
||||
}
|
||||
|
||||
export async function setupDaytonaSandboxAgent(): Promise<{
|
||||
baseUrl: string;
|
||||
token?: string;
|
||||
extraHeaders?: Record<string, string>;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: daytona({
|
||||
create: { envVars: collectEnvVars() },
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
baseUrl: inspectorUrlToBaseUrl(client.inspectorUrl),
|
||||
cleanup: async () => {
|
||||
await client.killSandbox();
|
||||
},
|
||||
};
|
||||
}
|
||||
32
examples/e2b/src/e2b.ts
Normal file
32
examples/e2b/src/e2b.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { e2b } from "sandbox-agent/e2b";
|
||||
|
||||
function collectEnvVars(): Record<string, string> {
|
||||
const envs: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
return envs;
|
||||
}
|
||||
|
||||
function inspectorUrlToBaseUrl(inspectorUrl: string): string {
|
||||
return inspectorUrl.replace(/\/ui\/$/, "");
|
||||
}
|
||||
|
||||
export async function setupE2BSandboxAgent(): Promise<{
|
||||
baseUrl: string;
|
||||
token?: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: e2b({
|
||||
create: { envs: collectEnvVars() },
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
baseUrl: inspectorUrlToBaseUrl(client.inspectorUrl),
|
||||
cleanup: async () => {
|
||||
await client.killSandbox();
|
||||
},
|
||||
};
|
||||
}
|
||||
35
examples/vercel/src/vercel.ts
Normal file
35
examples/vercel/src/vercel.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { vercel } from "sandbox-agent/vercel";
|
||||
|
||||
function collectEnvVars(): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
return env;
|
||||
}
|
||||
|
||||
function inspectorUrlToBaseUrl(inspectorUrl: string): string {
|
||||
return inspectorUrl.replace(/\/ui\/$/, "");
|
||||
}
|
||||
|
||||
export async function setupVercelSandboxAgent(): Promise<{
|
||||
baseUrl: string;
|
||||
token?: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: vercel({
|
||||
create: {
|
||||
runtime: "node24",
|
||||
env: collectEnvVars(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
baseUrl: inspectorUrlToBaseUrl(client.inspectorUrl),
|
||||
cleanup: async () => {
|
||||
await client.killSandbox();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -216,6 +216,18 @@ export class SandboxAgentError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class SandboxDestroyedError extends Error {
|
||||
readonly sandboxId: string;
|
||||
readonly provider: string;
|
||||
|
||||
constructor(sandboxId: string, provider: string, options?: { cause?: unknown }) {
|
||||
super(`Sandbox '${provider}/${sandboxId}' no longer exists and cannot be reconnected.`, options);
|
||||
this.name = "SandboxDestroyedError";
|
||||
this.sandboxId = sandboxId;
|
||||
this.provider = provider;
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedSessionCategoryError extends Error {
|
||||
readonly sessionId: string;
|
||||
readonly category: string;
|
||||
|
|
@ -904,6 +916,7 @@ export class SandboxAgent {
|
|||
const createdSandbox = !existingSandbox;
|
||||
|
||||
if (existingSandbox) {
|
||||
await provider.reconnect?.(rawSandboxId);
|
||||
await provider.ensureServer?.(rawSandboxId);
|
||||
}
|
||||
|
||||
|
|
@ -1007,6 +1020,50 @@ export class SandboxAgent {
|
|||
}
|
||||
}
|
||||
|
||||
async pauseSandbox(): Promise<void> {
|
||||
const provider = this.sandboxProvider;
|
||||
const rawSandboxId = this.sandboxProviderRawId;
|
||||
|
||||
try {
|
||||
if (provider && rawSandboxId) {
|
||||
if (provider.pause) {
|
||||
await provider.pause(rawSandboxId);
|
||||
} else {
|
||||
await provider.destroy(rawSandboxId);
|
||||
}
|
||||
} else if (!provider || !rawSandboxId) {
|
||||
throw new Error("SandboxAgent is not attached to a provisioned sandbox.");
|
||||
}
|
||||
} finally {
|
||||
await this.dispose();
|
||||
this.sandboxProvider = undefined;
|
||||
this.sandboxProviderId = undefined;
|
||||
this.sandboxProviderRawId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async killSandbox(): Promise<void> {
|
||||
const provider = this.sandboxProvider;
|
||||
const rawSandboxId = this.sandboxProviderRawId;
|
||||
|
||||
try {
|
||||
if (provider && rawSandboxId) {
|
||||
if (provider.kill) {
|
||||
await provider.kill(rawSandboxId);
|
||||
} else {
|
||||
await provider.destroy(rawSandboxId);
|
||||
}
|
||||
} else if (!provider || !rawSandboxId) {
|
||||
throw new Error("SandboxAgent is not attached to a provisioned sandbox.");
|
||||
}
|
||||
} finally {
|
||||
await this.dispose();
|
||||
this.sandboxProvider = undefined;
|
||||
this.sandboxProviderId = undefined;
|
||||
this.sandboxProviderRawId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async listSessions(request: ListPageRequest = {}): Promise<ListPage<Session>> {
|
||||
const page = await this.persist.listSessions(request);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export {
|
|||
ProcessTerminalSession,
|
||||
SandboxAgent,
|
||||
SandboxAgentError,
|
||||
SandboxDestroyedError,
|
||||
Session,
|
||||
UnsupportedPermissionReplyError,
|
||||
UnsupportedSessionCategoryError,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
import { Sandbox } from "@e2b/code-interpreter";
|
||||
import { NotFoundError, Sandbox, type SandboxBetaCreateOpts, type SandboxConnectOpts } from "@e2b/code-interpreter";
|
||||
import { SandboxDestroyedError } from "../client.ts";
|
||||
import type { SandboxProvider } from "./types.ts";
|
||||
import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
|
||||
|
||||
const DEFAULT_AGENT_PORT = 3000;
|
||||
const DEFAULT_TIMEOUT_MS = 3_600_000;
|
||||
|
||||
type E2BCreateOverrides = Omit<Partial<SandboxBetaCreateOpts>, "timeoutMs" | "autoPause">;
|
||||
type E2BConnectOverrides = Omit<Partial<SandboxConnectOpts>, "timeoutMs">;
|
||||
|
||||
export interface E2BProviderOptions {
|
||||
create?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>);
|
||||
connect?: Record<string, unknown> | ((sandboxId: string) => Record<string, unknown> | Promise<Record<string, unknown>>);
|
||||
create?: E2BCreateOverrides | (() => E2BCreateOverrides | Promise<E2BCreateOverrides>);
|
||||
connect?: E2BConnectOverrides | ((sandboxId: string) => E2BConnectOverrides | Promise<E2BConnectOverrides>);
|
||||
agentPort?: number;
|
||||
timeoutMs?: number;
|
||||
autoPause?: boolean;
|
||||
}
|
||||
|
||||
async function resolveOptions(value: E2BProviderOptions["create"] | E2BProviderOptions["connect"], sandboxId?: string): Promise<Record<string, unknown>> {
|
||||
|
|
@ -23,13 +30,15 @@ async function resolveOptions(value: E2BProviderOptions["create"] | E2BProviderO
|
|||
|
||||
export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
|
||||
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
||||
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
const autoPause = options.autoPause ?? true;
|
||||
|
||||
return {
|
||||
name: "e2b",
|
||||
async create(): Promise<string> {
|
||||
const createOpts = await resolveOptions(options.create);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const sandbox = await Sandbox.create({ allowInternetAccess: true, ...createOpts } as any);
|
||||
const sandbox = await Sandbox.betaCreate({ allowInternetAccess: true, ...createOpts, timeoutMs, autoPause } as any);
|
||||
|
||||
await sandbox.commands.run(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`).then((r) => {
|
||||
if (r.exitCode !== 0) throw new Error(`e2b install failed:\n${r.stderr}`);
|
||||
|
|
@ -44,18 +53,37 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
|
|||
return sandbox.sandboxId;
|
||||
},
|
||||
async destroy(sandboxId: string): Promise<void> {
|
||||
await this.pause?.(sandboxId);
|
||||
},
|
||||
async reconnect(sandboxId: string): Promise<void> {
|
||||
const connectOpts = await resolveOptions(options.connect, sandboxId);
|
||||
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
|
||||
try {
|
||||
await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
throw new SandboxDestroyedError(sandboxId, "e2b", { cause: error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async pause(sandboxId: string): Promise<void> {
|
||||
const connectOpts = await resolveOptions(options.connect, sandboxId);
|
||||
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
|
||||
await sandbox.betaPause();
|
||||
},
|
||||
async kill(sandboxId: string): Promise<void> {
|
||||
const connectOpts = await resolveOptions(options.connect, sandboxId);
|
||||
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
|
||||
await sandbox.kill();
|
||||
},
|
||||
async getUrl(sandboxId: string): Promise<string> {
|
||||
const connectOpts = await resolveOptions(options.connect, sandboxId);
|
||||
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
|
||||
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
|
||||
return `https://${sandbox.getHost(agentPort)}`;
|
||||
},
|
||||
async ensureServer(sandboxId: string): Promise<void> {
|
||||
const connectOpts = await resolveOptions(options.connect, sandboxId);
|
||||
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
|
||||
const sandbox = await Sandbox.connect(sandboxId, { ...connectOpts, timeoutMs } as SandboxConnectOpts);
|
||||
await sandbox.commands.run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`, { background: true, timeoutMs: 0 });
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,25 @@ export interface SandboxProvider {
|
|||
/** Permanently tear down a sandbox. */
|
||||
destroy(sandboxId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Reconnect to an existing sandbox before the SDK attempts health checks.
|
||||
* Providers can use this to resume paused sandboxes or surface provider-specific
|
||||
* reconnect errors.
|
||||
*/
|
||||
reconnect?(sandboxId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gracefully stop or pause a sandbox without permanently deleting it.
|
||||
* When omitted, callers should fall back to `destroy()`.
|
||||
*/
|
||||
pause?(sandboxId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Permanently delete a sandbox. When omitted, callers should fall back to
|
||||
* `destroy()`.
|
||||
*/
|
||||
kill?(sandboxId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Return the sandbox-agent base URL for this sandbox.
|
||||
* Providers that cannot expose a URL should implement `getFetch()` instead.
|
||||
|
|
|
|||
193
sdks/typescript/tests/provider-lifecycle.test.ts
Normal file
193
sdks/typescript/tests/provider-lifecycle.test.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SandboxAgent, SandboxDestroyedError, type SandboxProvider } from "../src/index.ts";
|
||||
|
||||
const e2bMocks = vi.hoisted(() => {
|
||||
class MockNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "NotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
MockNotFoundError,
|
||||
betaCreate: vi.fn(),
|
||||
connect: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@e2b/code-interpreter", () => ({
|
||||
NotFoundError: e2bMocks.MockNotFoundError,
|
||||
Sandbox: {
|
||||
betaCreate: e2bMocks.betaCreate,
|
||||
connect: e2bMocks.connect,
|
||||
},
|
||||
}));
|
||||
|
||||
import { e2b } from "../src/providers/e2b.ts";
|
||||
|
||||
function createFetch(): typeof fetch {
|
||||
return async () => new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
function createBaseProvider(overrides: Partial<SandboxProvider> = {}): SandboxProvider {
|
||||
return {
|
||||
name: "mock",
|
||||
async create(): Promise<string> {
|
||||
return "created";
|
||||
},
|
||||
async destroy(): Promise<void> {},
|
||||
async getUrl(): Promise<string> {
|
||||
return "http://127.0.0.1:3000";
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createMockSandbox() {
|
||||
return {
|
||||
sandboxId: "sbx-123",
|
||||
getHost: vi.fn(() => "sandbox.example"),
|
||||
betaPause: vi.fn(async () => true),
|
||||
kill: vi.fn(async () => undefined),
|
||||
commands: {
|
||||
run: vi.fn(async () => ({ exitCode: 0, stderr: "" })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("SandboxAgent provider lifecycle", () => {
|
||||
it("reconnects an existing sandbox before ensureServer", async () => {
|
||||
const order: string[] = [];
|
||||
const provider = createBaseProvider({
|
||||
reconnect: vi.fn(async () => {
|
||||
order.push("reconnect");
|
||||
}),
|
||||
ensureServer: vi.fn(async () => {
|
||||
order.push("ensureServer");
|
||||
}),
|
||||
});
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: provider,
|
||||
sandboxId: "mock/existing",
|
||||
skipHealthCheck: true,
|
||||
fetch: createFetch(),
|
||||
});
|
||||
|
||||
expect(order).toEqual(["reconnect", "ensureServer"]);
|
||||
|
||||
await sdk.killSandbox();
|
||||
});
|
||||
|
||||
it("surfaces SandboxDestroyedError from reconnect", async () => {
|
||||
const provider = createBaseProvider({
|
||||
reconnect: vi.fn(async () => {
|
||||
throw new SandboxDestroyedError("existing", "mock");
|
||||
}),
|
||||
ensureServer: vi.fn(async () => undefined),
|
||||
});
|
||||
|
||||
await expect(
|
||||
SandboxAgent.start({
|
||||
sandbox: provider,
|
||||
sandboxId: "mock/existing",
|
||||
skipHealthCheck: true,
|
||||
fetch: createFetch(),
|
||||
}),
|
||||
).rejects.toBeInstanceOf(SandboxDestroyedError);
|
||||
|
||||
expect(provider.ensureServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses provider pause and kill hooks for explicit lifecycle control", async () => {
|
||||
const pause = vi.fn(async () => undefined);
|
||||
const kill = vi.fn(async () => undefined);
|
||||
const provider = createBaseProvider({ pause, kill });
|
||||
|
||||
const paused = await SandboxAgent.start({
|
||||
sandbox: provider,
|
||||
skipHealthCheck: true,
|
||||
fetch: createFetch(),
|
||||
});
|
||||
await paused.pauseSandbox();
|
||||
expect(pause).toHaveBeenCalledWith("created");
|
||||
|
||||
const killed = await SandboxAgent.start({
|
||||
sandbox: provider,
|
||||
skipHealthCheck: true,
|
||||
fetch: createFetch(),
|
||||
});
|
||||
await killed.killSandbox();
|
||||
expect(kill).toHaveBeenCalledWith("created");
|
||||
});
|
||||
});
|
||||
|
||||
describe("e2b provider", () => {
|
||||
beforeEach(() => {
|
||||
e2bMocks.betaCreate.mockReset();
|
||||
e2bMocks.connect.mockReset();
|
||||
});
|
||||
|
||||
it("creates sandboxes with betaCreate, autoPause, and the default timeout", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
e2bMocks.betaCreate.mockResolvedValue(sandbox);
|
||||
|
||||
const provider = e2b({
|
||||
create: {
|
||||
envs: { ANTHROPIC_API_KEY: "test" },
|
||||
},
|
||||
});
|
||||
|
||||
await expect(provider.create()).resolves.toBe("sbx-123");
|
||||
|
||||
expect(e2bMocks.betaCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
allowInternetAccess: true,
|
||||
autoPause: true,
|
||||
timeoutMs: 3_600_000,
|
||||
envs: { ANTHROPIC_API_KEY: "test" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows timeoutMs and autoPause to be overridden", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
e2bMocks.betaCreate.mockResolvedValue(sandbox);
|
||||
|
||||
const provider = e2b({
|
||||
timeoutMs: 123_456,
|
||||
autoPause: false,
|
||||
});
|
||||
|
||||
await provider.create();
|
||||
|
||||
expect(e2bMocks.betaCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
autoPause: false,
|
||||
timeoutMs: 123_456,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("pauses by default in destroy and uses kill for permanent deletion", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
e2bMocks.connect.mockResolvedValue(sandbox);
|
||||
const provider = e2b();
|
||||
|
||||
await provider.destroy("sbx-123");
|
||||
expect(e2bMocks.connect).toHaveBeenLastCalledWith("sbx-123", { timeoutMs: 3_600_000 });
|
||||
expect(sandbox.betaPause).toHaveBeenCalledTimes(1);
|
||||
expect(sandbox.kill).not.toHaveBeenCalled();
|
||||
|
||||
await provider.kill?.("sbx-123");
|
||||
expect(sandbox.kill).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("maps missing reconnect targets to SandboxDestroyedError", async () => {
|
||||
e2bMocks.connect.mockRejectedValue(new e2bMocks.MockNotFoundError("gone"));
|
||||
const provider = e2b();
|
||||
|
||||
await expect(provider.reconnect?.("missing-sandbox")).rejects.toBeInstanceOf(SandboxDestroyedError);
|
||||
});
|
||||
});
|
||||
|
|
@ -291,7 +291,7 @@ function providerSuite(entry: ProviderEntry) {
|
|||
|
||||
afterEach(async () => {
|
||||
if (!sdk) return;
|
||||
await sdk.destroySandbox().catch(async () => {
|
||||
await sdk.killSandbox().catch(async () => {
|
||||
await sdk?.dispose().catch(() => {});
|
||||
});
|
||||
sdk = undefined;
|
||||
|
|
@ -364,6 +364,11 @@ function providerSuite(entry: ProviderEntry) {
|
|||
});
|
||||
await expect(reconnected.listAgents()).rejects.toThrow();
|
||||
}
|
||||
|
||||
if (entry.name === "e2b") {
|
||||
const rawSandboxId = sandboxId?.slice(sandboxId.indexOf("/") + 1);
|
||||
await entry.createProvider().kill?.(rawSandboxId!);
|
||||
}
|
||||
},
|
||||
entry.startTimeoutMs,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue