fix(daytona): migrate sandbox lookup from find_one to get/list

find_one is being deprecated. Primary lookup now uses get() with a
deterministic sandbox name (hermes-{task_id}). A legacy fallback via
list(labels=...) ensures sandboxes created before this migration are
still resumable.
This commit is contained in:
rovle
2026-03-19 11:40:11 +01:00
parent 35558dadf4
commit 18862145e4
2 changed files with 59 additions and 13 deletions

View File

@@ -64,7 +64,8 @@ def make_env(daytona_sdk, monkeypatch):
def _factory(
sandbox=None,
find_one_side_effect=None,
get_side_effect=None,
list_return=None,
home_dir="/root",
persistent=True,
**kwargs,
@@ -76,11 +77,17 @@ def make_env(daytona_sdk, monkeypatch):
mock_client = MagicMock()
mock_client.create.return_value = sandbox
if find_one_side_effect is not None:
mock_client.find_one.side_effect = find_one_side_effect
if get_side_effect is not None:
mock_client.get.side_effect = get_side_effect
else:
# Default: no existing sandbox found
mock_client.find_one.side_effect = daytona_sdk.DaytonaError("not found")
# Default: no existing sandbox found via get()
mock_client.get.side_effect = daytona_sdk.DaytonaError("not found")
# Default: no legacy sandbox found via list()
if list_return is not None:
mock_client.list.return_value = list_return
else:
mock_client.list.return_value = SimpleNamespace(items=[])
daytona_sdk.Daytona = MagicMock(return_value=mock_client)
@@ -131,24 +138,46 @@ class TestCwdResolution:
# ---------------------------------------------------------------------------
class TestPersistence:
def test_persistent_resumes_existing_sandbox(self, make_env):
def test_persistent_resumes_via_get(self, make_env):
existing = _make_sandbox(sandbox_id="sb-existing")
existing.process.exec.return_value = _make_exec_response(result="/root")
env = make_env(find_one_side_effect=lambda **kw: existing, persistent=True)
env = make_env(get_side_effect=lambda name: existing, persistent=True,
task_id="mytask")
existing.start.assert_called_once()
# Should NOT have called create since find_one succeeded
env._mock_client.get.assert_called_once_with("hermes-mytask")
env._mock_client.create.assert_not_called()
def test_persistent_resumes_legacy_via_list(self, make_env, daytona_sdk):
legacy = _make_sandbox(sandbox_id="sb-legacy")
legacy.process.exec.return_value = _make_exec_response(result="/root")
env = make_env(
get_side_effect=daytona_sdk.DaytonaError("not found"),
list_return=SimpleNamespace(items=[legacy]),
persistent=True,
task_id="mytask",
)
legacy.start.assert_called_once()
env._mock_client.list.assert_called_once_with(
labels={"hermes_task_id": "mytask"}, page=1, limit=1)
env._mock_client.create.assert_not_called()
def test_persistent_creates_new_when_none_found(self, make_env, daytona_sdk):
env = make_env(
find_one_side_effect=daytona_sdk.DaytonaError("not found"),
get_side_effect=daytona_sdk.DaytonaError("not found"),
persistent=True,
task_id="mytask",
)
env._mock_client.create.assert_called_once()
# Verify the name and labels were passed to CreateSandboxFromImageParams
# by checking get() was called with the right sandbox name
env._mock_client.get.assert_called_with("hermes-mytask")
env._mock_client.list.assert_called_with(
labels={"hermes_task_id": "mytask"}, page=1, limit=1)
def test_non_persistent_skips_find_one(self, make_env):
def test_non_persistent_skips_lookup(self, make_env):
env = make_env(persistent=False)
env._mock_client.find_one.assert_not_called()
env._mock_client.get.assert_not_called()
env._mock_client.list.assert_not_called()
env._mock_client.create.assert_called_once()

View File

@@ -68,11 +68,13 @@ class DaytonaEnvironment(BaseEnvironment):
resources = Resources(cpu=cpu, memory=memory_gib, disk=disk_gib)
labels = {"hermes_task_id": task_id}
sandbox_name = f"hermes-{task_id}"
# Try to resume an existing stopped sandbox for this task
# Try to resume an existing sandbox for this task
if self._persistent:
# 1. Try name-based lookup (new path)
try:
self._sandbox = self._daytona.find_one(labels=labels)
self._sandbox = self._daytona.get(sandbox_name)
self._sandbox.start()
logger.info("Daytona: resumed sandbox %s for task %s",
self._sandbox.id, task_id)
@@ -83,11 +85,26 @@ class DaytonaEnvironment(BaseEnvironment):
task_id, e)
self._sandbox = None
# 2. Legacy fallback: find sandbox created before the naming migration
if self._sandbox is None:
try:
page = self._daytona.list(labels=labels, page=1, limit=1)
if page.items:
self._sandbox = page.items[0]
self._sandbox.start()
logger.info("Daytona: resumed legacy sandbox %s for task %s",
self._sandbox.id, task_id)
except Exception as e:
logger.debug("Daytona: no legacy sandbox found for task %s: %s",
task_id, e)
self._sandbox = None
# Create a fresh sandbox if we don't have one
if self._sandbox is None:
self._sandbox = self._daytona.create(
CreateSandboxFromImageParams(
image=image,
name=sandbox_name,
labels=labels,
auto_stop_interval=0,
resources=resources,