Files
hermes-agent/tests/agent/lsp/test_python_discovery.py
ethernet b64fe7b366 fix(lsp): select the analyzed project's Python first
Pyright selected the Hermes interpreter ahead of an explicit or local
project environment. Keep PM as the fallback rather than hiding the
project's dependencies.

The regression drives the real server registry and spawn options with
disposable venvs, then executes each selected interpreter. Correct the
neighboring Windows pip fixture to patch the installer seam it uses.
No language-server subprocess or user project was changed.
2026-09-09 20:48:13 -04:00

112 lines
4.2 KiB
Python

"""Pyright uses the project environment before the Hermes runtime fallback."""
import json
import os
import subprocess
import sys
from pathlib import Path
from agent.lsp.servers import _detect_python, _pm_store_python
def _seed_pm_python(tmp_path, monkeypatch, entry="python-3.11.13"):
"""Stage a pm bundled-install layout: HERMES_RUNTIME_DIR -> store with a
manifest sibling (bundled), a python entry, and facts recording it."""
payload = tmp_path / "payload"
store = payload / "tools"
python_entry = store / entry
python_entry.mkdir(parents=True)
exe = python_entry / ("python.exe" if sys.platform == "win32" else "bin/python3")
exe.parent.mkdir(parents=True, exist_ok=True)
exe.write_text("", encoding="utf-8")
(payload / "manifest.json").write_text("{}", encoding="utf-8")
(store / "facts.json").write_text(
json.dumps(
{
"schema": 1,
"packages": {"python": {"entry": entry, "version": "3.11.13"}},
}
),
encoding="utf-8",
)
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(store))
return exe
def test_pm_store_python_resolved_without_virtual_env(tmp_path, monkeypatch):
"""No VIRTUAL_ENV anywhere — the pm store interpreter is the answer."""
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
exe = _seed_pm_python(tmp_path, monkeypatch)
assert _pm_store_python() == str(exe)
assert _detect_python(str(tmp_path / "some-workspace")) == str(exe)
def test_no_pm_python_falls_back_to_project_layouts(tmp_path, monkeypatch):
"""Without a pm python fact, project .venv candidates still resolve."""
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "no-such-store"))
dot_venv_bin = tmp_path / "proj" / ".venv" / "bin"
dot_venv_bin.mkdir(parents=True)
python = dot_venv_bin / "python"
python.write_text("", encoding="utf-8")
assert Path(_detect_python(str(tmp_path / "proj"))) == python
def test_missing_store_entry_is_not_an_answer(tmp_path, monkeypatch):
"""A python fact whose store entry is gone resolves to None (pm only
vouches for what is on disk)."""
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
payload = tmp_path / "payload"
store = payload / "tools"
store.mkdir(parents=True)
(payload / "manifest.json").write_text("{}", encoding="utf-8")
(store / "facts.json").write_text(
json.dumps(
{
"schema": 1,
"packages": {"python": {"entry": "python-3.11.13", "version": "3.11.13"}},
}
),
encoding="utf-8",
)
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(store))
assert _pm_store_python() is None
def test_pyright_uses_the_project_interpreter_before_hermes(tmp_path, monkeypatch):
from agent.lsp import servers
project = tmp_path / "project"
project.mkdir()
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
monkeypatch.setattr(servers, "_pm_store_python", lambda: sys.executable)
context = servers.ServerContext(
workspace_root=str(project), install_strategy="off",
binary_overrides={"pyright": [sys.executable]},
)
server = servers.find_server_for_file(str(project / "app.py"))
assert server is not None
spec = server.build_spawn(str(project), context)
assert spec.initialization_options["python"]["pythonPath"] == sys.executable
for environment in (project / ".venv", tmp_path / "explicit-environment"):
subprocess.run(
[sys.executable, "-m", "venv", "--without-pip", str(environment)],
check=True, capture_output=True, text=True, timeout=30,
)
python = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
if environment.name == "explicit-environment":
monkeypatch.setenv("VIRTUAL_ENV", str(environment))
spec = server.build_spawn(str(project), context)
selected = spec.initialization_options["python"]["pythonPath"]
assert Path(selected) == python
child = subprocess.run(
[selected, "-I", "-c", "import sys; print(sys.prefix)"],
check=True, capture_output=True, text=True, timeout=10,
)
assert Path(child.stdout.strip()) == environment