Files
hermes-agent/tests/hermes_cli/test_doctor_command_install.py
ethernet b728fdd627 refactor(pm): own the lock/atomic-write primitives in pm.filesystem
pm imported runtime_state's private helpers (_lock, _atomic_bytes, _bytes,
_digest) at a dozen sites while runtime_state imports pm.environments at
module top. The primitives are pm's: move them into the stdlib-only
pm.filesystem as lock_fd, durable_write_bytes, read_bytes_or_none and
file_digest, and repoint every pm caller.

runtime_state keeps the private names only as import aliases: it still
calls them through its own globals, and pre-PM updaters load them by these
names mid-swap (tests/compat/old_updater_surface.json).

Boot-subset test fixtures now copy pm/filesystem.py, since runtime_state
imports it at process boot; worker-injection tests patch the name
pm.publication now reads.
2026-09-24 14:08:03 -04:00

195 lines
7.9 KiB
Python

"""Command diagnostics use the same selection and launch contract as setup."""
import json
from argparse import Namespace
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
from hermes_cli import _launchers, doctor, doctor_platform
from pm.environments import install_state_dir, site_packages
def _tree(tmp_path, monkeypatch):
home = tmp_path / "data"
home.mkdir()
project = tmp_path / "source"
project.mkdir()
(project / ".install_method").write_text("git", encoding="utf-8")
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(home / "tools"))
monkeypatch.delenv("PREFIX", raising=False)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr(doctor, "PROJECT_ROOT", project)
monkeypatch.setattr(doctor, "HERMES_HOME", home)
monkeypatch.setattr(doctor, "DOCTOR_CHECKS", ((None, doctor_platform._check_command_installation),))
command = tmp_path / ".local" / "bin" / "hermes"
command.parent.mkdir(parents=True)
monkeypatch.setenv("PATH", str(command.parent))
return project, home, command
def _generation(project):
selected = install_state_dir(project) / "environments" / "selected" / "venv"
site_packages(selected).mkdir(parents=True)
(selected / "pyvenv.cfg").write_text("home = fixture\n", encoding="utf-8")
(install_state_dir(project) / "facts.json").write_text(
json.dumps({"packages": {"venv": {"environment": str(selected)}}}), encoding="utf-8"
)
return selected
def _pm_source(project, home):
root = Path(__file__).resolve().parents[2]
for relative in (
"hermes", "hermes_bootstrap.py", "hermes_constants.py", "hermes_cli/__init__.py",
"pm/environments.py", "pm/filesystem.py", "hermes_cli/runtime_state.py",
"hermes_cli/_early_recovery.py", "hermes_cli/_parser.py",
"hermes_cli/venv_sync.py", "hermes_cli/steward.py", "hermes_cli/stderr_timestamp.py",
):
target = project / relative
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(root / relative, target)
(project / "hermes_cli/main.py").write_text(
"def main():\n import selected_probe\n print(selected_probe.VALUE)\n return 0\n",
encoding="utf-8",
)
interpreter = home / "tools" / "python-fixture" / "bin" / "python3"
interpreter.parent.mkdir(parents=True)
interpreter.symlink_to(Path(sys._base_executable).resolve())
(home / "tools" / "facts.json").write_text(
json.dumps({"packages": {"python": {"entry": "python-fixture"}}}), encoding="utf-8"
)
return interpreter
@pytest.mark.platforms("posix")
def test_pm_generation_does_not_require_a_legacy_console_script(tmp_path, monkeypatch, capsys):
project, home, command = _tree(tmp_path, monkeypatch)
selected = _generation(project)
_pm_source(project, home)
assert _launchers.stage_launcher("hermes", project, command.parent) == command
doctor.run_doctor(Namespace(fix=True))
out = capsys.readouterr().out
assert "All checks passed" in out
assert not (project / "venv").exists()
assert not (selected / "bin" / "hermes").exists()
assert command.is_file() and not command.is_symlink()
@pytest.mark.platforms("posix")
@pytest.mark.parametrize("prior", ["missing", "legacy"])
def test_pm_fix_publishes_a_generation_aware_launcher(tmp_path, monkeypatch, capsys, prior):
project, home, command = _tree(tmp_path, monkeypatch)
_pm_source(project, home)
selected = _generation(project)
(site_packages(selected) / "selected_probe.py").write_text("VALUE = 'selected'\n", encoding="utf-8")
stale = project / "venv" / "bin" / "hermes"
stale.parent.mkdir(parents=True)
stale.write_text("#!/bin/sh\nexit 99\n", encoding="utf-8")
stale.chmod(0o755)
if prior == "legacy":
command.symlink_to(stale)
doctor.run_doctor(Namespace(fix=True))
assert "Fixed 1 issue" in capsys.readouterr().out
assert not command.is_symlink()
result = subprocess.run([str(command)], cwd=tmp_path, capture_output=True, text=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr
assert result.stdout.strip() == "selected"
assert stale.is_file()
@pytest.mark.platforms("posix")
@pytest.mark.parametrize("kind", ["wrapper", "symlink"])
def test_fix_preserves_user_managed_commands(tmp_path, monkeypatch, capsys, kind):
project, home, command = _tree(tmp_path, monkeypatch)
_pm_source(project, home)
_generation(project)
wrapper = tmp_path / "custom-wrapper"
body = "#!/bin/sh\nexit 42\n"
wrapper.write_text(body, encoding="utf-8")
wrapper.chmod(0o755)
if kind == "symlink":
command.symlink_to(wrapper)
else:
shutil.copy2(wrapper, command)
doctor.run_doctor(Namespace(fix=True))
out = capsys.readouterr().out
assert "Fixed 1 issue" not in out
assert command.read_text(encoding="utf-8") == body
if kind == "symlink":
assert command.is_symlink() and command.resolve() == wrapper
assert "manual" in out.lower()
@pytest.mark.parametrize("layout", ["legacy", "generation"])
@pytest.mark.parametrize("active", [True, False])
def test_doctor_reports_selected_import_tree_not_interpreter_prefix(tmp_path, monkeypatch, capsys, layout, active):
import importlib
import pm.paths
project, _home, _command = _tree(tmp_path, monkeypatch)
if layout == "generation":
selected = _generation(project)
else:
selected = project / "venv"
site_packages(selected).mkdir(parents=True)
(selected / "pyvenv.cfg").write_text("home = fixture\n", encoding="utf-8")
monkeypatch.setattr(pm.paths, "repo_root", lambda: project)
monkeypatch.setattr(doctor, "DOCTOR_CHECKS", ((None, doctor_platform._check_python_environment),))
# A venv prefix with no selected imports is not evidence of activation;
# conversely PM's base interpreter can import the selected tree directly.
monkeypatch.setattr(sys, "prefix", sys.base_prefix if active else str(selected))
if active:
module = site_packages(selected) / "doctor_selected_probe.py"
module.write_text("VALUE = 'selected'\n", encoding="utf-8")
monkeypatch.syspath_prepend(str(site_packages(selected)))
monkeypatch.delitem(sys.modules, "doctor_selected_probe", raising=False)
loaded = importlib.import_module("doctor_selected_probe")
assert Path(loaded.__file__) == module
monkeypatch.delitem(sys.modules, "doctor_selected_probe")
doctor.run_doctor(Namespace(fix=False))
out = capsys.readouterr().out
assert str(selected) in out
assert ("active in this process" in out) is active
assert ("runs outside it" in out) is not active
@pytest.mark.platforms("posix")
@pytest.mark.parametrize("method, remedy", [
("git", "hermes pm repair"), ("nix", "Nix"), ("docker", "docker pull"), ("apt", "pkg upgrade"),
])
def test_remedies_and_launcher_repairs_respect_install_owner(tmp_path, monkeypatch, capsys, method, remedy):
project, _home, command = _tree(tmp_path, monkeypatch)
(project / ".install_method").write_text(method, encoding="utf-8")
entry = project / "venv" / "bin" / "hermes"
entry.parent.mkdir(parents=True)
entry.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
entry.chmod(0o755)
monkeypatch.setattr(doctor_platform, "_PACKAGES", (("doctor_missing_dependency_probe", "Required dependency", False),))
monkeypatch.setattr(doctor, "DOCTOR_CHECKS", (
(None, doctor_platform._check_required_packages), (None, doctor_platform._check_command_installation),
))
doctor.run_doctor(Namespace(fix=True))
out = capsys.readouterr().out
assert remedy in out
assert "pip install" not in out
if method == "git":
assert command.is_symlink() and command.resolve() == entry
else:
assert not command.exists()
assert "hermes pm repair" not in out