3614 lines
126 KiB
Python
3614 lines
126 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import builtins
|
|
import importlib
|
|
import io
|
|
import os
|
|
import sys
|
|
import tarfile
|
|
import types
|
|
from collections.abc import Callable
|
|
from pathlib import Path, PureWindowsPath
|
|
from typing import Any, NoReturn, cast
|
|
|
|
import pytest
|
|
from pydantic import Field, PrivateAttr
|
|
|
|
from agents.sandbox import Manifest
|
|
from agents.sandbox.config import DEFAULT_PYTHON_SANDBOX_IMAGE
|
|
from agents.sandbox.entries import (
|
|
File,
|
|
GCSMount,
|
|
InContainerMountStrategy,
|
|
Mount,
|
|
MountpointMountPattern,
|
|
R2Mount,
|
|
S3Mount,
|
|
)
|
|
from agents.sandbox.entries.mounts.base import InContainerMountAdapter
|
|
from agents.sandbox.errors import (
|
|
InvalidManifestPathError,
|
|
MountConfigError,
|
|
WorkspaceArchiveReadError,
|
|
)
|
|
from agents.sandbox.files import EntryKind
|
|
from agents.sandbox.manifest import Environment
|
|
from agents.sandbox.materialization import MaterializedFile
|
|
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
|
|
from agents.sandbox.session.runtime_helpers import (
|
|
RESOLVE_WORKSPACE_PATH_HELPER,
|
|
WORKSPACE_FINGERPRINT_HELPER,
|
|
)
|
|
from agents.sandbox.snapshot import LocalSnapshot
|
|
from agents.sandbox.types import ExecResult
|
|
|
|
|
|
def _with_aio(fn: Callable[..., object]) -> Callable[..., object]:
|
|
def _sync(*args: object, **kwargs: object) -> object:
|
|
return fn(*args, **kwargs)
|
|
|
|
async def _aio(*args: object, **kwargs: object) -> object:
|
|
return fn(*args, **kwargs)
|
|
|
|
_sync.aio = _aio # type: ignore[attr-defined]
|
|
return _sync
|
|
|
|
|
|
def _set_aio_attr(obj: object, name: str, fn: Callable[..., object]) -> None:
|
|
setattr(obj, name, _with_aio(fn))
|
|
|
|
|
|
class _RecordingMount(Mount):
|
|
type: str = "modal_recording_mount"
|
|
mount_strategy: InContainerMountStrategy = Field(
|
|
default_factory=lambda: InContainerMountStrategy(pattern=MountpointMountPattern())
|
|
)
|
|
_events: list[tuple[str, str]] = PrivateAttr(default_factory=list)
|
|
_teardown_error: str | None = PrivateAttr(default=None)
|
|
|
|
def bind_events(self, events: list[tuple[str, str]]) -> _RecordingMount:
|
|
self._events = events
|
|
return self
|
|
|
|
def bind_teardown_error(self, message: str) -> _RecordingMount:
|
|
self._teardown_error = message
|
|
return self
|
|
|
|
def supported_in_container_patterns(
|
|
self,
|
|
) -> tuple[builtins.type[MountpointMountPattern], ...]:
|
|
return (MountpointMountPattern,)
|
|
|
|
def build_docker_volume_driver_config(
|
|
self,
|
|
strategy: object,
|
|
) -> tuple[str, dict[str, str], bool]:
|
|
_ = strategy
|
|
raise MountConfigError(
|
|
message="docker-volume mounts are not supported for this mount type",
|
|
context={"mount_type": self.type},
|
|
)
|
|
|
|
def in_container_adapter(self) -> InContainerMountAdapter:
|
|
mount = self
|
|
|
|
class _Adapter(InContainerMountAdapter):
|
|
def validate(self, strategy: InContainerMountStrategy) -> None:
|
|
_ = strategy
|
|
|
|
async def activate(
|
|
self,
|
|
strategy: InContainerMountStrategy,
|
|
session: BaseSandboxSession,
|
|
dest: Path,
|
|
base_dir: Path,
|
|
) -> list[MaterializedFile]:
|
|
_ = (strategy, session, dest, base_dir)
|
|
return []
|
|
|
|
async def deactivate(
|
|
self,
|
|
strategy: InContainerMountStrategy,
|
|
session: BaseSandboxSession,
|
|
dest: Path,
|
|
base_dir: Path,
|
|
) -> None:
|
|
_ = (strategy, session, dest, base_dir)
|
|
|
|
async def teardown_for_snapshot(
|
|
self,
|
|
strategy: InContainerMountStrategy,
|
|
session: BaseSandboxSession,
|
|
path: Path,
|
|
) -> None:
|
|
_ = (strategy, session)
|
|
if mount._teardown_error is not None:
|
|
raise RuntimeError(mount._teardown_error)
|
|
mount._events.append(("unmount", path.as_posix()))
|
|
|
|
async def restore_after_snapshot(
|
|
self,
|
|
strategy: InContainerMountStrategy,
|
|
session: BaseSandboxSession,
|
|
path: Path,
|
|
) -> None:
|
|
_ = (strategy, session)
|
|
mount._events.append(("mount", path.as_posix()))
|
|
|
|
return _Adapter(self)
|
|
|
|
|
|
def _load_modal_module(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> tuple[Any, list[dict[str, object]], list[str]]:
|
|
create_calls: list[dict[str, object]] = []
|
|
registry_tags: list[str] = []
|
|
|
|
class _FakeImage:
|
|
object_id = "im-123"
|
|
from_id_calls: list[str] = []
|
|
|
|
def __init__(self, object_id: str | None = None) -> None:
|
|
if object_id is not None:
|
|
self.object_id = object_id
|
|
self.cmd_calls: list[list[str]] = []
|
|
|
|
@staticmethod
|
|
def from_registry(_tag: str) -> _FakeImage:
|
|
registry_tags.append(_tag)
|
|
return _FakeImage()
|
|
|
|
@staticmethod
|
|
def from_id(_image_id: str) -> _FakeImage:
|
|
_FakeImage.from_id_calls.append(_image_id)
|
|
return _FakeImage(object_id=_image_id)
|
|
|
|
def cmd(self, command: list[str]) -> _FakeImage:
|
|
self.cmd_calls.append(command)
|
|
return self
|
|
|
|
class _FakeSandboxInstance:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.terminate_calls = 0
|
|
self.terminate_kwargs: list[dict[str, object]] = []
|
|
self.mount_image_calls: list[tuple[str, str | None]] = []
|
|
self.terminate = _with_aio(self._terminate)
|
|
self.poll = _with_aio(self._poll)
|
|
self.tunnels = _with_aio(self._tunnels)
|
|
self.exec = _with_aio(self._exec)
|
|
self.snapshot_directory = _with_aio(self._snapshot_directory)
|
|
self.mount_image = _with_aio(self._mount_image)
|
|
|
|
def _terminate(self, **kwargs: object) -> None:
|
|
self.terminate_calls += 1
|
|
self.terminate_kwargs.append(kwargs)
|
|
|
|
def _poll(self) -> None:
|
|
return None
|
|
|
|
def _tunnels(self, timeout: int = 50) -> dict[int, object]:
|
|
_ = timeout
|
|
return {
|
|
8765: types.SimpleNamespace(
|
|
host="sandbox.example.test",
|
|
port=443,
|
|
unencrypted_host="",
|
|
unencrypted_port=0,
|
|
)
|
|
}
|
|
|
|
def _snapshot_directory(self, _path: str) -> _FakeImage:
|
|
return _FakeImage()
|
|
|
|
def _mount_image(self, path: str, image: object) -> None:
|
|
self.mount_image_calls.append((path, getattr(image, "object_id", None)))
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = (command, kwargs)
|
|
resolve_helper_path = str(RESOLVE_WORKSPACE_PATH_HELPER.install_path)
|
|
fingerprint_helper_path = str(WORKSPACE_FINGERPRINT_HELPER.install_path)
|
|
|
|
class _FakeStream:
|
|
def __init__(self, payload: bytes = b"") -> None:
|
|
self.read = _with_aio(lambda: payload)
|
|
|
|
stdout = b""
|
|
if (
|
|
command[:2] == ("sh", "-c")
|
|
and isinstance(command[2], str)
|
|
and RESOLVE_WORKSPACE_PATH_HELPER.install_marker in command[2]
|
|
):
|
|
return types.SimpleNamespace(
|
|
stdout=_FakeStream(),
|
|
stderr=_FakeStream(),
|
|
wait=_with_aio(lambda: 0),
|
|
)
|
|
if command and command[0] == resolve_helper_path:
|
|
stdout = str(command[2]).encode("utf-8")
|
|
if command and command[0] == fingerprint_helper_path:
|
|
stdout = (
|
|
b'{"fingerprint":"fake-workspace-fingerprint",'
|
|
b'"version":"workspace_tar_sha256_v1"}\n'
|
|
)
|
|
if command == ("test", "-d", "/workspace"):
|
|
return types.SimpleNamespace(
|
|
stdout=_FakeStream(),
|
|
stderr=_FakeStream(),
|
|
wait=_with_aio(lambda: 1),
|
|
)
|
|
|
|
return types.SimpleNamespace(
|
|
stdout=_FakeStream(stdout),
|
|
stderr=_FakeStream(),
|
|
wait=_with_aio(lambda: 0),
|
|
)
|
|
|
|
class _FakeSandbox:
|
|
from_id_calls: list[str] = []
|
|
create: Any
|
|
from_id: Any
|
|
|
|
@staticmethod
|
|
def _create(**kwargs: object) -> _FakeSandboxInstance:
|
|
create_calls.append(
|
|
dict(
|
|
kwargs,
|
|
modal_image_builder_version_env=os.environ.get("MODAL_IMAGE_BUILDER_VERSION"),
|
|
)
|
|
)
|
|
return _FakeSandboxInstance()
|
|
|
|
@staticmethod
|
|
def _from_id(_sandbox_id: str) -> _FakeSandboxInstance:
|
|
_FakeSandbox.from_id_calls.append(_sandbox_id)
|
|
return _FakeSandboxInstance()
|
|
|
|
class _FakeApp:
|
|
lookup: Any
|
|
|
|
@staticmethod
|
|
def _lookup(_name: str, *, create_if_missing: bool = False) -> object:
|
|
_ = create_if_missing
|
|
return object()
|
|
|
|
class _FakeSecret:
|
|
def __init__(
|
|
self,
|
|
value: dict[str, str] | None = None,
|
|
*,
|
|
name: str | None = None,
|
|
environment_name: str | None = None,
|
|
) -> None:
|
|
self.value = value
|
|
self.name = name
|
|
self.environment_name = environment_name
|
|
|
|
@staticmethod
|
|
def from_dict(value: dict[str, str]) -> _FakeSecret:
|
|
return _FakeSecret(value)
|
|
|
|
@staticmethod
|
|
def from_name(name: str, *, environment_name: str | None = None) -> _FakeSecret:
|
|
return _FakeSecret(name=name, environment_name=environment_name)
|
|
|
|
class _FakeCloudBucketMount:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
bucket_name: str,
|
|
bucket_endpoint_url: str | None = None,
|
|
key_prefix: str | None = None,
|
|
secret: _FakeSecret | None = None,
|
|
read_only: bool = True,
|
|
) -> None:
|
|
self.bucket_name = bucket_name
|
|
self.bucket_endpoint_url = bucket_endpoint_url
|
|
self.key_prefix = key_prefix
|
|
self.secret = secret
|
|
self.read_only = read_only
|
|
|
|
class _FakeConfig:
|
|
override_calls: list[tuple[str, str]] = []
|
|
|
|
@staticmethod
|
|
def override_locally(key: str, value: str) -> None:
|
|
_FakeConfig.override_calls.append((key, value))
|
|
os.environ["MODAL_" + key.upper()] = value
|
|
|
|
class _FakeModalError(Exception):
|
|
pass
|
|
|
|
class _FakeModalConnectionError(_FakeModalError):
|
|
pass
|
|
|
|
class _FakeModalExecTimeoutError(TimeoutError):
|
|
pass
|
|
|
|
class _FakeModalInternalFailure(_FakeModalError):
|
|
pass
|
|
|
|
class _FakeModalInvalidError(_FakeModalError):
|
|
pass
|
|
|
|
class _FakeModalNotFoundError(_FakeModalError):
|
|
pass
|
|
|
|
_FakeSandbox.create = staticmethod(_with_aio(_FakeSandbox._create))
|
|
_FakeSandbox.from_id = staticmethod(_with_aio(_FakeSandbox._from_id))
|
|
_FakeApp.lookup = staticmethod(_with_aio(_FakeApp._lookup))
|
|
|
|
fake_modal: Any = types.ModuleType("modal")
|
|
fake_modal.Image = _FakeImage
|
|
fake_modal.App = _FakeApp
|
|
fake_modal.Sandbox = _FakeSandbox
|
|
fake_modal.Secret = _FakeSecret
|
|
fake_modal.CloudBucketMount = _FakeCloudBucketMount
|
|
|
|
fake_modal_exception: Any = types.ModuleType("modal.exception")
|
|
fake_modal_exception.ConnectionError = _FakeModalConnectionError
|
|
fake_modal_exception.ExecTimeoutError = _FakeModalExecTimeoutError
|
|
fake_modal_exception.InternalFailure = _FakeModalInternalFailure
|
|
fake_modal_exception.InvalidError = _FakeModalInvalidError
|
|
fake_modal_exception.NotFoundError = _FakeModalNotFoundError
|
|
fake_modal.exception = fake_modal_exception
|
|
|
|
fake_modal_config: Any = types.ModuleType("modal.config")
|
|
fake_modal_config.config = _FakeConfig
|
|
|
|
fake_container_process: Any = types.ModuleType("modal.container_process")
|
|
fake_container_process.ContainerProcess = object
|
|
|
|
monkeypatch.setitem(sys.modules, "modal", fake_modal)
|
|
monkeypatch.setitem(sys.modules, "modal.exception", fake_modal_exception)
|
|
monkeypatch.setitem(sys.modules, "modal.config", fake_modal_config)
|
|
monkeypatch.setitem(sys.modules, "modal.container_process", fake_container_process)
|
|
sys.modules.pop("agents.extensions.sandbox.modal.sandbox", None)
|
|
sys.modules.pop("agents.extensions.sandbox.modal.mounts", None)
|
|
sys.modules.pop("agents.extensions.sandbox.modal", None)
|
|
|
|
module: Any = importlib.import_module("agents.extensions.sandbox.modal.sandbox")
|
|
return module, create_calls, registry_tags
|
|
|
|
|
|
def test_modal_package_re_exports_backend_symbols(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
package_module = importlib.import_module("agents.extensions.sandbox.modal")
|
|
|
|
assert package_module.ModalSandboxClient is modal_module.ModalSandboxClient
|
|
assert (
|
|
package_module.ModalCloudBucketMountStrategy is modal_module.ModalCloudBucketMountStrategy
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_passes_manifest_environment(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
await client.create(
|
|
manifest=Manifest(environment=Environment(value={"SANDBOX_FLAG": "enabled"})),
|
|
options=modal_module.ModalSandboxClientOptions(app_name="sandbox-tests"),
|
|
)
|
|
|
|
assert create_calls
|
|
assert create_calls[0]["env"] == {"SANDBOX_FLAG": "enabled"}
|
|
assert create_calls[0]["modal_image_builder_version_env"] == "2025.06"
|
|
assert registry_tags == [DEFAULT_PYTHON_SANDBOX_IMAGE]
|
|
image = cast(Any, create_calls[0]["image"])
|
|
assert image.cmd_calls == [["sleep", "infinity"]]
|
|
assert os.environ.get("MODAL_IMAGE_BUILDER_VERSION") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_passes_idle_timeout(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.create(
|
|
options=modal_module.ModalSandboxClientOptions(
|
|
app_name="sandbox-tests",
|
|
idle_timeout=60,
|
|
),
|
|
)
|
|
|
|
assert create_calls
|
|
assert create_calls[0]["idle_timeout"] == 60
|
|
assert session.state.idle_timeout == 60
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_sets_default_cmd_for_custom_registry_image(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient(
|
|
image=modal_module.ModalImageSelector.from_tag("debian:bookworm-slim")
|
|
)
|
|
await client.create(
|
|
options=modal_module.ModalSandboxClientOptions(app_name="sandbox-tests"),
|
|
)
|
|
|
|
assert create_calls
|
|
assert registry_tags == ["debian:bookworm-slim"]
|
|
image = cast(Any, create_calls[0]["image"])
|
|
assert image.cmd_calls == [["sleep", "infinity"]]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_can_opt_out_of_default_cmd(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
await client.create(
|
|
options=modal_module.ModalSandboxClientOptions(
|
|
app_name="sandbox-tests",
|
|
use_sleep_cmd=False,
|
|
),
|
|
)
|
|
|
|
assert create_calls
|
|
assert registry_tags == [DEFAULT_PYTHON_SANDBOX_IMAGE]
|
|
image = cast(Any, create_calls[0]["image"])
|
|
assert image.cmd_calls == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_uses_custom_image_builder_version(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.create(
|
|
options=modal_module.ModalSandboxClientOptions(
|
|
app_name="sandbox-tests",
|
|
image_builder_version="PREVIEW",
|
|
),
|
|
)
|
|
|
|
assert create_calls
|
|
assert create_calls[0]["modal_image_builder_version_env"] == "PREVIEW"
|
|
assert session.state.image_builder_version == "PREVIEW"
|
|
assert os.environ.get("MODAL_IMAGE_BUILDER_VERSION") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_uses_existing_config_when_image_builder_version_is_none(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
monkeypatch.setenv("MODAL_IMAGE_BUILDER_VERSION", "USER-CONFIGURED")
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.create(
|
|
options=modal_module.ModalSandboxClientOptions(
|
|
app_name="sandbox-tests",
|
|
image_builder_version=None,
|
|
),
|
|
)
|
|
|
|
assert create_calls
|
|
assert create_calls[0]["modal_image_builder_version_env"] == "USER-CONFIGURED"
|
|
assert session.state.image_builder_version is None
|
|
assert os.environ.get("MODAL_IMAGE_BUILDER_VERSION") == "USER-CONFIGURED"
|
|
|
|
|
|
def test_modal_deserialize_session_state_defaults_missing_image_builder_version(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
image_builder_version="PREVIEW",
|
|
)
|
|
payload = state.model_dump(mode="json")
|
|
payload.pop("image_builder_version")
|
|
|
|
restored = modal_module.ModalSandboxClient().deserialize_session_state(
|
|
cast(dict[str, object], payload)
|
|
)
|
|
|
|
assert restored.image_builder_version == "2025.06"
|
|
|
|
|
|
def test_modal_deserialize_session_state_defaults_missing_idle_timeout(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
idle_timeout=60,
|
|
)
|
|
payload = state.model_dump(mode="json")
|
|
payload.pop("idle_timeout")
|
|
|
|
restored = modal_module.ModalSandboxClient().deserialize_session_state(
|
|
cast(dict[str, object], payload)
|
|
)
|
|
|
|
assert restored.idle_timeout is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_passes_modal_cloud_bucket_mounts(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
await client.create(
|
|
manifest=Manifest(
|
|
entries={
|
|
"remote": S3Mount(
|
|
bucket="bucket",
|
|
access_key_id="access-key",
|
|
secret_access_key="secret-key",
|
|
prefix="nested/prefix/",
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(),
|
|
read_only=False,
|
|
)
|
|
}
|
|
),
|
|
options=modal_module.ModalSandboxClientOptions(app_name="sandbox-tests"),
|
|
)
|
|
|
|
assert create_calls
|
|
volumes = create_calls[0]["volumes"]
|
|
assert isinstance(volumes, dict)
|
|
assert volumes.keys() == {"/workspace/remote"}
|
|
mount = volumes["/workspace/remote"]
|
|
assert mount.bucket_name == "bucket"
|
|
assert mount.bucket_endpoint_url is None
|
|
assert mount.key_prefix == "nested/prefix/"
|
|
assert mount.secret.value == {
|
|
"AWS_ACCESS_KEY_ID": "access-key",
|
|
"AWS_SECRET_ACCESS_KEY": "secret-key",
|
|
}
|
|
assert mount.read_only is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_passes_named_modal_secret_for_cloud_bucket_mount(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
await client.create(
|
|
manifest=Manifest(
|
|
entries={
|
|
"remote": S3Mount(
|
|
bucket="bucket",
|
|
prefix="nested/prefix/",
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(
|
|
secret_name="named-modal-secret"
|
|
),
|
|
read_only=False,
|
|
)
|
|
}
|
|
),
|
|
options=modal_module.ModalSandboxClientOptions(app_name="sandbox-tests"),
|
|
)
|
|
|
|
assert create_calls
|
|
volumes = create_calls[0]["volumes"]
|
|
assert isinstance(volumes, dict)
|
|
assert volumes.keys() == {"/workspace/remote"}
|
|
mount = volumes["/workspace/remote"]
|
|
assert mount.bucket_name == "bucket"
|
|
assert mount.bucket_endpoint_url is None
|
|
assert mount.key_prefix == "nested/prefix/"
|
|
assert mount.secret.name == "named-modal-secret"
|
|
assert mount.secret.value is None
|
|
assert mount.read_only is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_passes_named_modal_secret_environment_for_cloud_bucket_mount(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
await client.create(
|
|
manifest=Manifest(
|
|
entries={
|
|
"remote": S3Mount(
|
|
bucket="bucket",
|
|
prefix="nested/prefix/",
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(
|
|
secret_name="named-modal-secret",
|
|
secret_environment_name="staging",
|
|
),
|
|
read_only=False,
|
|
)
|
|
}
|
|
),
|
|
options=modal_module.ModalSandboxClientOptions(app_name="sandbox-tests"),
|
|
)
|
|
|
|
assert create_calls
|
|
volumes = create_calls[0]["volumes"]
|
|
assert isinstance(volumes, dict)
|
|
mount = volumes["/workspace/remote"]
|
|
assert mount.secret.name == "named-modal-secret"
|
|
assert mount.secret.environment_name == "staging"
|
|
assert mount.secret.value is None
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_round_trips_through_manifest_parse(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
manifest = Manifest.model_validate(
|
|
{
|
|
"entries": {
|
|
"remote": {
|
|
"type": "s3_mount",
|
|
"bucket": "bucket",
|
|
"mount_strategy": {"type": "modal_cloud_bucket"},
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
mount = manifest.entries["remote"]
|
|
|
|
assert isinstance(mount, S3Mount)
|
|
assert isinstance(mount.mount_strategy, modal_module.ModalCloudBucketMountStrategy)
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_round_trips_secret_name_through_manifest_parse(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
manifest = Manifest.model_validate(
|
|
{
|
|
"entries": {
|
|
"remote": {
|
|
"type": "s3_mount",
|
|
"bucket": "bucket",
|
|
"mount_strategy": {
|
|
"type": "modal_cloud_bucket",
|
|
"secret_name": "named-modal-secret",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
mount = manifest.entries["remote"]
|
|
|
|
assert isinstance(mount, S3Mount)
|
|
assert isinstance(mount.mount_strategy, modal_module.ModalCloudBucketMountStrategy)
|
|
assert mount.mount_strategy.secret_name == "named-modal-secret"
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_round_trips_secret_env_name(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
manifest = Manifest.model_validate(
|
|
{
|
|
"entries": {
|
|
"remote": {
|
|
"type": "s3_mount",
|
|
"bucket": "bucket",
|
|
"mount_strategy": {
|
|
"type": "modal_cloud_bucket",
|
|
"secret_name": "named-modal-secret",
|
|
"secret_environment_name": "staging",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
mount = manifest.entries["remote"]
|
|
|
|
assert isinstance(mount, S3Mount)
|
|
assert isinstance(mount.mount_strategy, modal_module.ModalCloudBucketMountStrategy)
|
|
assert mount.mount_strategy.secret_name == "named-modal-secret"
|
|
assert mount.mount_strategy.secret_environment_name == "staging"
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_builds_s3_config(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
strategy = modal_module.ModalCloudBucketMountStrategy()
|
|
mount = S3Mount(
|
|
bucket="bucket",
|
|
access_key_id="access-key",
|
|
secret_access_key="secret-key",
|
|
session_token="session-token",
|
|
prefix="nested/prefix/",
|
|
endpoint_url="https://s3.example.test",
|
|
mount_strategy=strategy,
|
|
read_only=False,
|
|
)
|
|
|
|
config = strategy._build_modal_cloud_bucket_mount_config(mount) # noqa: SLF001
|
|
|
|
assert config.bucket_name == "bucket"
|
|
assert config.bucket_endpoint_url == "https://s3.example.test"
|
|
assert config.key_prefix == "nested/prefix/"
|
|
assert config.credentials == {
|
|
"AWS_ACCESS_KEY_ID": "access-key",
|
|
"AWS_SECRET_ACCESS_KEY": "secret-key",
|
|
"AWS_SESSION_TOKEN": "session-token",
|
|
}
|
|
assert config.read_only is False
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_builds_s3_config_with_named_secret(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
strategy = modal_module.ModalCloudBucketMountStrategy(secret_name="named-modal-secret")
|
|
mount = S3Mount(
|
|
bucket="bucket",
|
|
prefix="nested/prefix/",
|
|
mount_strategy=strategy,
|
|
read_only=False,
|
|
)
|
|
|
|
config = strategy._build_modal_cloud_bucket_mount_config(mount) # noqa: SLF001
|
|
|
|
assert config.bucket_name == "bucket"
|
|
assert config.bucket_endpoint_url is None
|
|
assert config.key_prefix == "nested/prefix/"
|
|
assert config.credentials is None
|
|
assert config.secret_name == "named-modal-secret"
|
|
assert config.secret_environment_name is None
|
|
assert config.read_only is False
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_builds_s3_config_with_named_secret_environment(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
strategy = modal_module.ModalCloudBucketMountStrategy(
|
|
secret_name="named-modal-secret",
|
|
secret_environment_name="staging",
|
|
)
|
|
mount = S3Mount(
|
|
bucket="bucket",
|
|
prefix="nested/prefix/",
|
|
mount_strategy=strategy,
|
|
read_only=False,
|
|
)
|
|
|
|
config = strategy._build_modal_cloud_bucket_mount_config(mount) # noqa: SLF001
|
|
|
|
assert config.bucket_name == "bucket"
|
|
assert config.credentials is None
|
|
assert config.secret_name == "named-modal-secret"
|
|
assert config.secret_environment_name == "staging"
|
|
assert config.read_only is False
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_builds_r2_config(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
strategy = modal_module.ModalCloudBucketMountStrategy()
|
|
mount = R2Mount(
|
|
bucket="bucket",
|
|
account_id="abc123accountid",
|
|
access_key_id="access-key",
|
|
secret_access_key="secret-key",
|
|
mount_strategy=strategy,
|
|
)
|
|
|
|
config = strategy._build_modal_cloud_bucket_mount_config(mount) # noqa: SLF001
|
|
|
|
assert config.bucket_name == "bucket"
|
|
assert config.bucket_endpoint_url == "https://abc123accountid.r2.cloudflarestorage.com"
|
|
assert config.key_prefix is None
|
|
assert config.credentials == {
|
|
"AWS_ACCESS_KEY_ID": "access-key",
|
|
"AWS_SECRET_ACCESS_KEY": "secret-key",
|
|
}
|
|
assert config.read_only is True
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_builds_gcs_hmac_config(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
strategy = modal_module.ModalCloudBucketMountStrategy()
|
|
mount = GCSMount(
|
|
bucket="bucket",
|
|
access_id="access-id",
|
|
secret_access_key="secret-key",
|
|
prefix="nested/prefix/",
|
|
mount_strategy=strategy,
|
|
read_only=False,
|
|
)
|
|
|
|
config = strategy._build_modal_cloud_bucket_mount_config(mount) # noqa: SLF001
|
|
|
|
assert config.bucket_name == "bucket"
|
|
assert config.bucket_endpoint_url == "https://storage.googleapis.com"
|
|
assert config.key_prefix == "nested/prefix/"
|
|
assert config.credentials == {
|
|
"GOOGLE_ACCESS_KEY_ID": "access-id",
|
|
"GOOGLE_ACCESS_KEY_SECRET": "secret-key",
|
|
}
|
|
assert config.read_only is False
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_builds_gcs_hmac_config_with_named_secret(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
strategy = modal_module.ModalCloudBucketMountStrategy(secret_name="named-modal-secret")
|
|
mount = GCSMount(
|
|
bucket="bucket",
|
|
prefix="nested/prefix/",
|
|
mount_strategy=strategy,
|
|
read_only=False,
|
|
)
|
|
|
|
config = strategy._build_modal_cloud_bucket_mount_config(mount) # noqa: SLF001
|
|
|
|
assert config.bucket_name == "bucket"
|
|
assert config.bucket_endpoint_url == "https://storage.googleapis.com"
|
|
assert config.key_prefix == "nested/prefix/"
|
|
assert config.credentials is None
|
|
assert config.secret_name == "named-modal-secret"
|
|
assert config.secret_environment_name is None
|
|
assert config.read_only is False
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_rejects_secret_environment_name_without_secret_name(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
strategy = modal_module.ModalCloudBucketMountStrategy(secret_environment_name="staging")
|
|
|
|
with pytest.raises(
|
|
MountConfigError,
|
|
match="secret_environment_name requires secret_name to also be set",
|
|
):
|
|
strategy._build_modal_cloud_bucket_mount_config( # noqa: SLF001
|
|
S3Mount(bucket="bucket", mount_strategy=strategy)
|
|
)
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_rejects_mixed_inline_credentials_and_secret_name(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
strategy = modal_module.ModalCloudBucketMountStrategy(secret_name="named-modal-secret")
|
|
|
|
with pytest.raises(
|
|
MountConfigError,
|
|
match="do not support both inline credentials and secret_name",
|
|
):
|
|
strategy._build_modal_cloud_bucket_mount_config( # noqa: SLF001
|
|
S3Mount(
|
|
bucket="bucket",
|
|
access_key_id="access-key",
|
|
secret_access_key="secret-key",
|
|
mount_strategy=strategy,
|
|
)
|
|
)
|
|
|
|
|
|
def test_modal_cloud_bucket_mount_strategy_rejects_gcs_native_auth(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
with pytest.raises(
|
|
MountConfigError,
|
|
match="gcs modal cloud bucket mounts require access_id and secret_access_key",
|
|
):
|
|
GCSMount(
|
|
bucket="bucket",
|
|
service_account_file="/data/config/gcs.json",
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(),
|
|
)
|
|
|
|
|
|
def _load_modal_runner_module(monkeypatch: pytest.MonkeyPatch) -> Any:
|
|
_load_modal_module(monkeypatch)
|
|
monkeypatch.delitem(sys.modules, "agents.extensions.sandbox", raising=False)
|
|
monkeypatch.delitem(sys.modules, "examples.sandbox.extensions.modal_runner", raising=False)
|
|
return importlib.import_module("examples.sandbox.extensions.modal_runner")
|
|
|
|
|
|
def test_modal_runner_builds_s3_native_bucket_by_default(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
runner = _load_modal_runner_module(monkeypatch)
|
|
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "access-key")
|
|
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "secret-key")
|
|
|
|
manifest = runner._build_manifest(native_cloud_bucket_name="bucket") # noqa: SLF001
|
|
|
|
mount = manifest.entries["cloud-bucket"]
|
|
assert isinstance(mount, S3Mount)
|
|
assert mount.bucket == "bucket"
|
|
assert mount.access_key_id == "access-key"
|
|
assert mount.secret_access_key == "secret-key"
|
|
|
|
|
|
def test_modal_runner_builds_s3_native_bucket_with_named_secret(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
runner = _load_modal_runner_module(monkeypatch)
|
|
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "access-key")
|
|
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "secret-key")
|
|
|
|
manifest = runner._build_manifest( # noqa: SLF001
|
|
native_cloud_bucket_name="bucket",
|
|
native_cloud_bucket_secret_name="named-modal-secret",
|
|
)
|
|
|
|
mount = manifest.entries["cloud-bucket"]
|
|
assert isinstance(mount, S3Mount)
|
|
assert mount.bucket == "bucket"
|
|
assert mount.access_key_id is None
|
|
assert mount.secret_access_key is None
|
|
assert mount.session_token is None
|
|
strategy = mount.mount_strategy
|
|
assert isinstance(strategy, runner.ModalCloudBucketMountStrategy)
|
|
assert strategy.secret_name == "named-modal-secret"
|
|
assert strategy.secret_environment_name is None
|
|
|
|
|
|
def test_modal_runner_builds_gcs_hmac_native_bucket(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
runner = _load_modal_runner_module(monkeypatch)
|
|
monkeypatch.setenv("GCS_HMAC_ACCESS_KEY_ID", "access-id")
|
|
monkeypatch.setenv("GCS_HMAC_SECRET_ACCESS_KEY", "secret-key")
|
|
|
|
manifest = runner._build_manifest( # noqa: SLF001
|
|
native_cloud_bucket_name="bucket",
|
|
native_cloud_bucket_provider="gcs-hmac",
|
|
native_cloud_bucket_mount_path="mounted",
|
|
native_cloud_bucket_key_prefix="nested/prefix/",
|
|
)
|
|
|
|
mount = manifest.entries["cloud-bucket"]
|
|
assert isinstance(mount, GCSMount)
|
|
assert mount.bucket == "bucket"
|
|
assert mount.access_id == "access-id"
|
|
assert mount.secret_access_key == "secret-key"
|
|
assert mount.mount_path == Path("mounted")
|
|
assert mount.prefix == "nested/prefix/"
|
|
assert runner._native_cloud_bucket_mount_path(manifest) == Path("/workspace/mounted")
|
|
|
|
|
|
def test_modal_runner_builds_gcs_hmac_native_bucket_with_named_secret(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
runner = _load_modal_runner_module(monkeypatch)
|
|
monkeypatch.setenv("GCS_HMAC_ACCESS_KEY_ID", "access-id")
|
|
monkeypatch.setenv("GCS_HMAC_SECRET_ACCESS_KEY", "secret-key")
|
|
|
|
manifest = runner._build_manifest( # noqa: SLF001
|
|
native_cloud_bucket_name="bucket",
|
|
native_cloud_bucket_provider="gcs-hmac",
|
|
native_cloud_bucket_secret_name="named-modal-secret",
|
|
)
|
|
|
|
mount = manifest.entries["cloud-bucket"]
|
|
assert isinstance(mount, GCSMount)
|
|
assert mount.bucket == "bucket"
|
|
assert mount.access_id is None
|
|
assert mount.secret_access_key is None
|
|
strategy = mount.mount_strategy
|
|
assert isinstance(strategy, runner.ModalCloudBucketMountStrategy)
|
|
assert strategy.secret_name == "named-modal-secret"
|
|
assert strategy.secret_environment_name is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_start_ensures_sandbox_before_running_commands(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.create(
|
|
options=modal_module.ModalSandboxClientOptions(app_name="sandbox-tests"),
|
|
)
|
|
|
|
assert session._inner._sandbox is not None # noqa: SLF001
|
|
assert len(create_calls) == 1
|
|
|
|
await session.start()
|
|
|
|
assert session._inner._sandbox is not None # noqa: SLF001
|
|
assert len(create_calls) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_sandbox_create_exposes_declared_ports(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
await client.create(
|
|
options=modal_module.ModalSandboxClientOptions(
|
|
app_name="sandbox-tests",
|
|
exposed_ports=(8765,),
|
|
),
|
|
)
|
|
|
|
assert create_calls
|
|
assert create_calls[0]["encrypted_ports"] == (8765,)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_resume_eagerly_reconnects_sandbox(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-existing",
|
|
)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.resume(state)
|
|
|
|
assert session._inner._sandbox is not None # noqa: SLF001
|
|
assert create_calls == []
|
|
assert sys.modules["modal"].Sandbox.from_id_calls == ["sb-existing"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_resume_marks_reconnected_sandbox_preserved_before_snapshot_reuse(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
snapshot = LocalSnapshot(id="modal-snapshot", base_path=tmp_path)
|
|
await snapshot.persist(
|
|
io.BytesIO(modal_module._encode_snapshot_filesystem_ref(snapshot_id="snap-123"))
|
|
)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=snapshot,
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-existing",
|
|
workspace_persistence="snapshot_filesystem",
|
|
snapshot_fingerprint="fake-workspace-fingerprint",
|
|
snapshot_fingerprint_version="workspace_tar_sha256_v1",
|
|
workspace_root_ready=True,
|
|
)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.resume(state)
|
|
|
|
assert session._inner._running is True # noqa: SLF001
|
|
assert session._inner._workspace_state_preserved_on_start() is True # noqa: SLF001
|
|
assert session._inner._system_state_preserved_on_start() is True # noqa: SLF001
|
|
|
|
await session.start()
|
|
|
|
assert create_calls == []
|
|
assert sys.modules["modal"].Sandbox.from_id_calls == ["sb-existing"]
|
|
assert sys.modules["modal"].Image.from_id_calls == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_resume_restores_snapshot_when_workspace_readiness_unproven(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
snapshot = LocalSnapshot(id="modal-snapshot", base_path=tmp_path)
|
|
await snapshot.persist(
|
|
io.BytesIO(modal_module._encode_snapshot_filesystem_ref(snapshot_id="snap-123"))
|
|
)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=snapshot,
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-existing",
|
|
workspace_persistence="snapshot_filesystem",
|
|
snapshot_fingerprint="fake-workspace-fingerprint",
|
|
snapshot_fingerprint_version="workspace_tar_sha256_v1",
|
|
)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.resume(state)
|
|
|
|
assert session._inner._running is True # noqa: SLF001
|
|
assert session._inner._workspace_state_preserved_on_start() is True # noqa: SLF001
|
|
assert session._inner._can_reuse_preserved_workspace_on_resume() is False # noqa: SLF001
|
|
|
|
await session.start()
|
|
|
|
assert len(create_calls) == 1
|
|
assert create_calls[0]["workdir"] == "/workspace"
|
|
assert sys.modules["modal"].Sandbox.from_id_calls == ["sb-existing"]
|
|
assert sys.modules["modal"].Image.from_id_calls == ["snap-123"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_resume_restores_directory_snapshot_when_workspace_readiness_unproven(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
snapshot = LocalSnapshot(id="modal-snapshot", base_path=tmp_path)
|
|
await snapshot.persist(
|
|
io.BytesIO(modal_module._encode_snapshot_directory_ref(snapshot_id="snap-dir-123"))
|
|
)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=snapshot,
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-existing",
|
|
workspace_persistence="snapshot_directory",
|
|
snapshot_fingerprint="fake-workspace-fingerprint",
|
|
snapshot_fingerprint_version="workspace_tar_sha256_v1",
|
|
)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.resume(state)
|
|
inner = session._inner # noqa: SLF001
|
|
|
|
assert inner._running is True # noqa: SLF001
|
|
assert inner._workspace_state_preserved_on_start() is True # noqa: SLF001
|
|
assert inner._can_reuse_preserved_workspace_on_resume() is False # noqa: SLF001
|
|
|
|
await session.start()
|
|
|
|
assert create_calls == []
|
|
assert sys.modules["modal"].Sandbox.from_id_calls == ["sb-existing"]
|
|
assert sys.modules["modal"].Image.from_id_calls == ["snap-dir-123"]
|
|
assert inner._sandbox is not None # noqa: SLF001
|
|
assert inner._sandbox.mount_image_calls == [("/workspace", "snap-dir-123")] # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_resume_resets_workspace_readiness_when_sandbox_is_recreated(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _StoppedSandboxInstance:
|
|
object_id = "sb-stopped"
|
|
|
|
def __init__(self) -> None:
|
|
self.poll = _with_aio(lambda: 1)
|
|
|
|
def _from_stopped_id(_sandbox_id: str) -> object:
|
|
sys.modules["modal"].Sandbox.from_id_calls.append(_sandbox_id)
|
|
return _StoppedSandboxInstance()
|
|
|
|
sys.modules["modal"].Sandbox.from_id = staticmethod(_with_aio(_from_stopped_id))
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-stopped",
|
|
workspace_root_ready=True,
|
|
image_builder_version="PREVIEW",
|
|
)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
session = await client.resume(state)
|
|
|
|
assert session._inner._workspace_state_preserved_on_start() is False # noqa: SLF001
|
|
assert state.workspace_root_ready is False
|
|
assert create_calls
|
|
assert create_calls[0]["modal_image_builder_version_env"] == "PREVIEW"
|
|
assert state.sandbox_id == "sb-123"
|
|
assert os.environ.get("MODAL_IMAGE_BUILDER_VERSION") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_resume_bounds_reconnect_and_poll(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_create_timeout_s=12.5,
|
|
sandbox_id="sb-existing",
|
|
)
|
|
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
call_timeouts: list[float | None] = []
|
|
|
|
real_call_modal = session._call_modal # noqa: SLF001
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
call_timeouts.append(call_timeout)
|
|
return await real_call_modal(fn, *args, call_timeout=call_timeout, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
await session._ensure_sandbox() # noqa: SLF001
|
|
|
|
assert session._sandbox is not None # noqa: SLF001
|
|
assert create_calls == []
|
|
assert call_timeouts == [12.5, modal_module._DEFAULT_TIMEOUT_S] # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_ensure_sandbox_bounds_app_lookup(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
call_timeouts: list[float | None] = []
|
|
|
|
real_call_modal = session._call_modal # noqa: SLF001
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
call_timeouts.append(call_timeout)
|
|
return await real_call_modal(fn, *args, call_timeout=call_timeout, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
await session._ensure_sandbox() # noqa: SLF001
|
|
|
|
assert session._sandbox is not None # noqa: SLF001
|
|
assert len(create_calls) == 1
|
|
assert call_timeouts == [10.0, modal_module._DEFAULT_TIMEOUT_S] # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_ensure_sandbox_bounds_image_id_lookup(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
image_id="im-existing",
|
|
)
|
|
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
call_names: list[str] = []
|
|
call_timeouts: list[float | None] = []
|
|
|
|
real_call_modal = session._call_modal # noqa: SLF001
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
call_names.append(getattr(fn, "__name__", ""))
|
|
call_timeouts.append(call_timeout)
|
|
return await real_call_modal(fn, *args, call_timeout=call_timeout, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
await session._ensure_sandbox() # noqa: SLF001
|
|
|
|
assert session._sandbox is not None # noqa: SLF001
|
|
assert len(create_calls) == 1
|
|
assert sys.modules["modal"].Image.from_id_calls == ["im-existing"]
|
|
assert call_names == ["_sync"]
|
|
assert call_timeouts == [10.0]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_resolve_exposed_port_reads_tunnel_metadata(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
sandbox = sys.modules["modal"].Sandbox.create()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
exposed_ports=(8765,),
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
|
|
endpoint = await session.resolve_exposed_port(8765)
|
|
|
|
assert endpoint.host == "sandbox.example.test"
|
|
assert endpoint.port == 443
|
|
assert endpoint.tls is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_stop_is_persistence_only_and_shutdown_terminates(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
sandbox = sys.modules["modal"].Sandbox.create()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
session._running = True
|
|
call_timeouts: list[float | None] = []
|
|
|
|
real_call_modal = session._call_modal # noqa: SLF001
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
call_timeouts.append(call_timeout)
|
|
return await real_call_modal(fn, *args, call_timeout=call_timeout, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
await session.stop()
|
|
|
|
assert sandbox.terminate_calls == 0
|
|
assert session.state.sandbox_id == "sb-123"
|
|
assert await session.running() is True
|
|
|
|
await session.shutdown()
|
|
|
|
assert sandbox.terminate_calls == 1
|
|
assert sandbox.terminate_kwargs == [{}]
|
|
assert session.state.sandbox_id is None
|
|
assert await session.running() is False
|
|
assert call_timeouts == [modal_module._DEFAULT_TIMEOUT_S] # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_shutdown_rehydrates_sandbox_and_terminates_without_wait_kwarg(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
sandbox = sys.modules["modal"].Sandbox.create()
|
|
|
|
def _from_id(_sandbox_id: str) -> object:
|
|
sys.modules["modal"].Sandbox.from_id_calls.append(_sandbox_id)
|
|
return sandbox
|
|
|
|
sys.modules["modal"].Sandbox.from_id = staticmethod(_with_aio(_from_id))
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-existing",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
call_timeouts: list[float | None] = []
|
|
|
|
real_call_modal = session._call_modal # noqa: SLF001
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
call_timeouts.append(call_timeout)
|
|
return await real_call_modal(fn, *args, call_timeout=call_timeout, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
await session.shutdown()
|
|
|
|
assert sys.modules["modal"].Sandbox.from_id_calls == ["sb-existing"]
|
|
assert sandbox.terminate_kwargs == [{}]
|
|
assert session.state.sandbox_id is None
|
|
assert await session.running() is False
|
|
assert call_timeouts == [
|
|
modal_module._DEFAULT_TIMEOUT_S,
|
|
modal_module._DEFAULT_TIMEOUT_S,
|
|
] # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_tar_persist_respects_runtime_skip_paths(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-123",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
session.register_persist_workspace_skip_path(Path("logs/events.jsonl"))
|
|
|
|
commands: list[list[str]] = []
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
return ExecResult(stdout=b"fake-tar-bytes", stderr=b"", exit_code=0)
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
archive = await session.persist_workspace()
|
|
|
|
assert archive.read() == b"fake-tar-bytes"
|
|
assert commands == [
|
|
[
|
|
"tar",
|
|
"cf",
|
|
"-",
|
|
"--exclude",
|
|
"./logs/events.jsonl",
|
|
"-C",
|
|
"/workspace",
|
|
".",
|
|
]
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_failure_restores_ephemeral_paths(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeRestoreProcess:
|
|
def __init__(self, owner: Any) -> None:
|
|
self._owner = owner
|
|
self.stderr = types.SimpleNamespace(read=_with_aio(lambda: b""))
|
|
self.stdin = self._FakeStdin(owner)
|
|
_set_aio_attr(self.stdin, "drain", self.stdin.drain)
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
class _FakeStdin:
|
|
def __init__(self, owner: Any) -> None:
|
|
self._owner = owner
|
|
self._buffer = bytearray()
|
|
|
|
def write(self, data: bytes) -> None:
|
|
self._buffer.extend(data)
|
|
|
|
def write_eof(self) -> None:
|
|
return
|
|
|
|
def drain(self) -> None:
|
|
return
|
|
|
|
def _wait(self) -> int:
|
|
self._owner.restore_payloads.append(bytes(self.stdin._buffer))
|
|
return 0
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.restore_payloads: list[bytes] = []
|
|
self.snapshot_filesystem = _with_aio(self._snapshot_filesystem)
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _snapshot_filesystem(self) -> str:
|
|
raise RuntimeError("snapshot failed")
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> _FakeRestoreProcess:
|
|
_ = kwargs
|
|
assert command[:3] == ("tar", "xf", "-")
|
|
return _FakeRestoreProcess(self)
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={"tmp.txt": File(content=b"ephemeral", ephemeral=True)},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_filesystem",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
if rendered[:2] == ["sh", "-lc"]:
|
|
return ExecResult(stdout=b"ephemeral-backup", stderr=b"", exit_code=0)
|
|
if rendered[:3] == ["rm", "-rf", "--"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
_ = call_timeout
|
|
return fn(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
with pytest.raises(WorkspaceArchiveReadError) as exc_info:
|
|
await session.persist_workspace()
|
|
|
|
assert exc_info.value.context["reason"] == "snapshot_filesystem_failed"
|
|
assert sandbox.restore_payloads == [b"ephemeral-backup"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_cleanup_failure_raises_before_snapshot(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeRestoreProcess:
|
|
def __init__(self, owner: Any) -> None:
|
|
self._owner = owner
|
|
self.stderr = types.SimpleNamespace(read=_with_aio(lambda: b""))
|
|
self.stdin = self._FakeStdin(owner)
|
|
_set_aio_attr(self.stdin, "drain", self.stdin.drain)
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
class _FakeStdin:
|
|
def __init__(self, owner: Any) -> None:
|
|
self._owner = owner
|
|
self._buffer = bytearray()
|
|
|
|
def write(self, data: bytes) -> None:
|
|
self._buffer.extend(data)
|
|
|
|
def write_eof(self) -> None:
|
|
return
|
|
|
|
def drain(self) -> None:
|
|
return
|
|
|
|
def _wait(self) -> int:
|
|
self._owner.restore_payloads.append(bytes(self.stdin._buffer))
|
|
return 0
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.restore_payloads: list[bytes] = []
|
|
self.snapshot_calls = 0
|
|
self.snapshot_filesystem = _with_aio(self._snapshot_filesystem)
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _snapshot_filesystem(self) -> str:
|
|
self.snapshot_calls += 1
|
|
return "snap-123"
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> _FakeRestoreProcess:
|
|
_ = kwargs
|
|
assert command[:3] == ("tar", "xf", "-")
|
|
return _FakeRestoreProcess(self)
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={"tmp.txt": File(content=b"ephemeral", ephemeral=True)},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_filesystem",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
if rendered[:2] == ["sh", "-lc"]:
|
|
return ExecResult(stdout=b"ephemeral-backup", stderr=b"", exit_code=0)
|
|
if rendered[:3] == ["rm", "-rf", "--"]:
|
|
return ExecResult(stdout=b"", stderr=b"rm failed", exit_code=1)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
_ = call_timeout
|
|
return fn(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
with pytest.raises(WorkspaceArchiveReadError) as exc_info:
|
|
await session.persist_workspace()
|
|
|
|
assert exc_info.value.context["reason"] == "snapshot_filesystem_ephemeral_remove_failed"
|
|
assert exc_info.value.context["exit_code"] == 1
|
|
assert exc_info.value.context["stderr"] == "rm failed"
|
|
assert sandbox.snapshot_calls == 0
|
|
assert sandbox.restore_payloads == [b"ephemeral-backup"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_normalize_path_preserves_safe_leaf_symlink_path(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
if (
|
|
rendered[:2] == ["sh", "-c"]
|
|
and RESOLVE_WORKSPACE_PATH_HELPER.install_marker in rendered[2]
|
|
):
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered and rendered[0] == str(RESOLVE_WORKSPACE_PATH_HELPER.install_path):
|
|
return ExecResult(stdout=b"/workspace/target.txt", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
normalized = await session._validate_path_access("link.txt") # noqa: SLF001
|
|
|
|
assert normalized.as_posix() == "/workspace/link.txt"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_normalize_path_uses_posix_commands_for_windows_paths(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
commands: list[list[str]] = []
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if (
|
|
rendered[:2] == ["sh", "-c"]
|
|
and RESOLVE_WORKSPACE_PATH_HELPER.install_marker in rendered[2]
|
|
):
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered and rendered[0] == str(RESOLVE_WORKSPACE_PATH_HELPER.install_path):
|
|
return ExecResult(stdout=b"/workspace/link.txt", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
normalized = await session._validate_path_access(PureWindowsPath("/workspace/link.txt")) # noqa: SLF001
|
|
|
|
helper_path = str(RESOLVE_WORKSPACE_PATH_HELPER.install_path)
|
|
assert normalized.as_posix() == "/workspace/link.txt"
|
|
assert commands[-1] == [helper_path, "/workspace", "/workspace/link.txt", "0"]
|
|
assert all("\\" not in arg for arg in commands[-1])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_normalize_path_rejects_windows_drive_absolute_paths(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
|
|
async def _fake_exec(*args: object, **kwargs: object) -> ExecResult:
|
|
_ = (args, kwargs)
|
|
raise AssertionError("path validation should reject before remote helper execution")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
with pytest.raises(InvalidManifestPathError) as exc_info:
|
|
await session._validate_path_access(PureWindowsPath("C:/tmp/link.txt")) # noqa: SLF001
|
|
|
|
assert str(exc_info.value) == "manifest path must be relative: C:/tmp/link.txt"
|
|
assert exc_info.value.context == {"rel": "C:/tmp/link.txt", "reason": "absolute"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_normalize_path_rejects_symlink_escape(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
if (
|
|
rendered[:2] == ["sh", "-c"]
|
|
and RESOLVE_WORKSPACE_PATH_HELPER.install_marker in rendered[2]
|
|
):
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered and rendered[0] == str(RESOLVE_WORKSPACE_PATH_HELPER.install_path):
|
|
return ExecResult(stdout=b"", stderr=b"workspace escape", exit_code=111)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
with pytest.raises(InvalidManifestPathError, match="must not escape root"):
|
|
await session._validate_path_access("link/secret.txt") # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_normalize_path_reinstalls_helper_after_runtime_replacement(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-old",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
commands: list[list[str]] = []
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if (
|
|
rendered[:2] == ["sh", "-c"]
|
|
and RESOLVE_WORKSPACE_PATH_HELPER.install_marker in rendered[2]
|
|
):
|
|
if state.sandbox_id is None:
|
|
state.sandbox_id = "sb-new"
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered == ["test", "-x", str(RESOLVE_WORKSPACE_PATH_HELPER.install_path)]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered and rendered[0] == str(RESOLVE_WORKSPACE_PATH_HELPER.install_path):
|
|
return ExecResult(stdout=b"/workspace/target.txt", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
assert (await session._validate_path_access("link.txt")).as_posix() == "/workspace/link.txt"
|
|
first_run_commands = list(commands)
|
|
commands.clear()
|
|
|
|
state.sandbox_id = None
|
|
assert (await session._validate_path_access("link.txt")).as_posix() == "/workspace/link.txt"
|
|
second_run_commands = list(commands)
|
|
commands.clear()
|
|
|
|
assert (await session._validate_path_access("link.txt")).as_posix() == "/workspace/link.txt"
|
|
|
|
helper_path = str(RESOLVE_WORKSPACE_PATH_HELPER.install_path)
|
|
assert any(
|
|
cmd[:2] == ["sh", "-c"] and RESOLVE_WORKSPACE_PATH_HELPER.install_marker in cmd[2]
|
|
for cmd in first_run_commands
|
|
)
|
|
assert any(
|
|
cmd[:2] == ["sh", "-c"] and RESOLVE_WORKSPACE_PATH_HELPER.install_marker in cmd[2]
|
|
for cmd in second_run_commands
|
|
)
|
|
assert any(cmd and cmd[0] == helper_path for cmd in second_run_commands)
|
|
assert commands == [
|
|
["test", "-x", helper_path],
|
|
[helper_path, "/workspace", "/workspace/link.txt", "0"],
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_filesystem_uses_resolved_mount_paths_for_backup_and_removal(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeRestoreProcess:
|
|
def __init__(self) -> None:
|
|
self.stderr = types.SimpleNamespace(read=_with_aio(lambda: b""))
|
|
self.stdin = self._FakeStdin()
|
|
_set_aio_attr(self.stdin, "drain", self.stdin.drain)
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
class _FakeStdin:
|
|
def write(self, data: bytes) -> None:
|
|
_ = data
|
|
|
|
def write_eof(self) -> None:
|
|
return
|
|
|
|
def drain(self) -> None:
|
|
return
|
|
|
|
def _wait(self) -> int:
|
|
return 0
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.snapshot_filesystem = _with_aio(self._snapshot_filesystem)
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _snapshot_filesystem(self) -> str:
|
|
return "snap-123"
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> _FakeRestoreProcess:
|
|
_ = kwargs
|
|
assert command[:3] == ("tar", "xf", "-")
|
|
return _FakeRestoreProcess()
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"logical": _RecordingMount(
|
|
mount_path=Path("actual"),
|
|
ephemeral=False,
|
|
),
|
|
"logs/events.jsonl": File(content=b"skip", ephemeral=True),
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_filesystem",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
commands: list[list[str]] = []
|
|
|
|
def _snapshot_filesystem() -> str:
|
|
return "snap-123"
|
|
|
|
sandbox.snapshot_filesystem = _with_aio(_snapshot_filesystem)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if rendered[:2] == ["sh", "-lc"]:
|
|
return ExecResult(stdout=b"ephemeral-backup", stderr=b"", exit_code=0)
|
|
if rendered[:3] == ["rm", "-rf", "--"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
archive = await session.persist_workspace()
|
|
|
|
assert archive.read() == modal_module._encode_snapshot_filesystem_ref(snapshot_id="snap-123")
|
|
assert commands[0][0:2] == ["sh", "-lc"]
|
|
assert "logs/events.jsonl" in commands[0][2]
|
|
assert "actual" not in commands[0][2]
|
|
assert "logical" not in commands[0][2]
|
|
assert commands[1] == ["rm", "-rf", "--", "/workspace/logs/events.jsonl"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_directory_uses_resolved_mount_paths_for_backup_and_removal(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeRestoreProcess:
|
|
def __init__(self) -> None:
|
|
self.stderr = types.SimpleNamespace(read=_with_aio(lambda: b""))
|
|
self.stdin = self._FakeStdin()
|
|
_set_aio_attr(self.stdin, "drain", self.stdin.drain)
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
class _FakeStdin:
|
|
def write(self, data: bytes) -> None:
|
|
_ = data
|
|
|
|
def write_eof(self) -> None:
|
|
return
|
|
|
|
def drain(self) -> None:
|
|
return
|
|
|
|
def _wait(self) -> int:
|
|
return 0
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
snapshot_directory: Any
|
|
|
|
def __init__(self) -> None:
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> _FakeRestoreProcess:
|
|
_ = kwargs
|
|
assert command[:3] == ("tar", "xf", "-")
|
|
return _FakeRestoreProcess()
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
mount = _RecordingMount()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"logical": mount,
|
|
"logs/events.jsonl": File(content=b"skip", ephemeral=True),
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_directory",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
commands: list[list[str]] = []
|
|
|
|
def _snapshot_directory(path: str) -> str:
|
|
assert path == "/workspace"
|
|
return "snap-dir-123"
|
|
|
|
sandbox.snapshot_directory = _with_aio(_snapshot_directory)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if rendered[:2] == ["sh", "-lc"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered[:3] == ["rm", "-rf", "--"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
archive = await session.persist_workspace()
|
|
|
|
assert archive.read() == modal_module._encode_snapshot_directory_ref(snapshot_id="snap-dir-123")
|
|
assert commands[0][0:2] == ["sh", "-lc"]
|
|
assert "logs/events.jsonl" in commands[0][2]
|
|
assert "logical" not in commands[0][2]
|
|
assert "/tmp/openai-agents/session-state/" in commands[0][2]
|
|
assert "modal-snapshot-directory-ephemeral.tar" in commands[0][2]
|
|
assert "for rel in logs/events.jsonl;" in commands[0][2]
|
|
assert "tar cf" in commands[0][2]
|
|
assert "-T -" in commands[0][2]
|
|
assert commands[1] == ["rm", "-rf", "--", "/workspace/logs/events.jsonl"]
|
|
assert commands[2][0:2] == ["sh", "-lc"]
|
|
assert "modal-snapshot-directory-ephemeral.tar" in commands[2][2]
|
|
assert "tar xf" in commands[2][2]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_directory_backup_failure_aborts_before_removing_ephemeral_paths(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
snapshot_directory: Any
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"tmp.txt": File(content=b"skip", ephemeral=True),
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_directory",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
commands: list[list[str]] = []
|
|
|
|
def _snapshot_directory(_path: str) -> str:
|
|
raise AssertionError("snapshot_directory should not run after backup failure")
|
|
|
|
sandbox.snapshot_directory = _with_aio(_snapshot_directory)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if rendered[:2] == ["sh", "-lc"]:
|
|
return ExecResult(stdout=b"", stderr=b"mkdir failed", exit_code=1)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
with pytest.raises(WorkspaceArchiveReadError) as exc_info:
|
|
await session.persist_workspace()
|
|
|
|
assert exc_info.value.context["reason"] == "snapshot_directory_ephemeral_backup_failed"
|
|
assert exc_info.value.context["exit_code"] == 1
|
|
assert exc_info.value.context["stderr"] == "mkdir failed"
|
|
assert commands == [
|
|
[
|
|
"sh",
|
|
"-lc",
|
|
"mkdir -p -- /tmp/openai-agents/session-state/"
|
|
f"{session.state.session_id.hex} && "
|
|
"cd -- /workspace && "
|
|
'{ for rel in tmp.txt; do if [ -e "$rel" ]; '
|
|
"then printf '%s\\n' \"$rel\"; fi; done; } | tar cf "
|
|
f"/tmp/openai-agents/session-state/{session.state.session_id.hex}/"
|
|
"modal-snapshot-directory-ephemeral.tar -T - 2>/dev/null && test -f "
|
|
f"/tmp/openai-agents/session-state/{session.state.session_id.hex}/"
|
|
"modal-snapshot-directory-ephemeral.tar",
|
|
]
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_directory_teardown_failure_restores_partial_cleanup(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
events: list[tuple[str, str]] = []
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
snapshot_directory: Any
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"tmp.txt": File(content=b"skip", ephemeral=True),
|
|
"first": _RecordingMount(
|
|
mount_path=Path("actual-1"),
|
|
ephemeral=False,
|
|
).bind_events(events),
|
|
"second": _RecordingMount(
|
|
mount_path=Path("actual-2"),
|
|
ephemeral=False,
|
|
)
|
|
.bind_events(events)
|
|
.bind_teardown_error("teardown failed"),
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_directory",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
commands: list[list[str]] = []
|
|
|
|
def _snapshot_directory(_path: str) -> str:
|
|
raise AssertionError("snapshot_directory should not run after teardown failure")
|
|
|
|
sandbox.snapshot_directory = _with_aio(_snapshot_directory)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if rendered[:2] == ["sh", "-lc"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered[:3] == ["rm", "-rf", "--"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
with pytest.raises(WorkspaceArchiveReadError) as exc_info:
|
|
await session.persist_workspace()
|
|
|
|
assert isinstance(exc_info.value.cause, RuntimeError)
|
|
assert str(exc_info.value.cause) == "teardown failed"
|
|
assert events == [("unmount", "/workspace/actual-1"), ("mount", "/workspace/actual-1")]
|
|
assert commands[0][0:2] == ["sh", "-lc"]
|
|
assert "for rel in tmp.txt;" in commands[0][2]
|
|
assert commands[1] == ["rm", "-rf", "--", "/workspace/tmp.txt"]
|
|
assert commands[2][0:2] == ["sh", "-lc"]
|
|
assert "modal-snapshot-directory-ephemeral.tar" in commands[2][2]
|
|
assert "tar xf" in commands[2][2]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_directory_tolerates_missing_ephemeral_paths_in_backup_command(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
snapshot_directory: Any
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"tmp.txt": File(content=b"skip", ephemeral=True),
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_directory",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
commands: list[list[str]] = []
|
|
|
|
def _snapshot_directory(path: str) -> str:
|
|
assert path == "/workspace"
|
|
return "snap-dir-123"
|
|
|
|
sandbox.snapshot_directory = _with_aio(_snapshot_directory)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if rendered[:2] == ["sh", "-lc"]:
|
|
if "for rel in tmp.txt;" in rendered[2]:
|
|
assert "-T -" in rendered[2]
|
|
else:
|
|
assert "tar xf" in rendered[2]
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered[:3] == ["rm", "-rf", "--"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
archive = await session.persist_workspace()
|
|
|
|
assert archive.read() == modal_module._encode_snapshot_directory_ref(snapshot_id="snap-dir-123")
|
|
assert commands[1] == ["rm", "-rf", "--", "/workspace/tmp.txt"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_unexpected_return_restores_live_session_before_raising(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeRestoreProcess:
|
|
def __init__(self, owner: Any) -> None:
|
|
self._owner = owner
|
|
self.stderr = types.SimpleNamespace(read=_with_aio(lambda: b""))
|
|
self.stdin = self._FakeStdin(owner)
|
|
_set_aio_attr(self.stdin, "drain", self.stdin.drain)
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
class _FakeStdin:
|
|
def __init__(self, owner: Any) -> None:
|
|
self._owner = owner
|
|
self._buffer = bytearray()
|
|
|
|
def write(self, data: bytes) -> None:
|
|
self._buffer.extend(data)
|
|
|
|
def write_eof(self) -> None:
|
|
return
|
|
|
|
def drain(self) -> None:
|
|
return
|
|
|
|
def _wait(self) -> int:
|
|
self._owner.restore_payloads.append(bytes(self.stdin._buffer))
|
|
return 0
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.restore_payloads: list[bytes] = []
|
|
self.snapshot_filesystem = _with_aio(self._snapshot_filesystem)
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _snapshot_filesystem(self) -> object:
|
|
return object()
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> _FakeRestoreProcess:
|
|
_ = kwargs
|
|
assert command == ("tar", "xf", "-", "-C", "/workspace")
|
|
return _FakeRestoreProcess(self)
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"logical": _RecordingMount(
|
|
mount_path=Path("actual"),
|
|
ephemeral=False,
|
|
),
|
|
"tmp.txt": File(content=b"ephemeral", ephemeral=True),
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_filesystem",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
commands: list[list[str]] = []
|
|
events: list[tuple[str, str]] = []
|
|
|
|
def _snapshot_filesystem() -> object:
|
|
events.append(("snapshot", ""))
|
|
return object()
|
|
|
|
sandbox.snapshot_filesystem = _with_aio(_snapshot_filesystem)
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if rendered == [
|
|
"sh",
|
|
"-lc",
|
|
"cd -- /workspace && (tar cf - -- tmp.txt 2>/dev/null || true)",
|
|
]:
|
|
return ExecResult(stdout=b"ephemeral-backup", stderr=b"", exit_code=0)
|
|
if rendered == ["rm", "-rf", "--", "/workspace/tmp.txt"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
_ = call_timeout
|
|
if getattr(fn, "__name__", "") == "snapshot_filesystem":
|
|
events.append(("snapshot", ""))
|
|
return fn(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
with pytest.raises(WorkspaceArchiveReadError) as exc_info:
|
|
await session.persist_workspace()
|
|
|
|
assert exc_info.value.context == {
|
|
"path": "/workspace",
|
|
"reason": "snapshot_filesystem_unexpected_return",
|
|
"type": "object",
|
|
}
|
|
assert sandbox.restore_payloads == [b"ephemeral-backup"]
|
|
assert commands == [
|
|
["sh", "-lc", "cd -- /workspace && (tar cf - -- tmp.txt 2>/dev/null || true)"],
|
|
["rm", "-rf", "--", "/workspace/tmp.txt"],
|
|
]
|
|
assert events == [("snapshot", "")]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_unexpected_return_skips_restore_for_empty_ephemeral_backup(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.snapshot_filesystem = _with_aio(self._snapshot_filesystem)
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _snapshot_filesystem(self) -> object:
|
|
return object()
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> NoReturn:
|
|
_ = kwargs
|
|
raise AssertionError(f"restore should be skipped for empty backup: {command!r}")
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={"tmp.txt": File(content=b"ephemeral", ephemeral=True)},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_filesystem",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
commands: list[list[str]] = []
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if rendered == [
|
|
"sh",
|
|
"-lc",
|
|
"cd -- /workspace && (tar cf - -- tmp.txt 2>/dev/null || true)",
|
|
]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
if rendered == ["rm", "-rf", "--", "/workspace/tmp.txt"]:
|
|
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
|
|
raise AssertionError(f"unexpected command: {rendered!r}")
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
_ = call_timeout
|
|
return fn(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
with pytest.raises(WorkspaceArchiveReadError) as exc_info:
|
|
await session.persist_workspace()
|
|
|
|
assert exc_info.value.context == {
|
|
"path": "/workspace",
|
|
"reason": "snapshot_filesystem_unexpected_return",
|
|
"type": "object",
|
|
}
|
|
assert commands == [
|
|
["sh", "-lc", "cd -- /workspace && (tar cf - -- tmp.txt 2>/dev/null || true)"],
|
|
["rm", "-rf", "--", "/workspace/tmp.txt"],
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_tar_persist_uses_resolved_mount_paths_for_excludes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"logical": GCSMount(
|
|
bucket="bucket",
|
|
mount_path=Path("actual"),
|
|
ephemeral=False,
|
|
mount_strategy=InContainerMountStrategy(pattern=MountpointMountPattern()),
|
|
)
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=None)
|
|
commands: list[list[str]] = []
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
return ExecResult(stdout=b"tar-bytes", stderr=b"", exit_code=0)
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
archive = await session.persist_workspace()
|
|
|
|
assert archive.read() == b"tar-bytes"
|
|
assert commands == [
|
|
[
|
|
"tar",
|
|
"cf",
|
|
"-",
|
|
"--exclude",
|
|
"./actual",
|
|
"-C",
|
|
"/workspace",
|
|
".",
|
|
]
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_tar_persist_retries_wrapped_exec_transport_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=None)
|
|
commands: list[list[str]] = []
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
if len(commands) == 1:
|
|
raise modal_module.ExecTransportError(
|
|
command=tuple(rendered),
|
|
message="modal transport failed",
|
|
)
|
|
return ExecResult(stdout=b"tar-bytes", stderr=b"", exit_code=0)
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
archive = await session.persist_workspace()
|
|
|
|
assert archive.read() == b"tar-bytes"
|
|
assert commands == [
|
|
["tar", "cf", "-", "-C", "/workspace", "."],
|
|
["tar", "cf", "-", "-C", "/workspace", "."],
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_tar_persist_does_not_retry_wrapped_non_retryable_exec_transport_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=None)
|
|
commands: list[list[str]] = []
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
rendered = [str(part) for part in command]
|
|
commands.append(rendered)
|
|
raise modal_module.ExecTransportError(
|
|
command=tuple(rendered),
|
|
message="modal transport failed permanently",
|
|
retryable=False,
|
|
)
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
|
|
with pytest.raises(WorkspaceArchiveReadError) as exc_info:
|
|
await session.persist_workspace()
|
|
|
|
assert str(exc_info.value) == "failed to read archive for path: /workspace"
|
|
assert isinstance(exc_info.value.cause, modal_module.ExecTransportError)
|
|
assert str(exc_info.value.cause) == "modal transport failed permanently"
|
|
assert commands == [["tar", "cf", "-", "-C", "/workspace", "."]]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_filesystem_rejects_escaping_mount_paths_before_exec(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.snapshot_calls = 0
|
|
|
|
def snapshot_filesystem(self) -> str:
|
|
self.snapshot_calls += 1
|
|
return "snap-123"
|
|
|
|
sandbox = _FakeSnapshotSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"logical": GCSMount(
|
|
bucket="bucket",
|
|
mount_path=Path("/workspace/../../tmp"),
|
|
mount_strategy=InContainerMountStrategy(pattern=MountpointMountPattern()),
|
|
)
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
workspace_persistence="snapshot_filesystem",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
commands: list[list[str]] = []
|
|
|
|
async def _fake_exec(
|
|
*command: object,
|
|
timeout: float | None = None,
|
|
shell: bool | list[str] = True,
|
|
user: object | None = None,
|
|
) -> ExecResult:
|
|
_ = (timeout, shell, user)
|
|
commands.append([str(part) for part in command])
|
|
raise AssertionError("exec() should not run for escaping mount paths")
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
_ = (fn, args, call_timeout, kwargs)
|
|
raise AssertionError("snapshot_filesystem() should not run for escaping mount paths")
|
|
|
|
monkeypatch.setattr(session, "exec", _fake_exec)
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
with pytest.raises(InvalidManifestPathError, match="must not escape root"):
|
|
await session.persist_workspace()
|
|
|
|
assert commands == []
|
|
assert sandbox.snapshot_calls == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_write_chunks_large_payload_before_draining(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeWaitResult:
|
|
def __init__(self, *, stdout: bytes = b"", stderr: bytes = b"") -> None:
|
|
self.stdout = types.SimpleNamespace(read=_with_aio(lambda: stdout))
|
|
self.stderr = types.SimpleNamespace(read=_with_aio(lambda: stderr))
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
def _wait(self) -> int:
|
|
return 0
|
|
|
|
class _FakeStdin:
|
|
def __init__(self, *, limit: int) -> None:
|
|
self._limit = limit
|
|
self._buffer = bytearray()
|
|
self.chunks: list[bytes] = []
|
|
self.write_eof_calls = 0
|
|
self.drain_calls = 0
|
|
|
|
def write(self, data: bytes | bytearray | memoryview) -> None:
|
|
rendered = bytes(data)
|
|
if len(self._buffer) + len(rendered) > self._limit:
|
|
raise BufferError("Buffer size exceed limit. Call drain to flush the buffer.")
|
|
self._buffer.extend(rendered)
|
|
|
|
def write_eof(self) -> None:
|
|
self.write_eof_calls += 1
|
|
|
|
def drain(self) -> None:
|
|
self.chunks.append(bytes(self._buffer))
|
|
self._buffer.clear()
|
|
self.drain_calls += 1
|
|
|
|
class _FakeProcess:
|
|
def __init__(self, *, limit: int) -> None:
|
|
self.stdin = _FakeStdin(limit=limit)
|
|
_set_aio_attr(self.stdin, "drain", self.stdin.drain)
|
|
self.stderr = types.SimpleNamespace(read=_with_aio(lambda: b""))
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
def _wait(self) -> int:
|
|
return 0
|
|
|
|
class _FakeSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.processes: list[_FakeProcess] = []
|
|
self.commands: list[tuple[object, ...]] = []
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = kwargs
|
|
self.commands.append(command)
|
|
helper_path = str(RESOLVE_WORKSPACE_PATH_HELPER.install_path)
|
|
if command[:3] == ("mkdir", "-p", "--"):
|
|
return _FakeWaitResult()
|
|
if (
|
|
command[:2] == ("sh", "-c")
|
|
and isinstance(command[2], str)
|
|
and RESOLVE_WORKSPACE_PATH_HELPER.install_marker in command[2]
|
|
):
|
|
return _FakeWaitResult()
|
|
if command == ("test", "-x", helper_path):
|
|
return _FakeWaitResult()
|
|
if command and command[0] == helper_path:
|
|
return _FakeWaitResult(stdout=b"/workspace/nested/file.bin")
|
|
process = _FakeProcess(limit=5)
|
|
self.processes.append(process)
|
|
return process
|
|
|
|
sandbox = _FakeSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
monkeypatch.setattr(modal_module, "_MODAL_STDIN_CHUNK_SIZE", 5)
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
_ = call_timeout
|
|
return fn(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
payload = b"abcdefghijklm"
|
|
await session.write(Path("nested/file.bin"), io.BytesIO(payload))
|
|
|
|
assert sandbox.commands[-2:] == [
|
|
("mkdir", "-p", "--", "/workspace/nested"),
|
|
("sh", "-lc", "cat > /workspace/nested/file.bin"),
|
|
]
|
|
assert len(sandbox.processes) == 1
|
|
assert sandbox.processes[0].stdin.chunks == [b"abcde", b"fghij", b"klm", b""]
|
|
assert sandbox.processes[0].stdin.write_eof_calls == 1
|
|
assert sandbox.processes[0].stdin.drain_calls == 4
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_hydrate_tar_chunks_large_payload_before_draining(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeWaitResult:
|
|
def __init__(self) -> None:
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
def _wait(self) -> int:
|
|
return 0
|
|
|
|
class _FakeStdin:
|
|
def __init__(self, *, limit: int) -> None:
|
|
self._limit = limit
|
|
self._buffer = bytearray()
|
|
self.chunks: list[bytes] = []
|
|
self.write_eof_calls = 0
|
|
self.drain_calls = 0
|
|
|
|
def write(self, data: bytes | bytearray | memoryview) -> None:
|
|
rendered = bytes(data)
|
|
if len(self._buffer) + len(rendered) > self._limit:
|
|
raise BufferError("Buffer size exceed limit. Call drain to flush the buffer.")
|
|
self._buffer.extend(rendered)
|
|
|
|
def write_eof(self) -> None:
|
|
self.write_eof_calls += 1
|
|
|
|
def drain(self) -> None:
|
|
self.chunks.append(bytes(self._buffer))
|
|
self._buffer.clear()
|
|
self.drain_calls += 1
|
|
|
|
class _FakeProcess:
|
|
def __init__(self, *, limit: int) -> None:
|
|
self.stdin = _FakeStdin(limit=limit)
|
|
_set_aio_attr(self.stdin, "drain", self.stdin.drain)
|
|
self.stderr = types.SimpleNamespace(read=_with_aio(lambda: b""))
|
|
self.wait = _with_aio(self._wait)
|
|
|
|
def _wait(self) -> int:
|
|
return 0
|
|
|
|
class _FakeSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def __init__(self) -> None:
|
|
self.processes: list[_FakeProcess] = []
|
|
self.commands: list[tuple[object, ...]] = []
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = kwargs
|
|
self.commands.append(command)
|
|
if command[:3] == ("mkdir", "-p", "--"):
|
|
return _FakeWaitResult()
|
|
process = _FakeProcess(limit=7)
|
|
self.processes.append(process)
|
|
return process
|
|
|
|
sandbox = _FakeSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
monkeypatch.setattr(modal_module, "_MODAL_STDIN_CHUNK_SIZE", 7)
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
_ = call_timeout
|
|
return fn(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
tar_payload = io.BytesIO()
|
|
with tarfile.open(fileobj=tar_payload, mode="w") as tar:
|
|
info = tarfile.TarInfo(name="large.txt")
|
|
contents = b"abcdefghijklmno"
|
|
info.size = len(contents)
|
|
tar.addfile(info, io.BytesIO(contents))
|
|
tar_payload.seek(0)
|
|
|
|
await session.hydrate_workspace(tar_payload)
|
|
|
|
assert sandbox.commands == [
|
|
("mkdir", "-p", "--", "/workspace"),
|
|
("tar", "xf", "-", "-C", "/workspace"),
|
|
]
|
|
assert len(sandbox.processes) == 1
|
|
assert b"".join(sandbox.processes[0].stdin.chunks[:-1]) == tar_payload.getvalue()
|
|
assert sandbox.processes[0].stdin.write_eof_calls == 1
|
|
assert sandbox.processes[0].stdin.drain_calls >= 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_filesystem_restore_preserves_exposed_ports(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
workspace_persistence="snapshot_filesystem",
|
|
exposed_ports=(8765,),
|
|
idle_timeout=60,
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
call_names: list[str] = []
|
|
call_timeouts: list[float | None] = []
|
|
|
|
real_call_modal = session._call_modal # noqa: SLF001
|
|
|
|
async def _fake_call_modal(
|
|
fn: Callable[..., object],
|
|
*args: object,
|
|
call_timeout: float | None = None,
|
|
**kwargs: object,
|
|
) -> object:
|
|
call_names.append(getattr(fn, "__name__", ""))
|
|
call_timeouts.append(call_timeout)
|
|
return await real_call_modal(fn, *args, call_timeout=call_timeout, **kwargs)
|
|
|
|
monkeypatch.setattr(session, "_call_modal", _fake_call_modal)
|
|
|
|
await session.hydrate_workspace(
|
|
io.BytesIO(modal_module._encode_snapshot_filesystem_ref(snapshot_id="snap-123"))
|
|
)
|
|
|
|
assert create_calls
|
|
assert create_calls[0]["encrypted_ports"] == (8765,)
|
|
assert create_calls[0]["idle_timeout"] == 60
|
|
assert sys.modules["modal"].Image.from_id_calls == ["snap-123"]
|
|
assert call_names == []
|
|
assert call_timeouts == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_directory_restore_preserves_exposed_ports(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
workspace_persistence="snapshot_directory",
|
|
exposed_ports=(8765,),
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
|
|
await session.hydrate_workspace(
|
|
io.BytesIO(modal_module._encode_snapshot_directory_ref(snapshot_id="snap-dir-123"))
|
|
)
|
|
|
|
assert create_calls
|
|
assert create_calls[0]["encrypted_ports"] == (8765,)
|
|
assert session._sandbox is not None # noqa: SLF001
|
|
assert session._sandbox.mount_image_calls == [("/workspace", "snap-dir-123")] # noqa: SLF001
|
|
assert sys.modules["modal"].Image.from_id_calls == ["snap-dir-123"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_directory_restore_reactivates_durable_workspace_mounts(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
events: list[tuple[str, str]] = []
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"remote": _RecordingMount(
|
|
mount_path=Path("actual"),
|
|
ephemeral=False,
|
|
).bind_events(events)
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
workspace_persistence="snapshot_directory",
|
|
exposed_ports=(8765,),
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
|
|
await session.hydrate_workspace(
|
|
io.BytesIO(modal_module._encode_snapshot_directory_ref(snapshot_id="snap-dir-123"))
|
|
)
|
|
|
|
assert create_calls
|
|
assert session._sandbox is not None # noqa: SLF001
|
|
assert session._sandbox.mount_image_calls == [("/workspace", "snap-dir-123")] # noqa: SLF001
|
|
assert events == [("mount", "/workspace/actual")]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_directory_persist_only_detaches_durable_workspace_mounts(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
events: list[tuple[str, str]] = []
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"inside": _RecordingMount(
|
|
mount_path=Path("actual"),
|
|
ephemeral=False,
|
|
).bind_events(events),
|
|
"outside": _RecordingMount(
|
|
mount_path=Path("/mnt/remote"),
|
|
ephemeral=False,
|
|
).bind_events(events),
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
workspace_persistence="snapshot_directory",
|
|
exposed_ports=(8765,),
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
|
|
archive = await session.persist_workspace()
|
|
|
|
assert create_calls
|
|
assert session._sandbox is not None # noqa: SLF001
|
|
assert archive.read() == modal_module._encode_snapshot_directory_ref(snapshot_id="im-123")
|
|
assert events == [("unmount", "/workspace/actual"), ("mount", "/workspace/actual")]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_create_allows_snapshot_filesystem_with_modal_cloud_bucket_mounts(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
await client.create(
|
|
manifest=Manifest(
|
|
entries={
|
|
"remote": S3Mount(
|
|
bucket="bucket",
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(),
|
|
)
|
|
}
|
|
),
|
|
options=modal_module.ModalSandboxClientOptions(
|
|
app_name="sandbox-tests",
|
|
workspace_persistence="snapshot_filesystem",
|
|
),
|
|
)
|
|
|
|
assert create_calls
|
|
volumes = cast(dict[str, object], create_calls[0]["volumes"])
|
|
assert volumes.keys() == {"/workspace/remote"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_snapshot_filesystem_falls_back_to_tar_for_non_detachable_mounts(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeSnapshotSandbox:
|
|
object_id = "sb-123"
|
|
|
|
def snapshot_filesystem(self) -> str:
|
|
raise AssertionError("snapshot_filesystem() should not run for non-detachable mounts")
|
|
|
|
session = modal_module.ModalSandboxSession.from_state(
|
|
modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
root="/workspace",
|
|
entries={
|
|
"remote": S3Mount(
|
|
bucket="bucket",
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(),
|
|
)
|
|
},
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-123",
|
|
workspace_persistence="snapshot_filesystem",
|
|
),
|
|
sandbox=_FakeSnapshotSandbox(),
|
|
)
|
|
|
|
async def _fake_tar_persist() -> io.BytesIO:
|
|
return io.BytesIO(b"tar-fallback")
|
|
|
|
monkeypatch.setattr(session, "_persist_workspace_via_tar", _fake_tar_persist)
|
|
|
|
archive = await session.persist_workspace()
|
|
|
|
assert archive.read() == b"tar-fallback"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_create_rejects_snapshot_directory_with_cloud_bucket_mount_under_workspace(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
with pytest.raises(
|
|
MountConfigError,
|
|
match=(
|
|
"snapshot_directory is not supported when a Modal cloud bucket mount "
|
|
"lives at or under the workspace root"
|
|
),
|
|
):
|
|
await client.create(
|
|
manifest=Manifest(
|
|
entries={
|
|
"remote": S3Mount(
|
|
bucket="bucket",
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(),
|
|
)
|
|
}
|
|
),
|
|
options=modal_module.ModalSandboxClientOptions(
|
|
app_name="sandbox-tests",
|
|
workspace_persistence="snapshot_directory",
|
|
),
|
|
)
|
|
|
|
assert create_calls == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_create_allows_snapshot_directory_with_cloud_bucket_mount_outside_workspace(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
client = modal_module.ModalSandboxClient()
|
|
await client.create(
|
|
manifest=Manifest(
|
|
entries={
|
|
"remote": S3Mount(
|
|
bucket="bucket",
|
|
mount_path=Path("/mnt/remote"),
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(),
|
|
)
|
|
}
|
|
),
|
|
options=modal_module.ModalSandboxClientOptions(
|
|
app_name="sandbox-tests",
|
|
workspace_persistence="snapshot_directory",
|
|
),
|
|
)
|
|
|
|
assert create_calls
|
|
volumes = cast(dict[str, object], create_calls[0]["volumes"])
|
|
assert volumes.keys() == {"/mnt/remote"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_clear_workspace_root_on_resume_preserves_nested_cloud_bucket_mounts(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(
|
|
entries={
|
|
"a/b": S3Mount(
|
|
bucket="bucket",
|
|
mount_strategy=modal_module.ModalCloudBucketMountStrategy(),
|
|
),
|
|
}
|
|
),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state)
|
|
ls_calls: list[Path] = []
|
|
rm_calls: list[tuple[Path, bool]] = []
|
|
|
|
async def _fake_ls(path: Path | str) -> list[object]:
|
|
rendered = Path(path)
|
|
ls_calls.append(rendered)
|
|
if rendered == Path("/workspace"):
|
|
return [
|
|
types.SimpleNamespace(path="/workspace/a", kind=EntryKind.DIRECTORY),
|
|
types.SimpleNamespace(path="/workspace/root.txt", kind=EntryKind.FILE),
|
|
]
|
|
if rendered == Path("/workspace/a"):
|
|
return [
|
|
types.SimpleNamespace(path="/workspace/a/b", kind=EntryKind.DIRECTORY),
|
|
types.SimpleNamespace(path="/workspace/a/local.txt", kind=EntryKind.FILE),
|
|
]
|
|
raise AssertionError(f"unexpected ls path: {rendered}")
|
|
|
|
async def _fake_rm(path: Path | str, *, recursive: bool = False) -> None:
|
|
rm_calls.append((Path(path), recursive))
|
|
|
|
monkeypatch.setattr(session, "ls", _fake_ls)
|
|
monkeypatch.setattr(session, "rm", _fake_rm)
|
|
|
|
await session._clear_workspace_root_on_resume() # noqa: SLF001
|
|
|
|
assert ls_calls == [Path("/workspace"), Path("/workspace/a")]
|
|
assert rm_calls == [
|
|
(Path("/workspace/a/local.txt"), True),
|
|
(Path("/workspace/root.txt"), True),
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_pty_start_and_write_stdin(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeStream:
|
|
def __init__(self, chunks: list[bytes]) -> None:
|
|
self._chunks = chunks
|
|
self._chunk_event = asyncio.Event()
|
|
if self._chunks:
|
|
self._chunk_event.set()
|
|
self.read = _with_aio(self._read)
|
|
|
|
def __aiter__(self) -> _FakeStream:
|
|
return self
|
|
|
|
async def __anext__(self) -> bytes:
|
|
while not self._chunks:
|
|
self._chunk_event.clear()
|
|
await self._chunk_event.wait()
|
|
chunk = self._chunks.pop(0)
|
|
if not self._chunks:
|
|
self._chunk_event.clear()
|
|
return chunk
|
|
|
|
def append(self, chunk: bytes) -> None:
|
|
self._chunks.append(chunk)
|
|
self._chunk_event.set()
|
|
|
|
def _read(self, size: int | None = None) -> bytes:
|
|
if size is None:
|
|
raise AssertionError("PTY polling should not call read() with no size")
|
|
if self._chunks:
|
|
return self._chunks.pop(0)
|
|
return b""
|
|
|
|
class _FakeStdin:
|
|
def __init__(self, stdout: _FakeStream) -> None:
|
|
self.writes: list[bytes] = []
|
|
self._stdout = stdout
|
|
self.write = _with_aio(self._write)
|
|
self.drain = _with_aio(lambda: None)
|
|
|
|
def _write(self, payload: bytes) -> None:
|
|
self.writes.append(payload)
|
|
if payload == b"5 + 5\n":
|
|
self._stdout.append(b"10\n")
|
|
|
|
class _FakeProcess:
|
|
def __init__(self) -> None:
|
|
self.stdout = _FakeStream([b">>> "])
|
|
self.stderr = _FakeStream([])
|
|
self.stdin = _FakeStdin(self.stdout)
|
|
self.poll = _with_aio(lambda: None)
|
|
self.terminate = _with_aio(lambda: None)
|
|
|
|
class _FakeSandbox:
|
|
object_id = "sb-pty"
|
|
|
|
def __init__(self) -> None:
|
|
self.process = _FakeProcess()
|
|
self.exec_calls: list[tuple[tuple[object, ...], dict[str, object]]] = []
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
self.exec_calls.append((command, kwargs))
|
|
return self.process
|
|
|
|
sandbox = _FakeSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
|
|
started = await session.pty_exec_start("python3", shell=False, tty=True, yield_time_s=0.05)
|
|
|
|
assert started.process_id is not None
|
|
assert b">>>" in started.output
|
|
assert sandbox.exec_calls == [
|
|
(("python3",), {"text": False, "timeout": None, "pty": True}),
|
|
]
|
|
|
|
updated = await session.pty_write_stdin(
|
|
session_id=started.process_id,
|
|
chars="5 + 5\n",
|
|
yield_time_s=0.05,
|
|
)
|
|
|
|
assert updated.process_id == started.process_id
|
|
assert b"10" in updated.output
|
|
assert sandbox.process.stdin.writes == [b"5 + 5\n"]
|
|
|
|
await session.pty_terminate_all()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_pty_start_drains_all_buffered_output_after_exit(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeStream:
|
|
def __init__(self, chunks: list[bytes]) -> None:
|
|
self._chunks = chunks
|
|
self.read = _with_aio(self._read)
|
|
|
|
def __aiter__(self) -> _FakeStream:
|
|
return self
|
|
|
|
async def __anext__(self) -> bytes:
|
|
if self._chunks:
|
|
return self._chunks.pop(0)
|
|
raise StopAsyncIteration
|
|
|
|
def _read(self, _size: int | None = None) -> bytes:
|
|
raise AssertionError("PTY output collection should use stream iteration")
|
|
|
|
class _FakeProcess:
|
|
def __init__(self) -> None:
|
|
self.stdout = _FakeStream([b"out-1", b"out-2", b"out-3"])
|
|
self.stderr = _FakeStream([b"err-1", b"err-2"])
|
|
self.poll = _with_aio(lambda: 0)
|
|
self.terminate = _with_aio(lambda: None)
|
|
|
|
class _FakeSandbox:
|
|
object_id = "sb-exited"
|
|
|
|
def __init__(self) -> None:
|
|
self.process = _FakeProcess()
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = (command, kwargs)
|
|
return self.process
|
|
|
|
sandbox = _FakeSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
|
|
started = await session.pty_exec_start("python3", shell=False, tty=True, yield_time_s=0.05)
|
|
|
|
assert started.process_id is None
|
|
assert started.exit_code == 0
|
|
assert started.output == b"out-1err-1out-2out-3err-2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_pty_start_wraps_startup_failures(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FailingSandbox:
|
|
object_id = "sb-fail"
|
|
|
|
def __init__(self) -> None:
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = (command, kwargs)
|
|
raise FileNotFoundError("missing-shell")
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-fail",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=_FailingSandbox())
|
|
|
|
with pytest.raises(modal_module.ExecTransportError) as exc_info:
|
|
await session.pty_exec_start("python3", shell=False, tty=True)
|
|
assert str(exc_info.value) == "Modal exec failed: FileNotFoundError: missing-shell"
|
|
assert exc_info.value.context["backend"] == "modal"
|
|
assert exc_info.value.context["provider_error"] == "FileNotFoundError: missing-shell"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_pty_start_marks_typed_not_found_non_retryable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FailingSandbox:
|
|
object_id = "sb-fail"
|
|
|
|
def __init__(self) -> None:
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = (command, kwargs)
|
|
raise modal_module.modal.exception.NotFoundError("sandbox not found")
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-fail",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=_FailingSandbox())
|
|
|
|
with pytest.raises(modal_module.ExecTransportError) as exc_info:
|
|
await session.pty_exec_start("python3", shell=False, tty=True)
|
|
|
|
assert exc_info.value.retryable is False
|
|
assert exc_info.value.context["backend"] == "modal"
|
|
assert exc_info.value.context["reason"] == "_FakeModalNotFoundError"
|
|
assert exc_info.value.context["provider_error"] == "_FakeModalNotFoundError: sandbox not found"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_pty_start_marks_typed_internal_failure_retryable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FailingSandbox:
|
|
object_id = "sb-fail"
|
|
|
|
def __init__(self) -> None:
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = (command, kwargs)
|
|
raise modal_module.modal.exception.InternalFailure("internal failure")
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-fail",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=_FailingSandbox())
|
|
|
|
with pytest.raises(modal_module.ExecTransportError) as exc_info:
|
|
await session.pty_exec_start("python3", shell=False, tty=True)
|
|
|
|
assert exc_info.value.retryable is True
|
|
assert exc_info.value.context["backend"] == "modal"
|
|
assert exc_info.value.context["reason"] == "_FakeModalInternalFailure"
|
|
assert exc_info.value.context["provider_error"] == "_FakeModalInternalFailure: internal failure"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_start_wraps_exec_details(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FailingSandbox:
|
|
object_id = "sb-fail"
|
|
|
|
def __init__(self) -> None:
|
|
self.exec = _with_aio(self._exec)
|
|
self.poll = _with_aio(lambda: None)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = (command, kwargs)
|
|
raise FileNotFoundError("missing-shell")
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-fail",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=_FailingSandbox())
|
|
|
|
with pytest.raises(modal_module.WorkspaceStartError) as exc_info:
|
|
await session.start()
|
|
|
|
assert str(exc_info.value) == (
|
|
"failed to start session: Modal exec failed: FileNotFoundError: missing-shell"
|
|
)
|
|
assert exc_info.value.context["backend"] == "modal"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_pty_start_maps_timeout_failures(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _TimeoutSandbox:
|
|
object_id = "sb-timeout"
|
|
|
|
def __init__(self) -> None:
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = (command, kwargs)
|
|
raise asyncio.TimeoutError()
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-timeout",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=_TimeoutSandbox())
|
|
|
|
with pytest.raises(modal_module.ExecTimeoutError):
|
|
await session.pty_exec_start("python3", shell=False, tty=True, timeout=2.0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_pty_start_maps_modal_exec_timeout_failures(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _TimeoutSandbox:
|
|
object_id = "sb-timeout"
|
|
|
|
def __init__(self) -> None:
|
|
self.exec = _with_aio(self._exec)
|
|
|
|
def _exec(self, *command: object, **kwargs: object) -> object:
|
|
_ = (command, kwargs)
|
|
raise modal_module.modal.exception.ExecTimeoutError("command timed out")
|
|
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id="sb-timeout",
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=_TimeoutSandbox())
|
|
|
|
with pytest.raises(modal_module.ExecTimeoutError) as exc_info:
|
|
await session.pty_exec_start("python3", shell=False, tty=True, timeout=2.0)
|
|
|
|
assert exc_info.value.retryable is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_modal_pty_start_cleans_up_unregistered_process_on_cancellation(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
modal_module, _create_calls, _registry_tags = _load_modal_module(monkeypatch)
|
|
|
|
class _FakeStream:
|
|
def __init__(self) -> None:
|
|
self.read = _with_aio(lambda: b"")
|
|
|
|
class _FakeProcess:
|
|
def __init__(self) -> None:
|
|
self.stdout = _FakeStream()
|
|
self.stderr = _FakeStream()
|
|
self.poll = _with_aio(lambda: None)
|
|
self.terminate_calls = 0
|
|
self.terminate = _with_aio(self._terminate)
|
|
|
|
def _terminate(self) -> None:
|
|
self.terminate_calls += 1
|
|
|
|
class _FakeSandbox:
|
|
object_id = "sb-cancel"
|
|
|
|
def __init__(self) -> None:
|
|
self.process = _FakeProcess()
|
|
self.exec = _with_aio(lambda *args, **kwargs: self.process)
|
|
|
|
sandbox = _FakeSandbox()
|
|
state = modal_module.ModalSandboxSessionState(
|
|
manifest=Manifest(root="/workspace"),
|
|
snapshot=modal_module.resolve_snapshot(None, "snapshot"),
|
|
app_name="sandbox-tests",
|
|
sandbox_id=sandbox.object_id,
|
|
)
|
|
session = modal_module.ModalSandboxSession.from_state(state, sandbox=sandbox)
|
|
|
|
async def _raise_cancelled() -> None:
|
|
raise asyncio.CancelledError()
|
|
|
|
monkeypatch.setattr(session, "_prune_pty_processes_if_needed", _raise_cancelled)
|
|
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await session.pty_exec_start("python3", shell=False, tty=True)
|
|
|
|
assert sandbox.process.terminate_calls == 1
|
|
assert session._pty_processes == {} # noqa: SLF001
|