Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/petite-falcons-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@e2b/python-sdk': patch
'e2b': patch
---

Fix missing instance config propagation in Sandbox pause/connect and add regression tests for config forwarding + overrides.
48 changes: 27 additions & 21 deletions packages/js-sdk/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ export class Sandbox extends SandboxApi {
* ```
*/
async connect(opts?: SandboxConnectOpts): Promise<this> {
await SandboxApi.connectSandbox(this.sandboxId, opts)
await SandboxApi.connectSandbox(this.sandboxId, this.resolveApiOpts(opts))

return this
}
Expand Down Expand Up @@ -550,10 +550,11 @@ export class Sandbox extends SandboxApi {
return
}

await SandboxApi.setTimeout(this.sandboxId, timeoutMs, {
...this.connectionConfig,
...opts,
})
await SandboxApi.setTimeout(
this.sandboxId,
timeoutMs,
this.resolveApiOpts(opts)
)
}

/**
Expand All @@ -567,7 +568,7 @@ export class Sandbox extends SandboxApi {
return
}

await SandboxApi.kill(this.sandboxId, { ...this.connectionConfig, ...opts })
await SandboxApi.kill(this.sandboxId, this.resolveApiOpts(opts))
}

/**
Expand All @@ -584,14 +585,14 @@ export class Sandbox extends SandboxApi {
* ```
*/
async pause(opts?: ConnectionOpts): Promise<boolean> {
return await SandboxApi.pause(this.sandboxId, opts)
return await SandboxApi.pause(this.sandboxId, this.resolveApiOpts(opts))
}

/**
* @deprecated Use {@link Sandbox.pause} instead.
*/
async betaPause(opts?: ConnectionOpts): Promise<boolean> {
return await SandboxApi.betaPause(this.sandboxId, opts)
return await SandboxApi.betaPause(this.sandboxId, this.resolveApiOpts(opts))
}

/**
Expand Down Expand Up @@ -620,10 +621,10 @@ export class Sandbox extends SandboxApi {
* ```
*/
async createSnapshot(opts?: SandboxApiOpts): Promise<SnapshotInfo> {
return await SandboxApi.createSnapshot(this.sandboxId, {
...this.connectionConfig,
...opts,
})
return await SandboxApi.createSnapshot(
this.sandboxId,
this.resolveApiOpts(opts)
)
}

/**
Expand All @@ -635,8 +636,7 @@ export class Sandbox extends SandboxApi {
*/
listSnapshots(opts?: Omit<SnapshotListOpts, 'sandboxId'>): SnapshotPaginator {
return SandboxApi.listSnapshots({
...this.connectionConfig,
...opts,
...this.resolveApiOpts(opts),
sandboxId: this.sandboxId,
})
}
Expand Down Expand Up @@ -779,10 +779,7 @@ export class Sandbox extends SandboxApi {
* @returns information about the sandbox
*/
async getInfo(opts?: Pick<SandboxOpts, 'requestTimeoutMs'>) {
return await SandboxApi.getInfo(this.sandboxId, {
...this.connectionConfig,
...opts,
})
return await SandboxApi.getInfo(this.sandboxId, this.resolveApiOpts(opts))
}

/**
Expand All @@ -808,10 +805,19 @@ export class Sandbox extends SandboxApi {
}
}

return await SandboxApi.getMetrics(this.sandboxId, {
return await SandboxApi.getMetrics(
Comment thread
cursor[bot] marked this conversation as resolved.
this.sandboxId,
this.resolveApiOpts(opts)
)
}

private resolveApiOpts<T extends ConnectionOpts>(
opts?: T
): ConnectionOpts & T {
return {
...this.connectionConfig,
...opts,
})
...(opts ?? {}),
} as ConnectionOpts & T
}

private fileUrl(path: string | undefined, username: string | undefined) {
Expand Down
59 changes: 59 additions & 0 deletions packages/js-sdk/tests/sandbox/configPropagation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { afterEach, assert, describe, test, vi } from 'vitest'

import { Sandbox } from '../../src'
import { SandboxApi } from '../../src/sandbox/sandboxApi'

const baseConfig = {
apiKey: 'base-api-key',
domain: 'base.e2b.dev',
requestTimeoutMs: 1111,
debug: false,
headers: { 'X-Test': 'base' },
}

function createSandbox() {
return new Sandbox({
sandboxId: 'sbx-test',
sandboxDomain: 'sandbox.e2b.dev',
envdVersion: '0.2.4',
envdAccessToken: 'tok',
trafficAccessToken: 'tok',
...baseConfig,
})
}

describe('Sandbox API config propagation', () => {
afterEach(() => {
vi.restoreAllMocks()
})

test('passes connectionConfig to public API methods when called without overrides', async () => {
const pauseSpy = vi.spyOn(SandboxApi, 'pause').mockResolvedValue(true)
const sandbox = createSandbox()

await sandbox.pause()

const opts = pauseSpy.mock.calls[0][1]
assert.equal(opts?.apiKey, baseConfig.apiKey)
assert.equal(opts?.domain, baseConfig.domain)
assert.equal(opts?.requestTimeoutMs, baseConfig.requestTimeoutMs)
assert.equal(opts?.debug, baseConfig.debug)
assert.equal(opts?.headers?.['X-Test'], baseConfig.headers['X-Test'])
})

test('lets public method call overrides win over connectionConfig', async () => {
const pauseSpy = vi.spyOn(SandboxApi, 'pause').mockResolvedValue(true)
const sandbox = createSandbox()

await sandbox.pause({
domain: 'override.e2b.dev',
requestTimeoutMs: 9999,
})

const opts = pauseSpy.mock.calls[0][1]
assert.equal(opts?.apiKey, baseConfig.apiKey)
assert.equal(opts?.domain, 'override.e2b.dev')
assert.equal(opts?.requestTimeoutMs, 9999)
assert.equal(opts?.debug, baseConfig.debug)
})
})
4 changes: 2 additions & 2 deletions packages/python-sdk/e2b/sandbox_async/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ async def connect(
await SandboxApi._cls_connect(
sandbox_id=self.sandbox_id,
timeout=timeout,
**opts,
**self.connection_config.get_api_params(**opts),
)

return self
Expand Down Expand Up @@ -636,7 +636,7 @@ async def pause(

await SandboxApi._cls_pause(
sandbox_id=self.sandbox_id,
**opts,
**self.connection_config.get_api_params(**opts),
)

@overload
Expand Down
4 changes: 2 additions & 2 deletions packages/python-sdk/e2b/sandbox_sync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def connect(
SandboxApi._cls_connect(
sandbox_id=self.sandbox_id,
timeout=timeout,
**opts,
**self.connection_config.get_api_params(**opts),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid injecting base api_url into instance connect/pause opts

Using self.connection_config.get_api_params(**opts) here injects the stored api_url into every call; when users pass a per-call domain override without api_url, _cls_connect/_cls_pause rebuild ConnectionConfig from kwargs and keep the old api_url, so the override domain is ignored. This is a behavior change from the previous **opts path for these instance methods (and the same pattern is now present in both sync and async implementations).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine

)

return self
Expand Down Expand Up @@ -633,7 +633,7 @@ def pause(

SandboxApi._cls_pause(
sandbox_id=self.sandbox_id,
**opts,
**self.connection_config.get_api_params(**opts),
)

@overload
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock

import pytest
from packaging.version import Version

from e2b import AsyncSandbox
from e2b.connection_config import ConnectionConfig
import e2b.sandbox_async.main as sandbox_async_main


BASE_API_KEY = "base-api-key"
BASE_DOMAIN = "base.e2b.dev"
BASE_REQUEST_TIMEOUT = 11
BASE_DEBUG = False
BASE_HEADERS = {"X-Test": "base"}


def create_sandbox(monkeypatch) -> AsyncSandbox:
dummy_transport = SimpleNamespace(pool=object())

monkeypatch.setattr(
sandbox_async_main, "get_transport", lambda *_args, **_kwargs: dummy_transport
)
monkeypatch.setattr(
sandbox_async_main.httpx, "AsyncClient", lambda *args, **kwargs: object()
)
monkeypatch.setattr(
sandbox_async_main, "Filesystem", lambda *args, **kwargs: object()
)
monkeypatch.setattr(
sandbox_async_main, "Commands", lambda *args, **kwargs: object()
)
monkeypatch.setattr(sandbox_async_main, "Pty", lambda *args, **kwargs: object())
monkeypatch.setattr(sandbox_async_main, "Git", lambda *args, **kwargs: object())

return AsyncSandbox(
sandbox_id="sbx-test",
sandbox_domain="sandbox.e2b.dev",
envd_version=Version("0.2.4"),
envd_access_token="tok",
traffic_access_token="tok",
connection_config=ConnectionConfig(
api_key=BASE_API_KEY,
domain=BASE_DOMAIN,
request_timeout=BASE_REQUEST_TIMEOUT,
debug=BASE_DEBUG,
headers=BASE_HEADERS,
),
)


@pytest.mark.skip_debug()
async def test_pause_passes_connection_config_without_overrides(monkeypatch):
mock_pause = AsyncMock(return_value="sbx-test")
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_pause", mock_pause)

sandbox = create_sandbox(monkeypatch)
await sandbox.pause()

mock_pause.assert_awaited_once()
assert mock_pause.call_args.kwargs["sandbox_id"] == "sbx-test"
assert mock_pause.call_args.kwargs["api_key"] == BASE_API_KEY
assert mock_pause.call_args.kwargs["domain"] == BASE_DOMAIN
assert mock_pause.call_args.kwargs["request_timeout"] == BASE_REQUEST_TIMEOUT
assert mock_pause.call_args.kwargs["debug"] == BASE_DEBUG
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]


@pytest.mark.skip_debug()
async def test_pause_applies_overrides(monkeypatch):
mock_pause = AsyncMock(return_value="sbx-test")
monkeypatch.setattr(sandbox_async_main.SandboxApi, "_cls_pause", mock_pause)

sandbox = create_sandbox(monkeypatch)
await sandbox.pause(
domain="override.e2b.dev",
request_timeout=20,
headers={"X-Extra": "1"},
)

mock_pause.assert_awaited_once()
assert mock_pause.call_args.kwargs["sandbox_id"] == "sbx-test"
assert mock_pause.call_args.kwargs["api_key"] == BASE_API_KEY
assert mock_pause.call_args.kwargs["domain"] == "override.e2b.dev"
assert mock_pause.call_args.kwargs["request_timeout"] == 20
assert mock_pause.call_args.kwargs["debug"] == BASE_DEBUG
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]
assert mock_pause.call_args.kwargs["headers"]["X-Extra"] == "1"
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from types import SimpleNamespace
from unittest.mock import Mock

import pytest
from packaging.version import Version

from e2b import Sandbox
from e2b.connection_config import ConnectionConfig
import e2b.sandbox_sync.main as sandbox_sync_main


BASE_API_KEY = "base-api-key"
BASE_DOMAIN = "base.e2b.dev"
BASE_REQUEST_TIMEOUT = 11
BASE_DEBUG = False
BASE_HEADERS = {"X-Test": "base"}


def create_sandbox(monkeypatch) -> Sandbox:
dummy_transport = SimpleNamespace(pool=object())

monkeypatch.setattr(
sandbox_sync_main, "get_transport", lambda *_args, **_kwargs: dummy_transport
)
monkeypatch.setattr(
sandbox_sync_main.httpx, "Client", lambda *args, **kwargs: object()
)
monkeypatch.setattr(
sandbox_sync_main, "Filesystem", lambda *args, **kwargs: object()
)
monkeypatch.setattr(sandbox_sync_main, "Commands", lambda *args, **kwargs: object())
monkeypatch.setattr(sandbox_sync_main, "Pty", lambda *args, **kwargs: object())
monkeypatch.setattr(sandbox_sync_main, "Git", lambda *args, **kwargs: object())

return Sandbox(
sandbox_id="sbx-test",
sandbox_domain="sandbox.e2b.dev",
envd_version=Version("0.2.4"),
envd_access_token="tok",
traffic_access_token="tok",
connection_config=ConnectionConfig(
api_key=BASE_API_KEY,
domain=BASE_DOMAIN,
request_timeout=BASE_REQUEST_TIMEOUT,
debug=BASE_DEBUG,
headers=BASE_HEADERS,
),
)


@pytest.mark.skip_debug()
def test_pause_passes_connection_config_without_overrides(monkeypatch):
mock_pause = Mock(return_value="sbx-test")
monkeypatch.setattr(sandbox_sync_main.SandboxApi, "_cls_pause", mock_pause)

sandbox = create_sandbox(monkeypatch)
sandbox.pause()

mock_pause.assert_called_once()
assert mock_pause.call_args.kwargs["sandbox_id"] == "sbx-test"
assert mock_pause.call_args.kwargs["api_key"] == BASE_API_KEY
assert mock_pause.call_args.kwargs["domain"] == BASE_DOMAIN
assert mock_pause.call_args.kwargs["request_timeout"] == BASE_REQUEST_TIMEOUT
assert mock_pause.call_args.kwargs["debug"] == BASE_DEBUG
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]


@pytest.mark.skip_debug()
def test_pause_applies_overrides(monkeypatch):
mock_pause = Mock(return_value="sbx-test")
monkeypatch.setattr(sandbox_sync_main.SandboxApi, "_cls_pause", mock_pause)

sandbox = create_sandbox(monkeypatch)
sandbox.pause(
domain="override.e2b.dev",
request_timeout=20,
headers={"X-Extra": "1"},
)

mock_pause.assert_called_once()
assert mock_pause.call_args.kwargs["sandbox_id"] == "sbx-test"
assert mock_pause.call_args.kwargs["api_key"] == BASE_API_KEY
assert mock_pause.call_args.kwargs["domain"] == "override.e2b.dev"
assert mock_pause.call_args.kwargs["request_timeout"] == 20
assert mock_pause.call_args.kwargs["debug"] == BASE_DEBUG
assert mock_pause.call_args.kwargs["headers"]["X-Test"] == BASE_HEADERS["X-Test"]
assert mock_pause.call_args.kwargs["headers"]["X-Extra"] == "1"
Loading