Files
hermes-agent/tests/tools/test_ensure_task_env.py
PRATHAMESH75 6e87d43a57 fix(tools): lazily bring up sandbox for vision_analyze reads
vision_analyze reads container-only images by exec-reading them inside the
sandbox, but unlike terminal_tool it never triggered environment creation. Under
a non-local backend (ssh, docker, ...), a session whose first action was
vision_analyze on a remote path failed with 'no active sandbox session' until an
unrelated terminal command happened to establish the connection.

Add terminal_tool.ensure_task_env(task_id), a public lazy get-or-create that
reuses the terminal tool's own creation machinery, and call it from
image_source._resolve_container_fallback before the in-sandbox read. Extract the
ssh/container config-dict builders so both paths derive settings identically
(no duplication). Best-effort and fail-closed: a failed bring-up leaves the
existing 'no active sandbox' error intact, never a host read.

Fixes #62825
2026-08-07 09:11:48 -07:00

59 lines
2.2 KiB
Python

"""Unit tests for terminal_tool.ensure_task_env — the lazy sandbox bring-up
used by vision_analyze so a session's first action can be an in-sandbox file
read without a prior terminal command (issue #62825)."""
from types import SimpleNamespace
from unittest.mock import patch
import tools.terminal_tool as tt
def _clear(*task_ids):
for task_id in task_ids:
tt._active_environments.pop(task_id, None)
tt._last_activity.pop(task_id, None)
def test_local_backend_is_noop(monkeypatch):
"""Local backend reads images host-side — no sandbox is created."""
monkeypatch.setenv("TERMINAL_ENV", "local")
with patch.object(tt, "_create_environment") as create:
assert tt.ensure_task_env("t-local") is None
create.assert_not_called()
def test_non_local_creates_and_reuses(monkeypatch):
"""A non-local backend with no active env creates one, caches it, and a
second call reuses the cache instead of spawning a duplicate."""
monkeypatch.setenv("TERMINAL_ENV", "ssh")
task_id = "t-ssh"
eff = tt._resolve_container_task_id(task_id)
_clear(eff, task_id)
fake = SimpleNamespace(execute=lambda *a, **k: {"returncode": 0, "output": ""})
try:
with patch.object(tt, "_create_environment", return_value=fake) as create:
assert tt.ensure_task_env(task_id) is fake
create.assert_called_once()
assert tt.get_active_env(task_id) is fake
# Already active -> no second creation.
assert tt.ensure_task_env(task_id) is fake
create.assert_called_once()
finally:
_clear(eff, task_id)
def test_creation_failure_returns_none_and_caches_nothing(monkeypatch):
"""A failed bring-up is best-effort: return None and leave no env cached so
the caller keeps its fail-closed path."""
monkeypatch.setenv("TERMINAL_ENV", "ssh")
task_id = "t-ssh-fail"
eff = tt._resolve_container_task_id(task_id)
_clear(eff, task_id)
try:
with patch.object(tt, "_create_environment", side_effect=RuntimeError("boom")):
assert tt.ensure_task_env(task_id) is None
assert tt.get_active_env(task_id) is None
finally:
_clear(eff, task_id)