mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
## What broke After `hermes gateway install --system`, the systemd service ExecStart uses the uv-managed Python path instead of the venv Python path. This causes immediate crash with `ModuleNotFoundError: No module named 'yaml'` because the uv Python has no packages installed. ExecStart=/root/.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/bin/python3.11 Should be: ExecStart=/root/.hermes/hermes-agent/venv/bin/python3.11 ## Root cause `get_python_path()` in hermes_cli/gateway.py fell back to `sys.executable` when `_detect_venv_dir()` returned None. In uv-managed environments, `sys.executable` returns the uv Python path which has no packages. Meanwhile, `generate_systemd_unit()` already uses `PROJECT_ROOT/venv` as fallback for `venv_dir` (line 819), creating an inconsistency: - `venv_dir` = PROJECT_ROOT/venv (correct) - `python_path` = sys.executable (wrong - uv path) ## Why this fix is minimal Added 11 lines to `get_python_path()` to add a middle fallback tier: 1. detected_venv/bin/python (existing - highest priority) 2. PROJECT_ROOT/venv/bin/python (NEW - matches generate_systemd_unit) 3. sys.executable (existing - final fallback) This makes `get_python_path()` consistent with `venv_dir` calculation in `generate_systemd_unit()`. No behavior change when venv is detected. ## What I tested Added test suite tests/hermes_cli/test_gateway_python_path.py with 8 tests: - test_fallback_to_project_root_venv_when_detect_fails - test_detect_venv_dir_returns_valid_venv - test_fallback_chain_order - test_consistency_with_venv_dir_in_systemd_unit - test_windows_fallback_uses_scripts_python_exe - test_execstart_not_uv_python_path - test_system_unit_execstart_uses_venv_python - test_user_unit_execstart_uses_venv_python All verify ExecStart uses venv/bin/python, not uv Python path. ## What I intentionally did not change - No changes to _detect_venv_dir() logic - No changes to generate_systemd_unit() venv_dir calculation - No opportunistic refactoring - No changes to launchd plist generation (similar pattern) Fixes #9201
152 lines
No EOL
6.7 KiB
Python
152 lines
No EOL
6.7 KiB
Python
"""Test for get_python_path() fallback to PROJECT_ROOT/venv/bin/python.
|
|
|
|
Issue: #9201 - systemd service ExecStart uses uv Python instead of venv Python
|
|
when uv manages the Python environment.
|
|
|
|
Root cause: get_python_path() fell back to sys.executable when _detect_venv_dir()
|
|
returned None, but this returns uv's Python path which has no packages installed.
|
|
"""
|
|
|
|
import sys
|
|
import pytest
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
class TestGetPythonPathFallback:
|
|
"""Test that get_python_path() correctly falls back to PROJECT_ROOT/venv."""
|
|
|
|
def test_fallback_to_project_root_venv_when_detect_fails(self):
|
|
"""When _detect_venv_dir() returns None, should use PROJECT_ROOT/venv."""
|
|
from hermes_cli.gateway import get_python_path, PROJECT_ROOT
|
|
|
|
# Mock _detect_venv_dir to return None (simulate uv environment)
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=None):
|
|
# Mock the venv python to exist
|
|
with patch.object(Path, "exists", return_value=True):
|
|
result = get_python_path()
|
|
|
|
# Should be PROJECT_ROOT/venv/bin/python, not sys.executable
|
|
expected = str(PROJECT_ROOT / "venv" / "bin" / "python")
|
|
assert result == expected
|
|
|
|
def test_detect_venv_dir_returns_valid_venv(self):
|
|
"""When _detect_venv_dir() returns a valid venv, should use it."""
|
|
from hermes_cli.gateway import get_python_path
|
|
|
|
# Mock _detect_venv_dir to return a venv
|
|
mock_venv = MagicMock(spec=Path)
|
|
mock_venv.is_dir.return_value = True
|
|
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=mock_venv):
|
|
# Mock the venv python to exist
|
|
mock_python = MagicMock(spec=Path)
|
|
mock_python.exists.return_value = True
|
|
|
|
with patch.object(Path, "__truediv__", return_value=mock_python):
|
|
result = get_python_path()
|
|
|
|
# Should return the venv python path
|
|
assert result is not None
|
|
|
|
def test_fallback_chain_order(self):
|
|
"""Fallback order should be: detected_venv -> PROJECT_ROOT/venv -> sys.executable."""
|
|
from hermes_cli.gateway import get_python_path, PROJECT_ROOT
|
|
|
|
# Test 1: detected_venv has priority
|
|
mock_venv = MagicMock(spec=Path)
|
|
mock_venv_python = MagicMock(spec=Path)
|
|
mock_venv_python.exists.return_value = True
|
|
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=mock_venv):
|
|
with patch.object(Path, "__truediv__", return_value=mock_venv_python):
|
|
result = get_python_path()
|
|
# Should use detected_venv, not PROJECT_ROOT fallback
|
|
|
|
# Test 2: PROJECT_ROOT/venv fallback when detect fails but venv exists
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=None):
|
|
with patch.object(Path, "exists", return_value=True):
|
|
result = get_python_path()
|
|
expected = str(PROJECT_ROOT / "venv" / "bin" / "python")
|
|
assert result == expected
|
|
|
|
# Test 3: sys.executable when all fallbacks fail
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=None):
|
|
with patch.object(Path, "exists", return_value=False):
|
|
result = get_python_path()
|
|
assert result == sys.executable
|
|
|
|
def test_consistency_with_venv_dir_in_systemd_unit(self):
|
|
"""python_path should be consistent with venv_dir in generate_systemd_unit."""
|
|
from hermes_cli.gateway import (
|
|
get_python_path,
|
|
generate_systemd_unit,
|
|
PROJECT_ROOT,
|
|
_detect_venv_dir,
|
|
)
|
|
|
|
# Simulate scenario where _detect_venv_dir returns None
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=None):
|
|
with patch.object(Path, "exists", return_value=True):
|
|
python_path = get_python_path()
|
|
unit = generate_systemd_unit(system=False)
|
|
|
|
# python_path should match venv/bin/python in ExecStart
|
|
assert "venv/bin/python" in python_path
|
|
assert python_path in unit
|
|
|
|
def test_windows_fallback_uses_scripts_python_exe(self):
|
|
"""On Windows, fallback should use venv/Scripts/python.exe."""
|
|
from hermes_cli.gateway import get_python_path, PROJECT_ROOT
|
|
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=None):
|
|
with patch("hermes_cli.gateway.is_windows", return_value=True):
|
|
with patch.object(Path, "exists", return_value=True):
|
|
result = get_python_path()
|
|
expected = str(PROJECT_ROOT / "venv" / "Scripts" / "python.exe")
|
|
assert result == expected
|
|
|
|
def test_execstart_not_uv_python_path(self):
|
|
"""ExecStart should NOT contain uv Python path patterns."""
|
|
from hermes_cli.gateway import generate_systemd_unit
|
|
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=None):
|
|
with patch.object(Path, "exists", return_value=True):
|
|
unit = generate_systemd_unit(system=False)
|
|
|
|
# Should NOT have uv-specific paths
|
|
assert ".local/share/uv/python" not in unit
|
|
assert "cpython-" not in unit
|
|
# Should have venv path
|
|
assert "venv/bin/python" in unit
|
|
|
|
|
|
class TestSystemdUnitGeneration:
|
|
"""Test systemd unit file generation with correct Python path."""
|
|
|
|
def test_system_unit_execstart_uses_venv_python(self):
|
|
"""System unit ExecStart should use venv/bin/python."""
|
|
from hermes_cli.gateway import generate_systemd_unit, PROJECT_ROOT
|
|
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=None):
|
|
with patch.object(Path, "exists", return_value=True):
|
|
unit = generate_systemd_unit(system=True, run_as_user="root")
|
|
|
|
# ExecStart should contain venv/bin/python
|
|
assert "venv/bin/python" in unit
|
|
assert "ExecStart=" in unit
|
|
|
|
def test_user_unit_execstart_uses_venv_python(self):
|
|
"""User unit ExecStart should use venv/bin/python."""
|
|
from hermes_cli.gateway import generate_systemd_unit, PROJECT_ROOT
|
|
|
|
with patch("hermes_cli.gateway._detect_venv_dir", return_value=None):
|
|
with patch.object(Path, "exists", return_value=True):
|
|
unit = generate_systemd_unit(system=False)
|
|
|
|
# ExecStart should contain venv/bin/python
|
|
assert "venv/bin/python" in unit
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"]) |