Skip to content
Open
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
39 changes: 39 additions & 0 deletions packages/js-sdk/tests/volume/mount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { assert } from 'vitest'

import { Sandbox, Volume } from '../../src'
import { hostedTest, template } from '../setup'

/**
* Volume content persisting across sandboxes is server-side behavior — the
* mount happens on real compute, so this is the one volume test that can't be
* mocked. Everything else about volumes (CRUD, pagination, error mapping, the
* content API) is asserted against a mocked transport in the unit tier.
*/
hostedTest('a mounted volume persists content across sandboxes', async () => {
const volume = await Volume.create(`test-mount-${Date.now()}`)

try {
const writer = await Sandbox.create(template, {
volumeMounts: { '/mnt/data': volume },
})
try {
await writer.files.write('/mnt/data/hello.txt', 'written by the writer')
} finally {
await writer.kill()
}

const reader = await Sandbox.create(template, {
volumeMounts: { '/mnt/data': volume },
})
try {
assert.equal(
await reader.files.read('/mnt/data/hello.txt'),
'written by the writer'
)
} finally {
await reader.kill()
}
} finally {
await Volume.destroy(volume.volumeId)
}
})
56 changes: 56 additions & 0 deletions packages/js-sdk/tests/volume/mountPayload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { afterAll, afterEach, beforeAll, expect, test } from 'vitest'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'

import { Sandbox, Volume } from '../../src'
import { apiUrl, TEST_API_KEY } from '../setup'

let lastCreateBody: Record<string, unknown> | undefined

const server = setupServer(
http.post(apiUrl('/sandboxes'), async ({ request }) => {
lastCreateBody = (await request.json()) as Record<string, unknown>
return HttpResponse.json({
sandboxID: 'test-sandbox-id',
templateID: 'base',
envdVersion: '0.2.4',
})
})
)

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterAll(() => server.close())
afterEach(() => {
lastCreateBody = undefined
server.resetHandlers()
})

test('Sandbox.create omits volumeMounts when none are requested', async () => {
await Sandbox.create('base', { apiKey: TEST_API_KEY })

expect(lastCreateBody).not.toHaveProperty('volumeMounts')
})

test('Sandbox.create maps mount paths to named volume mounts', async () => {
await Sandbox.create('base', {
apiKey: TEST_API_KEY,
volumeMounts: { '/mnt/data': 'my-volume' },
})

expect(lastCreateBody?.volumeMounts).toEqual([
{ name: 'my-volume', path: '/mnt/data' },
])
})

test('Sandbox.create accepts a Volume instance as the mount source', async () => {
const volume = new Volume('vol-1', 'my-volume', 'volume-token')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

T-4 — objects with remote lifecycles are obtained through async static factories (Volume.create, Volume.list); constructors are internal wiring. This is the first new Volume(...) call site in the JS SDK, and a test is documentation: it cements a construction path TASTE says users never take, and it also spells out the optional token positionally, which is exactly the chain T-3 rules out.

msw is already intercepting here, so the compliant form is to get the volume the way a caller would — add a http.post(apiUrl('/volumes'), …) handler returning { volumeID: 'vol-1', name: 'my-volume', token: 'volume-token' } and then const volume = await Volume.create('my-volume', { apiKey: TEST_API_KEY }). That keeps the test asserting the public path end to end instead of a hand-assembled object.


await Sandbox.create('base', {
apiKey: TEST_API_KEY,
volumeMounts: { '/mnt/data': volume },
})

expect(lastCreateBody?.volumeMounts).toEqual([
{ name: 'my-volume', path: '/mnt/data' },
])
})
35 changes: 35 additions & 0 deletions packages/python-sdk/tests/async/volume_async/test_mount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Async counterpart of `tests/sync/volume_sync/test_mount.py`."""

from uuid import uuid4

import pytest

from e2b import AsyncSandbox, AsyncVolume


@pytest.mark.e2e
@pytest.mark.skip_debug()
async def test_mounted_volume_persists_content_across_sandboxes(template):
volume = await AsyncVolume.create(f"test-mount-{uuid4()}")

try:
writer = await AsyncSandbox.create(
template, volume_mounts={"/mnt/data": volume}
)
try:
await writer.files.write("/mnt/data/hello.txt", "written by the writer")
finally:
await writer.kill()

reader = await AsyncSandbox.create(
template, volume_mounts={"/mnt/data": volume}
)
try:
assert (
await reader.files.read("/mnt/data/hello.txt")
== "written by the writer"
)
finally:
await reader.kill()
finally:
await AsyncVolume.destroy(volume.volume_id)
77 changes: 77 additions & 0 deletions packages/python-sdk/tests/shared/volume/test_mount_payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Volume mounts in the sandbox create request — pure client-side shaping.

Mirrors `tests/volume/mountPayload.test.ts` in the JS SDK. The mount itself is
server-side behavior and is covered by the e2e tier (`test_mount.py`).
"""

from types import SimpleNamespace
from typing import Any, Dict
from unittest.mock import AsyncMock, Mock

from e2b import AsyncSandbox, Sandbox, Volume
from e2b.api.client.api.sandboxes import post_sandboxes
from e2b.api.client.models import Sandbox as SandboxModel


def _created_sandbox():
return SimpleNamespace(
status_code=200,
parsed=SandboxModel(
client_id="client-id",
envd_version="0.2.4",
sandbox_id="sbx-test",
template_id="template-id",
),
)


def _sync_request_body(monkeypatch, api_key: str, volume_mounts) -> Dict[str, Any]:
request = Mock(return_value=_created_sandbox())
monkeypatch.setattr(post_sandboxes, "sync_detailed", request)

Sandbox.create(api_key=api_key, volume_mounts=volume_mounts)

return request.call_args.kwargs["body"].to_dict()


async def _async_request_body(
monkeypatch, api_key: str, volume_mounts
) -> Dict[str, Any]:
request = AsyncMock(return_value=_created_sandbox())
monkeypatch.setattr(post_sandboxes, "asyncio_detailed", request)

await AsyncSandbox.create(api_key=api_key, volume_mounts=volume_mounts)

return request.call_args.kwargs["body"].to_dict()


def test_create_omits_volume_mounts_when_none_are_requested(monkeypatch, test_api_key):
body = _sync_request_body(monkeypatch, test_api_key, None)

assert "volumeMounts" not in body


def test_create_maps_mount_paths_to_named_volume_mounts(monkeypatch, test_api_key):
body = _sync_request_body(monkeypatch, test_api_key, {"/mnt/data": "my-volume"})

assert body["volumeMounts"] == [{"name": "my-volume", "path": "/mnt/data"}]


def test_create_accepts_a_volume_instance_as_the_mount_source(
monkeypatch, test_api_key
):
volume = Volume("vol-1", "my-volume", "volume-token")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

T-4 / T-3 — same constructor point as the JS side, plus a local inconsistency: token, domain, debug and proxy are optional on Volume.__init__, and every existing Volume test constructs by keyword (tests/sync/volume_sync/test_volume_content.py, tests/test_volume_client.py). Passing them positionally is the argument order TASTE rejects.

At minimum match the existing convention:

Suggested change
volume = Volume("vol-1", "my-volume", "volume-token")
volume = Volume(volume_id="vol-1", name="my-volume", token="volume-token")

Better, mirror whatever the JS test ends up doing and obtain the instance from Volume.create against the mocked transport.


body = _sync_request_body(monkeypatch, test_api_key, {"/mnt/data": volume})

assert body["volumeMounts"] == [{"name": "my-volume", "path": "/mnt/data"}]


async def test_async_create_maps_mount_paths_to_named_volume_mounts(
monkeypatch, test_api_key
):
body = await _async_request_body(
monkeypatch, test_api_key, {"/mnt/data": "my-volume"}
)

assert body["volumeMounts"] == [{"name": "my-volume", "path": "/mnt/data"}]
Comment on lines +70 to +77

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

T-1 / T-2 — sync and async are full mirrors with the same method names and semantics, so their coverage shouldn't diverge either. Sync gets three cases (omitted, string name, Volume instance); async gets only the string-name one. The gap that matters is the instance case: AsyncSandbox.create maps AsyncVolume instances through its own transform in sandbox_async/main.py, and that mapping is asserted nowhere in the mocked tier — the only place an AsyncVolume mount source appears is the e2e test, which won't run by default.

Add the two missing async cases (None → no volumeMounts key, and {"/mnt/data": AsyncVolume(...)}[{"name": …, "path": …}]) so the async mirror is complete. _async_request_body already takes the mounts argument, so each is a three-line test.

33 changes: 33 additions & 0 deletions packages/python-sdk/tests/sync/volume_sync/test_mount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Volume content persisting across sandboxes — real mounts, real compute.

Everything else about volumes (CRUD, pagination, error mapping, the content
API) is asserted against a mocked transport in the default tier; only the mount
behavior needs live infrastructure. Mirrors `tests/volume/mount.test.ts`.
"""

from uuid import uuid4

import pytest

from e2b import Sandbox, Volume


@pytest.mark.e2e
@pytest.mark.skip_debug()
def test_mounted_volume_persists_content_across_sandboxes(template):
volume = Volume.create(f"test-mount-{uuid4()}")

try:
writer = Sandbox.create(template, volume_mounts={"/mnt/data": volume})
try:
writer.files.write("/mnt/data/hello.txt", "written by the writer")
finally:
writer.kill()

reader = Sandbox.create(template, volume_mounts={"/mnt/data": volume})
try:
assert reader.files.read("/mnt/data/hello.txt") == "written by the writer"
finally:
reader.kill()
finally:
Volume.destroy(volume.volume_id)
Loading