Files
hermes-agent/tests/hermes_cli/test_plugin_update_recovery.py
ethernet bbec973514 refactor(pm): pm owns the dependency-environment layout and interpreter paths
hermes_cli.runtime_paths (venv generations, selection, activation) moves to
pm.environments, and gains venv_bin_dir / venv_python / project_python. Every
in-tree caller asks pm for an interpreter now; pm no longer reaches back into
hermes_cli for its own environment layout (pm.packages, pm.extras, pm.ensure,
pm.paths imported hermes_cli.runtime_paths). The three open-coded
"Scripts/python.exe or bin/python" ladders in pm collapse onto venv_python.

hermes_constants.venv_python_path / venv_bin_dir and hermes_cli.runtime_paths
stay as frozen-updater-surface shims only (tests/compat/old_updater_surface.json).

To keep the boot path light, pm/__init__ resolves its facade lazily (PEP 562)
and pm.registry loads the built-in package definitions on first read instead of
at import: `import hermes_bootstrap` now loads pm + pm.environments only (25ms,
was 37ms with the eager facade dragging in the downloader). The stripped-payload
fixtures that ship only pre-import files keep working for the same reason.

Also restores two frozen-surface re-exports the F401 sweep dropped
(banner._github_compare_behind, cua_backend.resolve_cua_driver_cmd).
2026-09-18 20:02:36 -04:00

63 lines
2.6 KiB
Python

"""Process death cannot leave new plugin code paired with the old dependency selection."""
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import sys
import pytest
@pytest.mark.parametrize("committed", [False, True])
def test_boot_recovers_plugin_publication_after_process_death(tmp_path, committed):
home = tmp_path / "home"
target = home / "plugins" / "example"
target.mkdir(parents=True)
(target / "__init__.py").write_text("old code", encoding="utf-8")
metadata = target.parent / ".install-metadata.json"
metadata.write_text('{"example":{"revision":"old"}}\n', encoding="utf-8")
previous = metadata.read_bytes()
staged = tmp_path / "staged"
staged.mkdir()
(staged / "__init__.py").write_text("new code", encoding="utf-8")
project = tmp_path / "project"
project.mkdir()
env = {**os.environ, "HERMES_HOME": str(home), "PYTHONPATH": str(Path(__file__).resolve().parents[2])}
program = '''
from pathlib import Path
import os,sys
from pm.publication import StagedPlugin
from pm.store import tree_digest
from pm.environments import runtime_facts_path
from pm.lock import Facts
project,staged,target = map(Path,sys.argv[1:4])
StagedPlugin({"staged": str(staged), "target": str(target), "target_digest": tree_digest(target),
"old_metadata": {"example":{"revision":"old"}},
"new_metadata": {"example":{"revision":"new"}}}).publish(project)
if sys.argv[4] == "True":
Facts(runtime_facts_path(project)).record_state("venv","new",[])
os._exit(17)
'''
child = subprocess.run([sys.executable, "-c", program, str(project), str(staged), str(target), str(committed)],
env=env, cwd=tmp_path, capture_output=True, text=True, timeout=30)
assert child.returncode == 17, child.stderr
recovery = '''
from pathlib import Path
import sys
from hermes_cli.runtime_state import runtime_lock,recover_publication
project=Path(sys.argv[1])
with runtime_lock(project):
recover_publication(project)
recover_publication(project)
'''
result = subprocess.run([sys.executable, "-c", recovery, str(project)], env=env, cwd=tmp_path,
capture_output=True, text=True, timeout=30)
assert result.returncode == 0, result.stderr
assert (target / "__init__.py").read_text(encoding="utf-8") == ("new code" if committed else "old code")
assert json.loads(metadata.read_text(encoding="utf-8"))["example"]["revision"] == ("new" if committed else "old")
if not committed:
assert metadata.read_bytes() == previous
assert not list(target.parent.glob(".previous-*"))