Spaces:
Paused
Paused
| """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 | |