Files
2026-07-13 12:39:17 +08:00

1985 lines
74 KiB
Python

from __future__ import annotations
import asyncio
import builtins
import importlib
import io
import shlex
import sys
import types
import uuid
from collections import deque
from pathlib import Path
from typing import Any, Literal, cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pydantic import Field, PrivateAttr
import agents.extensions.sandbox.daytona.mounts as _daytona_mounts
from agents.extensions.sandbox.daytona.mounts import (
DaytonaCloudBucketMountStrategy,
_assert_daytona_session,
_ensure_fuse_support,
_ensure_rclone,
_has_command,
_pkg_install,
)
from agents.sandbox import Manifest, SandboxPathGrant
from agents.sandbox.entries import (
Dir,
InContainerMountStrategy,
Mount,
MountpointMountPattern,
RcloneMountPattern,
S3Mount,
)
from agents.sandbox.entries.mounts.base import InContainerMountAdapter
from agents.sandbox.errors import ExecTimeoutError, ExecTransportError, MountConfigError
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 (
_MKDIR_ACCESS_CHECK_SCRIPT,
BaseSandboxSession,
)
from agents.sandbox.session.dependencies import Dependencies
from agents.sandbox.snapshot import NoopSnapshot, SnapshotBase
from agents.sandbox.types import ExecResult, ExposedPortEndpoint, User
from tests._fake_workspace_paths import resolve_fake_workspace_path
from tests.utils.factories import TestSessionState
class _RestorableSnapshot(SnapshotBase):
type: Literal["test-restorable-daytona"] = "test-restorable-daytona"
payload: bytes = b"restored"
async def persist(self, data: io.IOBase, *, dependencies: Dependencies | None = None) -> None:
_ = (data, dependencies)
async def restore(self, *, dependencies: Dependencies | None = None) -> io.IOBase:
_ = dependencies
return io.BytesIO(self.payload)
async def restorable(self, *, dependencies: Dependencies | None = None) -> bool:
_ = dependencies
return True
class _FakeExecResult:
def __init__(self, *, exit_code: int = 0, result: str = "") -> None:
self.exit_code = exit_code
self.result = result
class _FakePtyHandle:
def __init__(self, on_data: object) -> None:
self._on_data = on_data
self.exit_code: int | None = None
self._done = asyncio.Event()
async def wait_for_connection(self) -> None:
return None
async def send_input(self, chars: str) -> None:
if chars.endswith("\n") and "python3" in chars:
await cast(Any, self._on_data)(b">>> ")
elif chars == "5 + 5\n":
await cast(Any, self._on_data)(b"10\n")
elif chars == "exit\n":
self.exit_code = 0
self._done.set()
async def wait(self) -> None:
await self._done.wait()
class _FakeProcess:
def __init__(self) -> None:
self.exec_calls: list[tuple[str, dict[str, object]]] = []
self.next_result = _FakeExecResult()
self.next_session_command_result = types.SimpleNamespace(
cmd_id="cmd-123",
exit_code=0,
stdout="",
stderr="",
output="",
)
self.create_pty_session_calls: list[dict[str, object]] = []
self.create_session_calls: list[str] = []
self.create_session_error: BaseException | None = None
self.create_session_delay_s: float = 0.0
self.kill_pty_session_calls: list[str] = []
self.delete_session_calls: list[str] = []
self.execute_session_command_calls: list[tuple[str, object, dict[str, object]]] = []
self.get_session_command_logs_error: BaseException | None = None
self.session_command_exit_code: int | None = 0
self._pty_handles: dict[str, _FakePtyHandle] = {}
self.create_pty_session_error: BaseException | None = None
self.symlinks: dict[str, str] = {}
self.workspace_roots: set[str] = set()
self.require_workspace_root_for_cd = False
async def exec(self, cmd: str, **kwargs: object) -> _FakeExecResult:
self.exec_calls.append((cmd, dict(kwargs)))
parts = shlex.split(cmd)
if len(parts) >= 4 and parts[:3] == ["mkdir", "-p", "--"]:
self.workspace_roots.add(parts[3])
if "sleep 0.5" in cmd:
await asyncio.sleep(0.5)
result = self.next_result
self.next_result = _FakeExecResult()
return result
async def create_pty_session(self, **kwargs: object) -> _FakePtyHandle:
if self.create_pty_session_error is not None:
raise self.create_pty_session_error
self.create_pty_session_calls.append(dict(kwargs))
session_id = cast(str, kwargs["id"])
handle = _FakePtyHandle(kwargs["on_data"])
self._pty_handles[session_id] = handle
return handle
async def kill_pty_session(self, session_id: str) -> None:
self.kill_pty_session_calls.append(session_id)
async def create_session(self, session_id: str) -> None:
self.create_session_calls.append(session_id)
if self.create_session_delay_s:
await asyncio.sleep(self.create_session_delay_s)
if self.create_session_error is not None:
raise self.create_session_error
async def execute_session_command(
self, session_id: str, request: object, **kwargs: object
) -> object:
self.execute_session_command_calls.append((session_id, request, dict(kwargs)))
command = cast(str, getattr(request, "command", ""))
parts = shlex.split(command)
if (
self.require_workspace_root_for_cd
and len(parts) >= 3
and parts[0] == "cd"
and parts[2] == "&&"
and parts[1] not in self.workspace_roots
):
return types.SimpleNamespace(
cmd_id="cmd-123",
exit_code=1,
stdout="",
stderr=f"cd: no such file or directory: {parts[1]}",
output=f"cd: no such file or directory: {parts[1]}",
)
resolved = resolve_fake_workspace_path(
command,
symlinks=self.symlinks,
home_dir="/home/daytona/workspace",
)
if resolved is not None:
return types.SimpleNamespace(
exit_code=resolved.exit_code,
stdout=resolved.stdout,
stderr=resolved.stderr,
output=resolved.stdout,
)
if "sleep 0.5" in command:
await asyncio.sleep(0.5)
if getattr(request, "run_async", None):
return types.SimpleNamespace(cmd_id="cmd-123")
result = self.next_session_command_result
self.next_session_command_result = types.SimpleNamespace(
cmd_id="cmd-123",
exit_code=0,
stdout="",
stderr="",
output="",
)
return result
async def get_session_command_logs_async(
self,
session_id: str,
cmd_id: str,
on_stdout: object,
on_stderr: object,
) -> None:
_ = (session_id, cmd_id, on_stderr)
if self.get_session_command_logs_error is not None:
raise self.get_session_command_logs_error
await cast(Any, on_stdout)("started\n")
async def get_session_command(self, session_id: str, cmd_id: str) -> object:
_ = (session_id, cmd_id)
return types.SimpleNamespace(exit_code=self.session_command_exit_code)
async def delete_session(self, session_id: str) -> None:
self.delete_session_calls.append(session_id)
class _FakeFs:
def __init__(self) -> None:
self.create_folder_calls: list[tuple[str, str]] = []
self.download_file_calls: list[tuple[str, float | None]] = []
self.upload_file_calls: list[tuple[bytes, str, float | None]] = []
self.download_value: bytes = b""
async def create_folder(self, path: str, mode: str) -> None:
self.create_folder_calls.append((path, mode))
async def download_file(self, path: str, timeout: float | None = None) -> bytes:
self.download_file_calls.append((path, timeout))
return self.download_value
async def upload_file(self, data: bytes, path: str, *, timeout: float | None = None) -> None:
self.upload_file_calls.append((data, path, timeout))
class _FakeDaytonaSandbox:
def __init__(self, *, sandbox_id: str = "sandbox-123") -> None:
self.id = sandbox_id
self.state = "started"
self.process = _FakeProcess()
self.fs = _FakeFs()
self.start_calls: list[int | None] = []
self.stop_calls = 0
self.delete_calls = 0
self.signed_preview_url_calls: list[tuple[int, int | None]] = []
async def refresh_data(self) -> None:
return None
async def start(self, *, timeout: int | None = None) -> None:
self.start_calls.append(timeout)
self.state = "started"
async def stop(self) -> None:
self.stop_calls += 1
async def delete(self) -> None:
self.delete_calls += 1
async def create_signed_preview_url(
self,
port: int,
expires_in_seconds: int | None = None,
) -> object:
self.signed_preview_url_calls.append((port, expires_in_seconds))
return types.SimpleNamespace(
url=f"https://{port}-signed-token.daytonaproxy01.net",
token="signed-token",
)
class _FakeAsyncDaytona:
create_calls: list[tuple[object, int | None]] = []
get_calls: list[str] = []
current_sandbox: _FakeDaytonaSandbox | None = None
get_error: BaseException | None = None
def __init__(self, config: object | None = None) -> None:
_ = config
@classmethod
def reset(cls) -> None:
cls.create_calls = []
cls.get_calls = []
cls.current_sandbox = None
cls.get_error = None
async def create(self, params: object, timeout: int | None = None) -> _FakeDaytonaSandbox:
type(self).create_calls.append((params, timeout))
sandbox = _FakeDaytonaSandbox()
type(self).current_sandbox = sandbox
return sandbox
async def get(self, sandbox_id: str) -> _FakeDaytonaSandbox:
type(self).get_calls.append(sandbox_id)
get_error = type(self).get_error
if get_error is not None:
raise get_error
if type(self).current_sandbox is None:
type(self).current_sandbox = _FakeDaytonaSandbox(sandbox_id=sandbox_id)
sandbox = type(self).current_sandbox
assert sandbox is not None
return sandbox
async def close(self) -> None:
return None
def _load_daytona_module(monkeypatch: pytest.MonkeyPatch) -> Any:
_FakeAsyncDaytona.reset()
class _FakeParams:
def __init__(self, **kwargs: object) -> None:
for key, value in kwargs.items():
setattr(self, key, value)
class _FakeDaytonaConfig:
def __init__(self, api_key: str | None = None, api_url: str | None = None) -> None:
self.api_key = api_key
self.api_url = api_url
class _FakePtySize:
def __init__(self, *, cols: int, rows: int) -> None:
self.cols = cols
self.rows = rows
class _FakeResources:
def __init__(
self,
*,
cpu: int | None = None,
memory: int | None = None,
disk: int | None = None,
) -> None:
self.cpu = cpu
self.memory = memory
self.disk = disk
fake_daytona: Any = types.ModuleType("daytona")
fake_daytona.AsyncDaytona = _FakeAsyncDaytona
fake_daytona.DaytonaConfig = _FakeDaytonaConfig
fake_daytona.CreateSandboxFromSnapshotParams = _FakeParams
fake_daytona.CreateSandboxFromImageParams = _FakeParams
fake_daytona.SessionExecuteRequest = _FakeParams
fake_daytona.Resources = _FakeResources
fake_daytona.SandboxState = types.SimpleNamespace(STARTED="started")
fake_daytona_common: Any = types.ModuleType("daytona.common")
fake_daytona_common_pty: Any = types.ModuleType("daytona.common.pty")
fake_daytona_common_pty.PtySize = _FakePtySize
monkeypatch.setitem(sys.modules, "daytona", fake_daytona)
monkeypatch.setitem(sys.modules, "daytona.common", fake_daytona_common)
monkeypatch.setitem(sys.modules, "daytona.common.pty", fake_daytona_common_pty)
sys.modules.pop("agents.extensions.sandbox.daytona.sandbox", None)
sys.modules.pop("agents.extensions.sandbox.daytona", None)
return importlib.import_module("agents.extensions.sandbox.daytona.sandbox")
def test_daytona_package_re_exports_backend_symbols(monkeypatch: pytest.MonkeyPatch) -> None:
daytona_module = _load_daytona_module(monkeypatch)
package_module = importlib.import_module("agents.extensions.sandbox.daytona")
assert package_module.DaytonaSandboxClient is daytona_module.DaytonaSandboxClient
class _RecordingMount(Mount):
type: str = "daytona_recording_mount"
mount_strategy: InContainerMountStrategy = Field(
default_factory=lambda: InContainerMountStrategy(pattern=MountpointMountPattern())
)
_mounted_paths: list[Path] = PrivateAttr(default_factory=list)
_unmounted_paths: list[Path] = PrivateAttr(default_factory=list)
_events: list[tuple[str, str]] = PrivateAttr(default_factory=list)
def bind_events(self, events: list[tuple[str, str]]) -> _RecordingMount:
self._events = events
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, base_dir)
path = mount._resolve_mount_path(session, dest)
mount._events.append(("mount", path.as_posix()))
mount._mounted_paths.append(path)
return []
async def deactivate(
self,
strategy: InContainerMountStrategy,
session: BaseSandboxSession,
dest: Path,
base_dir: Path,
) -> None:
_ = (strategy, session, base_dir)
path = mount._resolve_mount_path(session, dest)
mount._events.append(("unmount", path.as_posix()))
mount._unmounted_paths.append(path)
async def teardown_for_snapshot(
self,
strategy: InContainerMountStrategy,
session: BaseSandboxSession,
path: Path,
) -> None:
_ = (strategy, session)
mount._events.append(("unmount", path.as_posix()))
mount._unmounted_paths.append(path)
async def restore_after_snapshot(
self,
strategy: InContainerMountStrategy,
session: BaseSandboxSession,
path: Path,
) -> None:
_ = (strategy, session)
mount._events.append(("mount", path.as_posix()))
mount._mounted_paths.append(path)
return _Adapter(self)
async def mount(self, session: object, path: Path) -> None:
_ = session
self._events.append(("mount", path.as_posix()))
self._mounted_paths.append(path)
async def unmount_path(self, session: object, path: Path) -> None:
_ = session
self._events.append(("unmount", path.as_posix()))
self._unmounted_paths.append(path)
class _FailingUnmountMount(_RecordingMount):
type: str = "daytona_failing_unmount_mount"
def in_container_adapter(self) -> InContainerMountAdapter:
mount = self
base_adapter = super().in_container_adapter()
class _Adapter(InContainerMountAdapter):
def validate(self, strategy: InContainerMountStrategy) -> None:
base_adapter.validate(strategy)
async def activate(
self,
strategy: InContainerMountStrategy,
session: BaseSandboxSession,
dest: Path,
base_dir: Path,
) -> list[MaterializedFile]:
return await base_adapter.activate(strategy, session, dest, base_dir)
async def deactivate(
self,
strategy: InContainerMountStrategy,
session: BaseSandboxSession,
dest: Path,
base_dir: Path,
) -> None:
_ = (strategy, session, base_dir)
path = mount._resolve_mount_path(session, dest)
mount._events.append(("unmount_fail", path.as_posix()))
raise RuntimeError("boom while unmounting second mount")
async def teardown_for_snapshot(
self,
strategy: InContainerMountStrategy,
session: BaseSandboxSession,
path: Path,
) -> None:
_ = (strategy, session)
mount._events.append(("unmount_fail", path.as_posix()))
raise RuntimeError("boom while unmounting second mount")
async def restore_after_snapshot(
self,
strategy: InContainerMountStrategy,
session: BaseSandboxSession,
path: Path,
) -> None:
await base_adapter.restore_after_snapshot(strategy, session, path)
return _Adapter(self)
async def unmount_path(self, session: object, path: Path) -> None:
_ = session
self._events.append(("unmount_fail", path.as_posix()))
raise RuntimeError("boom while unmounting second mount")
class TestDaytonaSandbox:
@pytest.mark.asyncio
async def test_create_uses_daytona_safe_default_workspace_root(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify omitted manifests default to a writable Daytona workspace root."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
assert session.state.manifest.root == daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT
@pytest.mark.asyncio
async def test_start_prepares_workspace_root_before_runtime_helpers(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify Daytona creates the root before exec uses it as cwd."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
sandbox.process.require_workspace_root_for_cd = True
await session.start()
root = daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT
assert root in sandbox.process.workspace_roots
assert sandbox.process.exec_calls[0][0] == f"mkdir -p -- {root}"
assert sandbox.process.execute_session_command_calls
_session_id, request, _kwargs = sandbox.process.execute_session_command_calls[0]
assert cast(str, cast(Any, request).command).startswith(f"cd {root} && ")
assert session.state.workspace_root_ready is True
@pytest.mark.asyncio
async def test_start_wraps_workspace_root_prepare_failure(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify Daytona surfaces root preparation failures as start errors."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
sandbox.process.next_result = _FakeExecResult(exit_code=2, result="mkdir failed")
with pytest.raises(daytona_module.WorkspaceStartError) as exc_info:
await session.start()
assert exc_info.value.context == {
"path": daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
"backend": "daytona",
"reason": "workspace_root_nonzero_exit",
"exit_code": 2,
"output": "mkdir failed",
}
assert str(exc_info.value) == (
"failed to start session: Daytona workspace root setup exited with 2: mkdir failed"
)
assert sandbox.process.execute_session_command_calls == []
assert session.state.workspace_root_ready is False
@pytest.mark.asyncio
async def test_create_passes_only_option_env_vars_to_daytona(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify manifest env vars are not passed into Daytona's create-time env shell."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
await client.create(
manifest=Manifest(
root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
environment=Environment(value={"SHARED": "manifest", "ONLY_MANIFEST": "1"}),
),
options=daytona_module.DaytonaSandboxClientOptions(
env_vars={"SHARED": "option", "ONLY_OPTION": "1"},
),
)
assert _FakeAsyncDaytona.create_calls
params, _timeout = _FakeAsyncDaytona.create_calls[0]
assert cast(Any, params).env_vars == {
"SHARED": "option",
"ONLY_OPTION": "1",
}
@pytest.mark.asyncio
async def test_exec_enforces_subsecond_caller_timeout(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify a sub-second user timeout fails even though the SDK timeout is ceiled."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
with pytest.raises(ExecTimeoutError):
await session.exec("sleep 0.5", shell=False, timeout=0.1)
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
_session_id, _request, kwargs = sandbox.process.execute_session_command_calls[0]
assert kwargs["timeout"] == 2
@pytest.mark.asyncio
async def test_exec_timeout_budget_includes_session_create(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
sandbox.process.create_session_delay_s = 0.2
await session.exec("echo", "done", shell=False, timeout=1.1)
assert sandbox.process.create_session_calls
_session_id, _request, kwargs = sandbox.process.execute_session_command_calls[0]
assert kwargs["timeout"] == 2
@pytest.mark.asyncio
async def test_exec_delete_session_cleanup_is_bounded(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
real_wait_for = asyncio.wait_for
cleanup_timeouts: list[float | None] = []
async def _record_cleanup_wait_for(awaitable: Any, timeout: float | None = None) -> Any:
code = getattr(awaitable, "cr_code", None)
if getattr(code, "co_name", None) == "delete_session":
awaitable.close()
cleanup_timeouts.append(timeout)
return None
return await real_wait_for(awaitable, timeout=timeout)
monkeypatch.setattr(daytona_module.asyncio, "wait_for", _record_cleanup_wait_for)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(
options=daytona_module.DaytonaSandboxClientOptions(
timeouts=daytona_module.DaytonaSandboxTimeouts(cleanup_s=7)
)
)
await session.exec("echo", "done", shell=False, timeout=5.0)
assert cleanup_timeouts == [7]
@pytest.mark.asyncio
async def test_exec_merges_manifest_env_with_option_precedence(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify manifest env vars are applied through the adapter-controlled exec path."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(
manifest=Manifest(
root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
environment=Environment(value={"SHARED": "manifest", "ONLY_MANIFEST": "1"}),
),
options=daytona_module.DaytonaSandboxClientOptions(
env_vars={"SHARED": "option", "ONLY_OPTION": "1"},
),
)
await session.exec("printenv", "SHARED", shell=False, timeout=5.0)
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
_session_id, request, _kwargs = sandbox.process.execute_session_command_calls[0]
command = cast(str, cast(Any, request).command)
assert "env --" in command
assert "SHARED=manifest" in command
assert "ONLY_MANIFEST=1" in command
assert "ONLY_OPTION=1" in command
@pytest.mark.asyncio
async def test_exec_preserves_session_command_stdout_and_stderr(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
sandbox.process.next_session_command_result = types.SimpleNamespace(
cmd_id="cmd-123",
exit_code=7,
stdout="hello stdout",
stderr="hello stderr",
output="hello stdouthello stderr",
)
result = await session.exec("sh", "-c", "printf out; printf err >&2", shell=False)
assert result.exit_code == 7
assert result.stdout == b"hello stdout"
assert result.stderr == b"hello stderr"
@pytest.mark.asyncio
async def test_resume_reconnects_paused_sandbox_and_preserves_state(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify pause-on-exit resumes an existing sandbox instead of creating a new one."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(
options=daytona_module.DaytonaSandboxClientOptions(pause_on_exit=True),
)
state = session.state
_FakeAsyncDaytona.create_calls.clear()
resumed = await client.resume(state)
assert _FakeAsyncDaytona.get_calls == [state.sandbox_id]
assert _FakeAsyncDaytona.create_calls == []
assert resumed._inner._workspace_state_preserved_on_start() is True # noqa: SLF001
assert resumed._inner._system_state_preserved_on_start() is True # noqa: SLF001
assert resumed._inner._can_reuse_preserved_workspace_on_resume() is False # noqa: SLF001
@pytest.mark.asyncio
async def test_resume_reconnects_unpaused_live_sandbox_after_unclean_worker_exit(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify resume reconnects to a live sandbox that was never cleanly deleted."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
state = session.state
_FakeAsyncDaytona.create_calls.clear()
resumed = await client.resume(state)
assert _FakeAsyncDaytona.get_calls == [state.sandbox_id]
assert _FakeAsyncDaytona.create_calls == []
assert resumed.state.sandbox_id == state.sandbox_id
assert resumed._inner._workspace_state_preserved_on_start() is True # noqa: SLF001
assert resumed._inner._system_state_preserved_on_start() is True # noqa: SLF001
@pytest.mark.asyncio
async def test_resume_recreates_unpaused_sandbox_when_reconnect_fails(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify resume falls back to a fresh Daytona sandbox when the old id is gone."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
state = session.state
old_sandbox_id = state.sandbox_id
_FakeAsyncDaytona.create_calls.clear()
_FakeAsyncDaytona.get_error = RuntimeError("sandbox_not_found")
resumed = await client.resume(state)
assert _FakeAsyncDaytona.get_calls == [old_sandbox_id]
assert len(_FakeAsyncDaytona.create_calls) == 1
assert resumed.state.sandbox_id == "sandbox-123"
assert resumed._inner._workspace_state_preserved_on_start() is False # noqa: SLF001
assert resumed._inner._system_state_preserved_on_start() is False # noqa: SLF001
@pytest.mark.asyncio
async def test_preserved_start_rehydrates_when_snapshot_gate_requests_restore(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify resumed paused sandboxes can still rehydrate when the fingerprint gate fails."""
daytona_module = _load_daytona_module(monkeypatch)
session = daytona_module.DaytonaSandboxSession.from_state(
daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root="/workspace"),
snapshot=_RestorableSnapshot(id="snapshot"),
sandbox_id="sandbox-123",
pause_on_exit=True,
workspace_root_ready=True,
),
sandbox=_FakeDaytonaSandbox(),
)
session._set_start_state_preserved(True) # noqa: SLF001
events: list[object] = []
async def _running() -> bool:
return True
async def _gate(*, is_running: bool) -> bool:
events.append(("gate", is_running))
return False
async def _restore() -> None:
events.append("restore")
async def _reapply() -> None:
events.append("reapply")
monkeypatch.setattr(session, "running", _running)
session._can_skip_snapshot_restore_on_resume = _gate
monkeypatch.setattr(session, "_restore_snapshot_into_workspace_on_resume", _restore)
monkeypatch.setattr(session, "_reapply_ephemeral_manifest_on_resume", _reapply)
await session.start()
assert events == [("gate", True), "restore", "reapply"]
@pytest.mark.asyncio
async def test_resolve_exposed_port_uses_signed_preview_url(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify Daytona maps signed preview URLs to the shared exposed-port endpoint shape."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(
options=daytona_module.DaytonaSandboxClientOptions(
exposed_ports=(4500,),
exposed_port_url_ttl_s=1800,
),
)
endpoint = await session.resolve_exposed_port(4500)
assert endpoint == ExposedPortEndpoint(
host="4500-signed-token.daytonaproxy01.net",
port=443,
tls=True,
)
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
assert sandbox.signed_preview_url_calls == [(4500, 1800)]
@pytest.mark.asyncio
async def test_resolve_exposed_port_rejects_invalid_preview_urls(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify malformed Daytona preview URLs become ExposedPortUnavailableError."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(
options=daytona_module.DaytonaSandboxClientOptions(exposed_ports=(4500,)),
)
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
async def _bad_preview_url(
port: int,
expires_in_seconds: int | None = None,
) -> object:
_ = (port, expires_in_seconds)
return types.SimpleNamespace(url=":", token="bad")
sandbox.create_signed_preview_url = _bad_preview_url # type: ignore[method-assign]
with pytest.raises(daytona_module.ExposedPortUnavailableError) as exc_info:
await session.resolve_exposed_port(4500)
assert exc_info.value.context["detail"] == "invalid_preview_url"
@pytest.mark.asyncio
async def test_normalize_path_rejects_workspace_escape_and_allows_absolute_in_root(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify Daytona normalizes paths without host resolution and enforces the root."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
inner = session._inner # noqa: SLF001
with pytest.raises(daytona_module.InvalidManifestPathError):
inner.normalize_path("../outside")
with pytest.raises(daytona_module.InvalidManifestPathError):
inner.normalize_path("/etc/passwd")
assert inner.normalize_path(
f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/nested/file.txt"
) == Path(f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/nested/file.txt")
@pytest.mark.asyncio
async def test_read_and_write_reject_paths_outside_workspace_root(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify Daytona read/write reject absolute and traversal paths before remote FS calls."""
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
with pytest.raises(daytona_module.InvalidManifestPathError):
await session.read("../outside.txt")
with pytest.raises(daytona_module.InvalidManifestPathError):
await session.write("/etc/passwd", io.BytesIO(b"nope"))
@pytest.mark.asyncio
async def test_read_rejects_workspace_symlink_to_ungranted_path(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
sandbox.process.symlinks[f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/link"] = (
"/private"
)
with pytest.raises(daytona_module.InvalidManifestPathError) as exc_info:
await session.read("link/secret.txt")
assert sandbox.fs.download_file_calls == []
assert str(exc_info.value) == "manifest path must not escape root: link/secret.txt"
assert exc_info.value.context == {
"rel": "link/secret.txt",
"reason": "escape_root",
"resolved_path": "workspace escape: /private/secret.txt",
}
@pytest.mark.asyncio
async def test_write_rejects_workspace_symlink_to_read_only_extra_path_grant(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(
manifest=Manifest(
root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
extra_path_grants=(SandboxPathGrant(path="/tmp/protected", read_only=True),),
),
options=daytona_module.DaytonaSandboxClientOptions(),
)
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
sandbox.process.symlinks[f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/link"] = (
"/tmp/protected"
)
with pytest.raises(daytona_module.WorkspaceArchiveWriteError) as exc_info:
await session.write("link/out.txt", io.BytesIO(b"blocked"))
assert sandbox.fs.upload_file_calls == []
assert str(exc_info.value) == (
"failed to write archive for path: "
f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/link/out.txt"
)
assert exc_info.value.context == {
"path": f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/link/out.txt",
"reason": "read_only_extra_path_grant",
"grant_path": "/tmp/protected",
"resolved_path": "/tmp/protected/out.txt",
}
@pytest.mark.asyncio
async def test_mkdir_rejects_workspace_symlink_to_read_only_extra_path_grant(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(
manifest=Manifest(
root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
extra_path_grants=(SandboxPathGrant(path="/tmp/protected", read_only=True),),
),
options=daytona_module.DaytonaSandboxClientOptions(),
)
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
sandbox.process.symlinks[f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/link"] = (
"/tmp/protected"
)
with pytest.raises(daytona_module.WorkspaceArchiveWriteError) as exc_info:
await session.mkdir("link/newdir")
assert sandbox.fs.create_folder_calls == []
assert str(exc_info.value) == (
"failed to write archive for path: "
f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/link/newdir"
)
assert exc_info.value.context == {
"path": f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/link/newdir",
"reason": "read_only_extra_path_grant",
"grant_path": "/tmp/protected",
"resolved_path": "/tmp/protected/newdir",
}
@pytest.mark.asyncio
async def test_mkdir_as_user_checks_permissions_then_uses_files_api(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
async with daytona_module.DaytonaSandboxClient() as client:
session = await client.create(options=daytona_module.DaytonaSandboxClientOptions())
sandbox = _FakeAsyncDaytona.current_sandbox
assert sandbox is not None
await session.mkdir("nested", user=User(name="sandbox-user"))
assert sandbox.fs.create_folder_calls == [
(f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/nested", "755")
]
commands = [
cast(str, cast(Any, request).command)
for _session_id, request, _kwargs in sandbox.process.execute_session_command_calls
]
expected_cmd = f"cd {daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT} && " + shlex.join(
[
"sudo",
"-u",
"sandbox-user",
"--",
"sh",
"-lc",
_MKDIR_ACCESS_CHECK_SCRIPT,
"sh",
f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/nested",
"0",
]
)
assert commands[-1] == expected_cmd
@pytest.mark.asyncio
async def test_persist_workspace_remounts_mounts_after_snapshot(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify mounts are restored after a Daytona workspace snapshot completes."""
daytona_module = _load_daytona_module(monkeypatch)
mount = _RecordingMount()
sandbox = _FakeDaytonaSandbox()
sandbox.fs.download_value = b"fake-tar-bytes"
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(
root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
entries={"mount": mount},
),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
archive = await session.persist_workspace()
assert archive.read() == b"fake-tar-bytes"
mount_path = Path(f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/mount")
assert mount._unmounted_paths == [mount_path]
assert mount._mounted_paths == [mount_path]
@pytest.mark.asyncio
async def test_persist_workspace_marks_stopped_sandbox_non_retryable(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify stopped Daytona sandboxes expose provider-neutral retryability."""
daytona_module = _load_daytona_module(monkeypatch)
sandbox = _FakeDaytonaSandbox()
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
async def _raise_stopped_sandbox(_cmd: str, **_kwargs: object) -> object:
raise RuntimeError(
"bad request: failed to resolve container IP after 3 attempts: "
"no IP address found. Is the Sandbox started?"
)
monkeypatch.setattr(sandbox.process, "exec", _raise_stopped_sandbox)
with pytest.raises(daytona_module.WorkspaceArchiveReadError) as exc_info:
await session.persist_workspace()
assert exc_info.value.retryable is False
assert exc_info.value.context["backend"] == "daytona"
assert exc_info.value.context["reason"] == "sandbox_not_running"
@pytest.mark.asyncio
async def test_persist_workspace_uses_nested_mount_targets_and_runtime_skip_paths(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify Daytona excludes nested mount targets and runtime-registered skip paths."""
daytona_module = _load_daytona_module(monkeypatch)
parent_mount = _RecordingMount(mount_path=Path("repo"))
child_mount = _RecordingMount(mount_path=Path("repo/sub"))
events: list[tuple[str, str]] = []
sandbox = _FakeDaytonaSandbox()
sandbox.fs.download_value = b"fake-tar-bytes"
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(
root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
entries={
"parent": parent_mount.bind_events(events),
"nested": Dir(children={"child": child_mount.bind_events(events)}),
},
),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
session.register_persist_workspace_skip_path("runtime.tmp")
archive = await session.persist_workspace()
assert archive.read() == b"fake-tar-bytes"
assert {path for kind, path in events if kind == "unmount"} == {
f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/repo",
f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/repo/sub",
}
assert {path for kind, path in events if kind == "mount"} == {
f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/repo",
f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/repo/sub",
}
tar_command = sandbox.process.exec_calls[0][0]
assert "--exclude=repo" in tar_command
assert "--exclude=./repo" in tar_command
assert "--exclude=repo/sub" in tar_command
assert "--exclude=./repo/sub" in tar_command
assert "--exclude=runtime.tmp" in tar_command
@pytest.mark.asyncio
async def test_persist_workspace_remounts_prior_mounts_after_unmount_failure(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify a partial Daytona unmount failure remounts earlier mounts before raising."""
daytona_module = _load_daytona_module(monkeypatch)
events: list[tuple[str, str]] = []
sandbox = _FakeDaytonaSandbox()
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(
root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
entries={
"repo": Dir(
children={
"mount1": _RecordingMount().bind_events(events),
"mount2": _FailingUnmountMount().bind_events(events),
}
)
},
),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
with pytest.raises(daytona_module.WorkspaceArchiveReadError):
await session.persist_workspace()
assert [kind for kind, _path in events] == [
"unmount",
"unmount_fail",
"mount",
]
assert sandbox.process.exec_calls == []
@pytest.mark.asyncio
async def test_clear_workspace_root_on_resume_preserves_nested_mounts(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify inherited resume cleanup skips mounted directories."""
daytona_module = _load_daytona_module(monkeypatch)
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(
root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT,
entries={
"a/b": _RecordingMount(),
},
),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id="sandbox-123",
)
session = daytona_module.DaytonaSandboxSession.from_state(
state,
sandbox=_FakeDaytonaSandbox(),
)
workspace_root = Path(daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT)
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 == workspace_root:
return [
types.SimpleNamespace(
path=str(workspace_root / "a"),
kind=EntryKind.DIRECTORY,
),
types.SimpleNamespace(
path=str(workspace_root / "root.txt"),
kind=EntryKind.FILE,
),
]
if rendered == workspace_root / "a":
return [
types.SimpleNamespace(
path=str(workspace_root / "a/b"),
kind=EntryKind.DIRECTORY,
),
types.SimpleNamespace(
path=str(workspace_root / "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 == [workspace_root, workspace_root / "a"]
assert rm_calls == [
(workspace_root / "a/local.txt", True),
(workspace_root / "root.txt", True),
]
@pytest.mark.asyncio
async def test_pty_start_write_and_exit(self, monkeypatch: pytest.MonkeyPatch) -> None:
daytona_module = _load_daytona_module(monkeypatch)
sandbox = _FakeDaytonaSandbox()
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.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
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
finished = await session.pty_write_stdin(
session_id=started.process_id,
chars="exit\n",
yield_time_s=0.05,
)
assert finished.process_id is None
assert finished.exit_code == 0
@pytest.mark.asyncio
async def test_stop_terminates_live_pty_sessions(self, monkeypatch: pytest.MonkeyPatch) -> None:
daytona_module = _load_daytona_module(monkeypatch)
sandbox = _FakeDaytonaSandbox()
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.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
await session.stop()
assert sandbox.process.kill_pty_session_calls
@pytest.mark.asyncio
async def test_pty_start_wraps_startup_failures(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
sandbox = _FakeDaytonaSandbox()
sandbox.process.create_pty_session_error = FileNotFoundError("missing-shell")
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
with pytest.raises(ExecTransportError) as exc_info:
await session.pty_exec_start("python3", shell=False, tty=True)
assert str(exc_info.value) == "Daytona exec failed: FileNotFoundError: missing-shell"
assert exc_info.value.context["backend"] == "daytona"
assert exc_info.value.context["provider_error"] == "FileNotFoundError: missing-shell"
@pytest.mark.asyncio
async def test_pty_start_maps_sdk_timeout_failures(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
class _FakeTimeout(Exception):
pass
monkeypatch.setattr(
daytona_module,
"_daytona_timeout_error_types",
lambda: (_FakeTimeout,),
)
sandbox = _FakeDaytonaSandbox()
sandbox.process.create_session_error = _FakeTimeout("timed out")
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
with pytest.raises(ExecTimeoutError):
await session.pty_exec_start("python3", shell=False, tty=False, timeout=2.0)
@pytest.mark.asyncio
async def test_pty_start_marks_documented_sdk_not_found_non_retryable(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
class _FakeNotFound(Exception):
status_code = 404
error_code = "sandbox_not_found"
monkeypatch.setattr(
daytona_module,
"_daytona_non_retryable_error_types",
lambda: (_FakeNotFound,),
)
monkeypatch.setattr(daytona_module, "_daytona_retryable_error_types", lambda: ())
sandbox = _FakeDaytonaSandbox()
sandbox.process.create_pty_session_error = _FakeNotFound("sandbox not found")
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
with pytest.raises(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"] == "daytona"
assert exc_info.value.context["http_status"] == 404
assert exc_info.value.context["provider_error_code"] == "sandbox_not_found"
assert exc_info.value.context["reason"] == "sandbox_not_found"
@pytest.mark.asyncio
async def test_pty_start_marks_documented_sdk_rate_limit_retryable(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
class _FakeRateLimit(Exception):
status_code = 429
error_code = "rate_limit_exceeded"
monkeypatch.setattr(
daytona_module,
"_daytona_retryable_error_types",
lambda: (_FakeRateLimit,),
)
monkeypatch.setattr(daytona_module, "_daytona_non_retryable_error_types", lambda: ())
sandbox = _FakeDaytonaSandbox()
sandbox.process.create_pty_session_error = _FakeRateLimit("rate limit exceeded")
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
with pytest.raises(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"] == "daytona"
assert exc_info.value.context["http_status"] == 429
assert exc_info.value.context["provider_error_code"] == "rate_limit_exceeded"
assert exc_info.value.context["reason"] == "rate_limit_exceeded"
@pytest.mark.parametrize(
("status", "expected_retryable"),
[
(400, False),
(401, False),
(403, False),
(404, False),
(409, False),
(429, True),
(500, True),
(502, True),
(503, True),
(504, True),
],
)
def test_daytona_retryability_status_table(
self,
monkeypatch: pytest.MonkeyPatch,
status: int,
expected_retryable: bool,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
monkeypatch.setattr(daytona_module, "_daytona_non_retryable_error_types", lambda: ())
monkeypatch.setattr(daytona_module, "_daytona_retryable_error_types", lambda: ())
class FakeStatusError(Exception):
status_code = status
retryable, reason = daytona_module._daytona_provider_retryability(FakeStatusError())
assert retryable is expected_retryable
assert reason == f"http_{status}"
@pytest.mark.asyncio
async def test_session_reader_keeps_entry_live_when_logs_fail_without_exit_code(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
sandbox = _FakeDaytonaSandbox()
sandbox.process.get_session_command_logs_error = RuntimeError("logs failed")
sandbox.process.session_command_exit_code = None
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
entry = daytona_module._DaytonaPtySessionEntry( # noqa: SLF001
daytona_session_id="session-123",
pty_handle=object(),
tty=False,
cmd_id="cmd-123",
)
await session._run_session_reader( # noqa: SLF001
entry,
"session-123",
"cmd-123",
lambda _chunk: None,
)
assert entry.done is False
assert entry.exit_code is None
@pytest.mark.asyncio
async def test_terminate_pty_entry_awaits_worker_finalizer(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
sandbox = _FakeDaytonaSandbox()
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
entry = daytona_module._DaytonaPtySessionEntry( # noqa: SLF001
daytona_session_id="session-123",
pty_handle=object(),
tty=False,
cmd_id="cmd-123",
)
finalizer_finished = asyncio.Event()
async def worker() -> None:
try:
await asyncio.Event().wait()
finally:
await asyncio.sleep(0)
finalizer_finished.set()
entry.worker_task = asyncio.create_task(worker())
await asyncio.sleep(0)
await session._terminate_pty_entry(entry) # noqa: SLF001
assert finalizer_finished.is_set()
assert entry.worker_task is None
assert sandbox.process.delete_session_calls == ["session-123"]
@pytest.mark.asyncio
async def test_terminate_pty_entry_bounds_worker_finalizer(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
daytona_module = _load_daytona_module(monkeypatch)
sandbox = _FakeDaytonaSandbox()
state = daytona_module.DaytonaSandboxSessionState(
manifest=Manifest(root=daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT),
snapshot=NoopSnapshot(id="snapshot"),
sandbox_id=sandbox.id,
)
monkeypatch.setattr(state.timeouts, "cleanup_s", 0.01)
session = daytona_module.DaytonaSandboxSession.from_state(state, sandbox=sandbox)
entry = daytona_module._DaytonaPtySessionEntry( # noqa: SLF001
daytona_session_id="session-123",
pty_handle=object(),
tty=False,
cmd_id="cmd-123",
)
logs_started = asyncio.Event()
finalizer_started = asyncio.Event()
async def read_logs(*_args: object) -> None:
logs_started.set()
await asyncio.Event().wait()
async def get_command(*_args: object) -> object:
finalizer_started.set()
await asyncio.Event().wait()
return types.SimpleNamespace(exit_code=None)
monkeypatch.setattr(sandbox.process, "get_session_command_logs_async", read_logs)
monkeypatch.setattr(sandbox.process, "get_session_command", get_command)
worker_task = asyncio.create_task(
session._run_session_reader( # noqa: SLF001
entry,
"session-123",
"cmd-123",
lambda _chunk: None,
)
)
entry.worker_task = worker_task
await logs_started.wait()
await asyncio.wait_for(session._terminate_pty_entry(entry), timeout=0.5) # noqa: SLF001
assert finalizer_started.is_set()
assert worker_task.done()
assert entry.worker_task is None
assert sandbox.process.delete_session_calls == ["session-123"]
# ---------------------------------------------------------------------------
# DaytonaCloudBucketMountStrategy tests
# ---------------------------------------------------------------------------
class _FakePreflightSession(BaseSandboxSession):
"""Fake session for testing mount preflights with queued exec results."""
# Make type(instance).__name__ return "DaytonaSandboxSession" so the session guard passes.
__name__ = "DaytonaSandboxSession"
def __init__(self, results: list[ExecResult] | None = None) -> None:
self.state = TestSessionState(
session_id=uuid.uuid4(),
manifest=Manifest(root="/workspace"),
snapshot=NoopSnapshot(id="test"),
)
self._results: deque[ExecResult] = deque(results or [])
self.exec_calls: list[str] = []
def _ok(self) -> ExecResult:
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
def _fail(self) -> ExecResult:
return ExecResult(stdout=b"", stderr=b"", exit_code=1)
async def _exec_internal(
self,
*command: str | Path,
timeout: float | None = None,
) -> ExecResult:
_ = timeout
cmd_str = " ".join(str(c) for c in command)
self.exec_calls.append(cmd_str)
if self._results:
return self._results.popleft()
return self._ok()
async def read(self, path: Path, *, user: object = None) -> io.IOBase:
_ = (path, user)
return io.BytesIO(b"")
async def write(self, path: Path, data: io.IOBase, *, user: object = None) -> None:
_ = (path, data, user)
async def running(self) -> bool:
return True
async def persist_workspace(self) -> io.IOBase:
raise AssertionError("not expected")
async def hydrate_workspace(self, data: io.IOBase) -> None:
raise AssertionError("not expected")
# Override __name__ at the class level so type(instance).__name__ == "DaytonaSandboxSession".
_FakePreflightSession.__name__ = "DaytonaSandboxSession"
def _ok() -> ExecResult:
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
def _fail() -> ExecResult:
return ExecResult(stdout=b"", stderr=b"", exit_code=1)
# --- Export & Construction ---
def test_daytona_mount_strategy_importable(monkeypatch: pytest.MonkeyPatch) -> None:
_load_daytona_module(monkeypatch)
package = importlib.import_module("agents.extensions.sandbox.daytona")
assert hasattr(package, "DaytonaCloudBucketMountStrategy")
assert package.DaytonaCloudBucketMountStrategy is DaytonaCloudBucketMountStrategy
def test_daytona_mount_strategy_type_and_default_pattern() -> None:
strategy = DaytonaCloudBucketMountStrategy()
assert strategy.type == "daytona_cloud_bucket"
assert isinstance(strategy.pattern, RcloneMountPattern)
assert strategy.pattern.mode == "fuse"
def test_daytona_mount_strategy_round_trips_through_manifest(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_load_daytona_module(monkeypatch)
manifest = Manifest.model_validate(
{
"root": "/workspace",
"entries": {
"bucket": {
"type": "s3_mount",
"bucket": "my-bucket",
"mount_strategy": {"type": "daytona_cloud_bucket"},
}
},
}
)
mount = manifest.entries["bucket"]
assert isinstance(mount, S3Mount)
assert isinstance(mount.mount_strategy, DaytonaCloudBucketMountStrategy)
# --- Session Guard ---
def test_daytona_session_guard_rejects_wrong_type() -> None:
class _WrongSession:
pass
with pytest.raises(MountConfigError, match="DaytonaSandboxSession"):
_assert_daytona_session(_WrongSession()) # type: ignore[arg-type]
def test_daytona_session_guard_accepts_correct_type() -> None:
session = _FakePreflightSession()
_assert_daytona_session(session) # should not raise
# --- _has_command ---
@pytest.mark.asyncio
async def test_has_command_found() -> None:
session = _FakePreflightSession([_ok()])
assert await _has_command(session, "rclone") is True
assert len(session.exec_calls) == 1
assert "command -v rclone" in session.exec_calls[0]
@pytest.mark.asyncio
async def test_has_command_not_found() -> None:
session = _FakePreflightSession([_fail()])
assert await _has_command(session, "rclone") is False
# --- _pkg_install ---
@pytest.mark.asyncio
async def test_pkg_install_via_apt() -> None:
session = _FakePreflightSession(
[
_ok(), # _has_command("apt-get") → found
_ok(), # install succeeds
]
)
await _pkg_install(session, "rclone", what="rclone")
assert any("apt-get" in c and "rclone" in c for c in session.exec_calls)
assert any(c.startswith("sudo -u root --") and "apt-get" in c for c in session.exec_calls)
@pytest.mark.asyncio
async def test_pkg_install_via_apk() -> None:
session = _FakePreflightSession(
[
_fail(), # _has_command("apt-get") → not found
_ok(), # _has_command("apk") → found
_ok(), # install succeeds
]
)
await _pkg_install(session, "fuse3", what="fusermount")
assert any("apk add" in c and "fuse3" in c for c in session.exec_calls)
assert any(c.startswith("sudo -u root --") and "apk add" in c for c in session.exec_calls)
@pytest.mark.asyncio
async def test_pkg_install_no_package_manager() -> None:
session = _FakePreflightSession(
[
_fail(), # _has_command("apt-get") → not found
_fail(), # _has_command("apk") → not found
]
)
with pytest.raises(MountConfigError, match="no supported package manager"):
await _pkg_install(session, "rclone", what="rclone")
@pytest.mark.asyncio
async def test_pkg_install_retries_then_fails() -> None:
session = _FakePreflightSession(
[
_ok(), # _has_command("apt-get") → found
_fail(), # install attempt 1
_fail(), # install attempt 2
_fail(), # install attempt 3
]
)
with pytest.raises(MountConfigError, match="after 3 attempts"):
await _pkg_install(session, "rclone", what="rclone")
# 1 check + 3 install attempts = 4 exec calls.
assert len(session.exec_calls) == 4
assert all(c.startswith("sudo -u root --") for c in session.exec_calls[1:])
# --- _ensure_fuse_support ---
@pytest.mark.asyncio
async def test_ensure_fuse_dev_fuse_missing() -> None:
session = _FakePreflightSession([_fail()])
with pytest.raises(MountConfigError, match="/dev/fuse not available"):
await _ensure_fuse_support(session)
@pytest.mark.asyncio
async def test_ensure_fuse_kernel_module_missing() -> None:
session = _FakePreflightSession(
[
_ok(), # /dev/fuse exists
_fail(), # fuse not in /proc/filesystems
]
)
with pytest.raises(MountConfigError, match="FUSE kernel module not loaded"):
await _ensure_fuse_support(session)
@pytest.mark.asyncio
async def test_ensure_fuse_fusermount_present() -> None:
session = _FakePreflightSession(
[
_ok(), # /dev/fuse
_ok(), # /proc/filesystems
_ok(), # _has_command("fusermount3") → found
]
)
await _ensure_fuse_support(session)
assert len(session.exec_calls) == 3
@pytest.mark.asyncio
async def test_ensure_fuse_installs_when_missing() -> None:
session = _FakePreflightSession(
[
_ok(), # /dev/fuse
_ok(), # /proc/filesystems
_fail(), # _has_command("fusermount3") → not found
_fail(), # _has_command("fusermount") → not found
_ok(), # _has_command("apt-get") → found (inside _pkg_install)
_ok(), # apt-get install fuse3 → success
_ok(), # re-check: _has_command("fusermount3") → found
]
)
await _ensure_fuse_support(session)
assert any("fuse3" in c for c in session.exec_calls)
assert len(session.exec_calls) == 7
# --- _ensure_rclone ---
@pytest.mark.asyncio
async def test_ensure_rclone_present() -> None:
session = _FakePreflightSession([_ok()])
await _ensure_rclone(session)
assert len(session.exec_calls) == 1
@pytest.mark.asyncio
async def test_ensure_rclone_installs_when_missing() -> None:
session = _FakePreflightSession(
[
_fail(), # _has_command("rclone") → not found
_ok(), # _has_command("apt-get") → found (inside _pkg_install)
_ok(), # apt-get install rclone → success
_ok(), # re-check: _has_command("rclone") → found
]
)
await _ensure_rclone(session)
assert any("rclone" in c for c in session.exec_calls)
assert len(session.exec_calls) == 4
# --- Strategy lifecycle ---
@pytest.mark.asyncio
async def test_activate_calls_preflights_and_delegates() -> None:
strategy = DaytonaCloudBucketMountStrategy()
mount = MagicMock()
session = _FakePreflightSession()
dest = Path("/workspace")
base_dir = Path("/workspace")
with (
patch.object(_daytona_mounts, "_ensure_fuse_support", new_callable=AsyncMock) as fuse_mock,
patch.object(_daytona_mounts, "_ensure_rclone", new_callable=AsyncMock) as rclone_mock,
patch.object(
InContainerMountStrategy, "activate", new_callable=AsyncMock, return_value=[]
) as delegate_mock,
):
await strategy.activate(mount, session, dest, base_dir)
fuse_mock.assert_awaited_once_with(session)
rclone_mock.assert_awaited_once_with(session)
delegate_mock.assert_awaited_once()
@pytest.mark.asyncio
async def test_deactivate_delegates_without_preflights() -> None:
strategy = DaytonaCloudBucketMountStrategy()
mount = MagicMock()
session = _FakePreflightSession()
dest = Path("/workspace")
base_dir = Path("/workspace")
with (
patch.object(_daytona_mounts, "_ensure_fuse_support", new_callable=AsyncMock) as fuse_mock,
patch.object(_daytona_mounts, "_ensure_rclone", new_callable=AsyncMock) as rclone_mock,
patch.object(
InContainerMountStrategy, "deactivate", new_callable=AsyncMock
) as delegate_mock,
):
await strategy.deactivate(mount, session, dest, base_dir)
fuse_mock.assert_not_awaited()
rclone_mock.assert_not_awaited()
delegate_mock.assert_awaited_once()
@pytest.mark.asyncio
async def test_teardown_delegates_without_preflights() -> None:
strategy = DaytonaCloudBucketMountStrategy()
mount = MagicMock()
session = _FakePreflightSession()
path = Path("/workspace/bucket")
with (
patch.object(_daytona_mounts, "_ensure_fuse_support", new_callable=AsyncMock) as fuse_mock,
patch.object(_daytona_mounts, "_ensure_rclone", new_callable=AsyncMock) as rclone_mock,
patch.object(
InContainerMountStrategy, "teardown_for_snapshot", new_callable=AsyncMock
) as delegate_mock,
):
await strategy.teardown_for_snapshot(mount, session, path)
fuse_mock.assert_not_awaited()
rclone_mock.assert_not_awaited()
delegate_mock.assert_awaited_once()
@pytest.mark.asyncio
async def test_restore_after_snapshot_reruns_preflights() -> None:
strategy = DaytonaCloudBucketMountStrategy()
mount = MagicMock()
session = _FakePreflightSession()
path = Path("/workspace/bucket")
with (
patch.object(_daytona_mounts, "_ensure_fuse_support", new_callable=AsyncMock) as fuse_mock,
patch.object(_daytona_mounts, "_ensure_rclone", new_callable=AsyncMock) as rclone_mock,
patch.object(
InContainerMountStrategy, "restore_after_snapshot", new_callable=AsyncMock
) as delegate_mock,
):
await strategy.restore_after_snapshot(mount, session, path)
fuse_mock.assert_awaited_once_with(session)
rclone_mock.assert_awaited_once_with(session)
delegate_mock.assert_awaited_once()
def test_build_docker_volume_driver_config_returns_none() -> None:
strategy = DaytonaCloudBucketMountStrategy()
mount = MagicMock()
assert strategy.build_docker_volume_driver_config(mount) is None