"""Tests for per-profile subprocess HOME isolation (#4426). Verifies that subprocesses (terminal, execute_code, background processes) receive a per-profile HOME directory while the Python process's own HOME and Path.home() remain unchanged. See: https://github.com/NousResearch/hermes-agent/issues/4426 """ import os from pathlib import Path from unittest.mock import patch import pytest # --------------------------------------------------------------------------- # get_subprocess_home() # --------------------------------------------------------------------------- class TestGetSubprocessHome: """Unit tests for hermes_constants.get_subprocess_home().""" def test_returns_none_when_hermes_home_unset(self, monkeypatch): monkeypatch.delenv("HERMES_HOME", raising=False) from hermes_constants import get_subprocess_home assert get_subprocess_home() is None def test_returns_none_when_home_dir_missing(self, tmp_path, monkeypatch): hermes_home = tmp_path / ".hermes" hermes_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) # No home/ subdirectory created from hermes_constants import get_subprocess_home assert get_subprocess_home() is None def test_returns_path_when_home_dir_exists(self, tmp_path, monkeypatch): hermes_home = tmp_path / ".hermes" hermes_home.mkdir() profile_home = hermes_home / "home" profile_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) from hermes_constants import get_subprocess_home assert get_subprocess_home() == str(profile_home) def test_returns_profile_specific_path(self, tmp_path, monkeypatch): """Named profiles get their own isolated HOME.""" profile_dir = tmp_path / ".hermes" / "profiles" / "coder" profile_dir.mkdir(parents=True) profile_home = profile_dir / "home" profile_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(profile_dir)) from hermes_constants import get_subprocess_home assert get_subprocess_home() == str(profile_home) def test_two_profiles_get_different_homes(self, tmp_path, monkeypatch): base = tmp_path / ".hermes" / "profiles" for name in ("alpha", "beta"): p = base / name p.mkdir(parents=True) (p / "home").mkdir() from hermes_constants import get_subprocess_home monkeypatch.setenv("HERMES_HOME", str(base / "alpha")) home_a = get_subprocess_home() monkeypatch.setenv("HERMES_HOME", str(base / "beta")) home_b = get_subprocess_home() assert home_a != home_b assert home_a.endswith("alpha/home") assert home_b.endswith("beta/home") # --------------------------------------------------------------------------- # _make_run_env() injection # --------------------------------------------------------------------------- class TestMakeRunEnvHomeInjection: """Verify _make_run_env() injects HOME into subprocess envs.""" def test_injects_home_when_profile_home_exists(self, tmp_path, monkeypatch): hermes_home = tmp_path / "hermes" hermes_home.mkdir() (hermes_home / "home").mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.setenv("HOME", "/root") monkeypatch.setenv("PATH", "/usr/bin:/bin") from tools.environments.local import _make_run_env result = _make_run_env({}) assert result["HOME"] == str(hermes_home / "home") def test_no_injection_when_home_dir_missing(self, tmp_path, monkeypatch): hermes_home = tmp_path / "hermes" hermes_home.mkdir() # No home/ subdirectory monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.setenv("HOME", "/root") monkeypatch.setenv("PATH", "/usr/bin:/bin") from tools.environments.local import _make_run_env result = _make_run_env({}) assert result["HOME"] == "/root" def test_no_injection_when_hermes_home_unset(self, monkeypatch): monkeypatch.delenv("HERMES_HOME", raising=False) monkeypatch.setenv("HOME", "/home/user") monkeypatch.setenv("PATH", "/usr/bin:/bin") from tools.environments.local import _make_run_env result = _make_run_env({}) assert result["HOME"] == "/home/user" # --------------------------------------------------------------------------- # _sanitize_subprocess_env() injection # --------------------------------------------------------------------------- class TestSanitizeSubprocessEnvHomeInjection: """Verify _sanitize_subprocess_env() injects HOME for background procs.""" def test_injects_home_when_profile_home_exists(self, tmp_path, monkeypatch): hermes_home = tmp_path / "hermes" hermes_home.mkdir() (hermes_home / "home").mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) base_env = {"HOME": "/root", "PATH": "/usr/bin", "USER": "root"} from tools.environments.local import _sanitize_subprocess_env result = _sanitize_subprocess_env(base_env) assert result["HOME"] == str(hermes_home / "home") def test_no_injection_when_home_dir_missing(self, tmp_path, monkeypatch): hermes_home = tmp_path / "hermes" hermes_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) base_env = {"HOME": "/root", "PATH": "/usr/bin"} from tools.environments.local import _sanitize_subprocess_env result = _sanitize_subprocess_env(base_env) assert result["HOME"] == "/root" # --------------------------------------------------------------------------- # Profile bootstrap # --------------------------------------------------------------------------- class TestProfileBootstrap: """Verify new profiles get a home/ subdirectory.""" def test_profile_dirs_includes_home(self): from hermes_cli.profiles import _PROFILE_DIRS assert "home" in _PROFILE_DIRS def test_create_profile_bootstraps_home_dir(self, tmp_path, monkeypatch): """create_profile() should create home/ inside the profile dir.""" home = tmp_path / ".hermes" home.mkdir() monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.setenv("HERMES_HOME", str(home)) from hermes_cli.profiles import create_profile profile_dir = create_profile("testbot", no_alias=True) assert (profile_dir / "home").is_dir() # --------------------------------------------------------------------------- # Python process HOME unchanged # --------------------------------------------------------------------------- class TestPythonProcessUnchanged: """Confirm the Python process's own HOME is never modified.""" def test_path_home_unchanged_after_subprocess_home_resolved( self, tmp_path, monkeypatch ): hermes_home = tmp_path / "hermes" hermes_home.mkdir() (hermes_home / "home").mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) original_home = os.environ.get("HOME") original_path_home = str(Path.home()) from hermes_constants import get_subprocess_home sub_home = get_subprocess_home() # Subprocess home is set but Python HOME stays the same assert sub_home is not None assert os.environ.get("HOME") == original_home assert str(Path.home()) == original_path_home