Something went wrong. Try again.
The agent that grows with you
Something went wrong. Try again.
11 kB · 303 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304"""Tests for container-aware CLI routing (NixOS container mode).
When container.enable = true in the NixOS module, the activation scriptwrites a .container-mode metadata file. The host CLI detects this andexecs into the container instead of running locally."""import osimport subprocessfrom pathlib import Pathfrom unittest.mock import MagicMock, patch
import pytest
from hermes_cli.config import ( get_container_exec_info,)
# =============================================================================# get_container_exec_info# =============================================================================
@pytest.fixturedef container_env(tmp_path, monkeypatch): """Set up a fake HERMES_HOME with .container-mode file.""" hermes_home = tmp_path / ".hermes" hermes_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.delenv("HERMES_DEV", raising=False)
container_mode = hermes_home / ".container-mode" container_mode.write_text( "# Written by NixOS activation script. Do not edit manually.\n" "backend=podman\n" "container_name=hermes-agent\n" "exec_user=hermes\n" "hermes_bin=/data/current-package/bin/hermes\n" ) return hermes_home
def test_get_container_exec_info_returns_metadata(container_env): """Reads .container-mode and returns all fields including exec_user.""" with patch("hermes_constants.is_container", return_value=False): info = get_container_exec_info()
assert info is not None assert info["backend"] == "podman" assert info["container_name"] == "hermes-agent" assert info["exec_user"] == "hermes" assert info["hermes_bin"] == "/data/current-package/bin/hermes"
def test_get_container_exec_info_none_inside_container(container_env): """Returns None when we're already inside a container.""" with patch("hermes_constants.is_container", return_value=True): info = get_container_exec_info()
assert info is None
def test_get_container_exec_info_none_without_file(tmp_path, monkeypatch): """Returns None when .container-mode doesn't exist (native mode).""" hermes_home = tmp_path / ".hermes" hermes_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.delenv("HERMES_DEV", raising=False)
with patch("hermes_constants.is_container", return_value=False): info = get_container_exec_info()
assert info is None
def test_get_container_exec_info_skipped_when_hermes_dev(container_env, monkeypatch): """Returns None when HERMES_DEV=1 is set (dev mode bypass).""" monkeypatch.setenv("HERMES_DEV", "1")
with patch("hermes_constants.is_container", return_value=False): info = get_container_exec_info()
assert info is None
def test_get_container_exec_info_not_skipped_when_hermes_dev_zero(container_env, monkeypatch): """HERMES_DEV=0 does NOT trigger bypass — only '1' does.""" monkeypatch.setenv("HERMES_DEV", "0")
with patch("hermes_constants.is_container", return_value=False): info = get_container_exec_info()
assert info is not None
def test_get_container_exec_info_defaults(): """Falls back to defaults for missing keys.""" import tempfile
with tempfile.TemporaryDirectory() as tmpdir: hermes_home = Path(tmpdir) / ".hermes" hermes_home.mkdir() (hermes_home / ".container-mode").write_text( "# minimal file with no keys\n" )
with patch("hermes_constants.is_container", return_value=False), \ patch("hermes_cli.config.get_hermes_home", return_value=hermes_home), \ patch.dict(os.environ, {}, clear=False): os.environ.pop("HERMES_DEV", None) info = get_container_exec_info()
assert info is not None assert info["backend"] == "docker" assert info["container_name"] == "hermes-agent" assert info["exec_user"] == "hermes" assert info["hermes_bin"] == "/data/current-package/bin/hermes"
def test_get_container_exec_info_docker_backend(container_env): """Correctly reads docker backend with custom exec_user.""" (container_env / ".container-mode").write_text( "backend=docker\n" "container_name=hermes-custom\n" "exec_user=myuser\n" "hermes_bin=/opt/hermes/bin/hermes\n" )
with patch("hermes_constants.is_container", return_value=False): info = get_container_exec_info()
assert info["backend"] == "docker" assert info["container_name"] == "hermes-custom" assert info["exec_user"] == "myuser" assert info["hermes_bin"] == "/opt/hermes/bin/hermes"
def test_get_container_exec_info_crashes_on_permission_error(container_env): """PermissionError propagates instead of being silently swallowed.""" with patch("hermes_constants.is_container", return_value=False), \ patch("builtins.open", side_effect=PermissionError("permission denied")): with pytest.raises(PermissionError): get_container_exec_info()
# =============================================================================# _exec_in_container# =============================================================================
@pytest.fixturedef docker_container_info(): return { "backend": "docker", "container_name": "hermes-agent", "exec_user": "hermes", "hermes_bin": "/data/current-package/bin/hermes", }
@pytest.fixturedef podman_container_info(): return { "backend": "podman", "container_name": "hermes-agent", "exec_user": "hermes", "hermes_bin": "/data/current-package/bin/hermes", }
def test_exec_in_container_calls_execvp(docker_container_info): """Verifies os.execvp is called with correct args: runtime, tty flags, user, env vars, container name, binary, and CLI args.""" from hermes_cli.main import _exec_in_container
with patch("shutil.which", return_value="/usr/bin/docker"), \ patch("subprocess.run") as mock_run, \ patch("sys.stdin") as mock_stdin, \ patch("os.execvp") as mock_execvp, \ patch.dict(os.environ, {"TERM": "xterm-256color", "LANG": "en_US.UTF-8"}, clear=False): mock_stdin.isatty.return_value = True mock_run.return_value = MagicMock(returncode=0)
_exec_in_container(docker_container_info, ["chat", "-m", "opus"])
mock_execvp.assert_called_once() cmd = mock_execvp.call_args[0][1] assert cmd[0] == "/usr/bin/docker" assert cmd[1] == "exec" assert "-it" in cmd idx_u = cmd.index("-u") assert cmd[idx_u + 1] == "hermes" e_indices = [i for i, v in enumerate(cmd) if v == "-e"] e_values = [cmd[i + 1] for i in e_indices] assert "TERM=xterm-256color" in e_values assert "LANG=en_US.UTF-8" in e_values assert "hermes-agent" in cmd assert "/data/current-package/bin/hermes" in cmd assert "chat" in cmd
def test_exec_in_container_non_tty_uses_i_only(docker_container_info): """Non-TTY mode uses -i instead of -it.""" from hermes_cli.main import _exec_in_container
with patch("shutil.which", return_value="/usr/bin/docker"), \ patch("subprocess.run") as mock_run, \ patch("sys.stdin") as mock_stdin, \ patch("os.execvp") as mock_execvp: mock_stdin.isatty.return_value = False mock_run.return_value = MagicMock(returncode=0)
_exec_in_container(docker_container_info, ["sessions", "list"])
cmd = mock_execvp.call_args[0][1] assert "-i" in cmd assert "-it" not in cmd
def test_exec_in_container_no_runtime_hard_fails(podman_container_info): """Hard fails when runtime not found (no fallback).""" from hermes_cli.main import _exec_in_container
with patch("shutil.which", return_value=None), \ patch("subprocess.run") as mock_run, \ patch("os.execvp") as mock_execvp, \ pytest.raises(SystemExit) as exc_info: _exec_in_container(podman_container_info, ["chat"])
mock_run.assert_not_called() mock_execvp.assert_not_called() assert exc_info.value.code != 0
def test_exec_in_container_sudo_probe_sets_prefix(podman_container_info): """When first probe fails and sudo probe succeeds, execvp is called with sudo -n prefix.""" from hermes_cli.main import _exec_in_container
def which_side_effect(name): if name == "podman": return "/usr/bin/podman" if name == "sudo": return "/usr/bin/sudo" return None
with patch("shutil.which", side_effect=which_side_effect), \ patch("subprocess.run") as mock_run, \ patch("sys.stdin") as mock_stdin, \ patch("os.execvp") as mock_execvp: mock_stdin.isatty.return_value = True mock_run.side_effect = [ MagicMock(returncode=1), # direct probe fails MagicMock(returncode=0), # sudo probe succeeds ]
_exec_in_container(podman_container_info, ["chat"])
mock_execvp.assert_called_once() cmd = mock_execvp.call_args[0][1] assert cmd[0] == "/usr/bin/sudo" assert cmd[1] == "-n" assert cmd[2] == "/usr/bin/podman" assert cmd[3] == "exec"
def test_exec_in_container_probe_timeout_prints_message(docker_container_info): """TimeoutExpired from probe produces a human-readable error, not a raw traceback.""" from hermes_cli.main import _exec_in_container
with patch("shutil.which", return_value="/usr/bin/docker"), \ patch("subprocess.run", side_effect=subprocess.TimeoutExpired( cmd=["docker", "inspect"], timeout=15)), \ patch("os.execvp") as mock_execvp, \ pytest.raises(SystemExit) as exc_info: _exec_in_container(docker_container_info, ["chat"])
mock_execvp.assert_not_called() assert exc_info.value.code == 1
def test_exec_in_container_container_not_running_no_sudo(docker_container_info): """When runtime exists but container not found and no sudo available, prints helpful error about root containers.""" from hermes_cli.main import _exec_in_container
def which_side_effect(name): if name == "docker": return "/usr/bin/docker" return None
with patch("shutil.which", side_effect=which_side_effect), \ patch("subprocess.run") as mock_run, \ patch("os.execvp") as mock_execvp, \ pytest.raises(SystemExit) as exc_info: mock_run.return_value = MagicMock(returncode=1)
_exec_in_container(docker_container_info, ["chat"])
mock_execvp.assert_not_called() assert exc_info.value.code == 1