diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index c3315f8d0043..72125fd593f6 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -371,13 +371,32 @@ def print_systemd_linger_guidance() -> None: def get_launchd_plist_path() -> Path: return Path.home() / "Library" / "LaunchAgents" / "ai.hermes.gateway.plist" +def _find_venv_dir() -> Path | None: + """Return the active virtualenv directory under PROJECT_ROOT, or None. + + Checks .venv first (setup-hermes.sh default), then venv, then falls + back to sys.prefix if it lives under PROJECT_ROOT (pip install -e). + """ + for name in (".venv", "venv"): + candidate = PROJECT_ROOT / name + if (candidate / "bin" / "python").exists() or (candidate / "Scripts" / "python.exe").exists(): + return candidate + # pip-installed editable: sys.prefix may point to a venv outside PROJECT_ROOT + prefix = Path(sys.prefix) + if prefix != Path(sys.base_prefix): # we're inside a venv + return prefix + return None + + def get_python_path() -> str: - if is_windows(): - venv_python = PROJECT_ROOT / "venv" / "Scripts" / "python.exe" - else: - venv_python = PROJECT_ROOT / "venv" / "bin" / "python" - if venv_python.exists(): - return str(venv_python) + venv_dir = _find_venv_dir() + if venv_dir is not None: + if is_windows(): + venv_python = venv_dir / "Scripts" / "python.exe" + else: + venv_python = venv_dir / "bin" / "python" + if venv_python.exists(): + return str(venv_python) return sys.executable def get_hermes_cli_path() -> str: @@ -399,8 +418,9 @@ def get_hermes_cli_path() -> str: def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) -> str: python_path = get_python_path() working_dir = str(PROJECT_ROOT) - venv_dir = str(PROJECT_ROOT / "venv") - venv_bin = str(PROJECT_ROOT / "venv" / "bin") + _venv = _find_venv_dir() + venv_dir = str(_venv) if _venv else str(PROJECT_ROOT / "venv") + venv_bin = str(_venv / "bin") if _venv else str(PROJECT_ROOT / "venv" / "bin") node_bin = str(PROJECT_ROOT / "node_modules" / ".bin") path_entries = [venv_bin, node_bin] diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index 0bfe1a98aa03..8bfecbc6267a 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -354,3 +354,58 @@ def test_systemctl_cmd_skips_ensure_for_system_mode(self, monkeypatch): result = gateway_cli._systemctl_cmd(system=True) assert result == ["systemctl"] assert calls == [] + + +class TestFindVenvDir: + def test_finds_dot_venv(self, tmp_path, monkeypatch): + dot_venv = tmp_path / ".venv" / "bin" + dot_venv.mkdir(parents=True) + (dot_venv / "python").write_text("#!/bin/sh") + monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path) + result = gateway_cli._find_venv_dir() + assert result == tmp_path / ".venv" + + def test_finds_venv(self, tmp_path, monkeypatch): + venv_bin = tmp_path / "venv" / "bin" + venv_bin.mkdir(parents=True) + (venv_bin / "python").write_text("#!/bin/sh") + monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path) + result = gateway_cli._find_venv_dir() + assert result == tmp_path / "venv" + + def test_prefers_dot_venv_over_venv(self, tmp_path, monkeypatch): + for name in (".venv", "venv"): + bin_dir = tmp_path / name / "bin" + bin_dir.mkdir(parents=True) + (bin_dir / "python").write_text("#!/bin/sh") + monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path) + result = gateway_cli._find_venv_dir() + assert result == tmp_path / ".venv" + + def test_returns_none_when_no_venv(self, tmp_path, monkeypatch): + import sys + monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path) + monkeypatch.setattr(gateway_cli.sys, "prefix", sys.base_prefix) + monkeypatch.setattr(gateway_cli.sys, "base_prefix", sys.base_prefix) + result = gateway_cli._find_venv_dir() + assert result is None + + +class TestSystemdUnitVenvPath: + def test_unit_uses_dot_venv_when_present(self, tmp_path, monkeypatch): + dot_venv = tmp_path / ".venv" / "bin" + dot_venv.mkdir(parents=True) + (dot_venv / "python").write_text("#!/bin/sh") + monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path) + unit = gateway_cli.generate_systemd_unit(system=False) + assert ".venv" in unit + assert "VIRTUAL_ENV=" in unit + + def test_unit_uses_venv_when_only_venv_present(self, tmp_path, monkeypatch): + venv_bin = tmp_path / "venv" / "bin" + venv_bin.mkdir(parents=True) + (venv_bin / "python").write_text("#!/bin/sh") + monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path) + unit = gateway_cli.generate_systemd_unit(system=False) + assert "VIRTUAL_ENV=" in unit + assert ".venv" not in unit