This commit is contained in:
Andy Tien 2026-04-24 16:33:16 -05:00 committed by GitHub
commit f4d3067e36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 163 additions and 0 deletions

View file

@ -1437,6 +1437,17 @@ def get_python_path() -> str:
venv_python = venv / "bin" / "python"
if venv_python.exists():
return str(venv_python)
# Fallback: use PROJECT_ROOT/venv/bin/python (same logic as generate_systemd_unit)
# This is more reliable than sys.executable when running under uv.
fallback_venv = PROJECT_ROOT / "venv"
if is_windows():
fallback_python = fallback_venv / "Scripts" / "python.exe"
else:
fallback_python = fallback_venv / "bin" / "python"
if fallback_python.exists():
return str(fallback_python)
return sys.executable

View file

@ -0,0 +1,152 @@
"""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"])