diff --git a/tests/tools/test_daytona_environment.py b/tests/tools/test_daytona_environment.py index 6d32f744..94a28dc7 100644 --- a/tests/tools/test_daytona_environment.py +++ b/tests/tools/test_daytona_environment.py @@ -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() diff --git a/tools/environments/daytona.py b/tools/environments/daytona.py index 5c2204e6..cc046bb4 100644 --- a/tools/environments/daytona.py +++ b/tools/environments/daytona.py @@ -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,