Files
hermes-agent/tests/pm/test_activation_setup.py
ethernet 53e6f001c7 refactor(pm): consolidate runtime ownership and updater completion
Run historical updater completion in a fresh interpreter so cached imports
cannot revive retired dependency installers. Share Git and ZIP completion,
carry receipt and recovery state, and preserve child exit status.

Route plugin admission, binary acquisition, desktop launch and build paths
through PM. Replace redundant helpers and tests with real worker, package,
publication and launch checks. Keep the shipped compatibility surface fixed.

Targeted Python and desktop checks pass. Native update journeys and fresh
production image qualification remain pending. This is a checkpoint before
those acceptance runs.
2026-09-12 16:30:35 -04:00

245 lines
11 KiB
Python

"""Bash activation crosses real setup, PM install, and uv dependency publication.
Only the downloaded tool payloads are fixtures: bootstrap uv locates the test
interpreter, then delegates every dependency operation to real, offline uv.
The real PM stage builder seeds its locked dependencies online before activation.
PM/activation/setup code is copied unmodified; all state is disposable.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import shlex
import shutil
import stat
import subprocess
import sys
import pytest
from pm.lock import Lockfile
from pm.store import current_target
from tests.pm._fixtures import _wheel, make_tar, served # noqa: F401 -- shared HTTP fixture
pytestmark = pytest.mark.platforms("posix")
REPO = Path(__file__).resolve().parents[2]
def _snapshot(paths):
"""Notice creation, rewriting, chmod, and new children in protected locations."""
result = {}
for root in paths:
for path in [root, *sorted(root.rglob("*"))]:
if path.exists():
info = path.stat()
result[str(path)] = (
stat.S_IMODE(info.st_mode), info.st_mtime_ns,
path.read_bytes() if path.is_file() else None,
)
return result
def test_activation_real_setup_pm_lifecycle(tmp_path, served):
bash, uv = shutil.which("bash"), shutil.which("uv")
assert bash and uv, "this integration contract requires Bash and real uv"
interpreter = str(Path(sys._base_executable).resolve())
core = tmp_path / "checkout with spaces"
core.mkdir()
home = tmp_path / "home"
home.mkdir()
hermes_home = home / ".hermes"
runtime = tmp_path / "runtime"
scratch = tmp_path / "tmp"
scratch.mkdir()
env = {
"PATH": os.environ["PATH"], "HOME": str(home), "HERMES_HOME": str(hermes_home),
"HERMES_RUNTIME_DIR": str(runtime), "SHELL": bash, "TMPDIR": str(scratch),
"XDG_CONFIG_HOME": str(home / ".config"), "XDG_CACHE_HOME": str(home / ".cache"),
"LANG": "C.UTF-8", "PYTHONNOUSERSITE": "1", "UV_OFFLINE": "1",
"UV_CACHE_DIR": str(home / ".cache" / "uv"), "UV_PYTHON_DOWNLOADS": "never",
}
for name in ("activate", "setup-hermes.sh", "hermes_constants.py", "hermes_yaml.py", "utils.py"):
shutil.copy2(REPO / name, core / name)
for name in ("pm", "hermes_cli"):
shutil.copytree(REPO / name, core / name, ignore=shutil.ignore_patterns("__pycache__"))
# Plugin selection imports the real CLI config reader even with no plugins.
# Supply its installed YAML dependency, not a stub parser or config module.
import ruamel.yaml
shutil.copytree(Path(ruamel.yaml.__file__).parent, core / "ruamel" / "yaml", ignore=shutil.ignore_patterns("__pycache__"))
# Never copy real user files: these are deliberately public fixture sentinels.
protected = [core / ".env", home / ".local" / "bin", hermes_home / "skills",
home / ".bashrc", home / ".bash_profile", home / ".zshrc"]
for path in (home / ".local" / "bin" / "hermes", hermes_home / "skills" / "keep.md",
home / ".bashrc", home / ".bash_profile", home / ".zshrc"):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("user-owned fixture\n", encoding="utf-8")
(hermes_home / "skills").chmod(0o755)
(core / ".env.example").write_text("FIXTURE_ONLY=example\n", encoding="utf-8")
(core / "skills").mkdir()
(core / "skills" / "bundled.md").write_text("must not be seeded\n", encoding="utf-8")
untouched = _snapshot(protected)
wheels = tmp_path / "wheels"
wheels.mkdir()
_wheel(wheels, "activation_dep", "1.0")
(core / "pyproject.toml").write_text(
'[project]\nname="activation-proof"\nversion="1"\nrequires-python=">=3.11"\n'
'dependencies=["activation-dep==1.0"]\n[project.optional-dependencies]\nall=[]\n'
'[tool.uv]\npackage=false\nno-index=true\n'
f'find-links=[{json.dumps(wheels.as_posix())}]\n', encoding="utf-8",
)
locked = subprocess.run(
[uv, "lock", "--offline", "--python", interpreter], cwd=core, env=env,
capture_output=True, text=True, timeout=60,
)
assert locked.returncode == 0, locked.stdout + locked.stderr
dependency_lock = (core / "uv.lock").read_bytes()
# Seed PM's own cache with its real locked wheels, not the application's
# fixture wheel or a copied host cache. Discard this environment so source
# still bootstraps and publishes the isolated PM runtime itself, offline.
seed = tmp_path / "pm-seed"
seed_env = {key: value for key, value in env.items() if key != "UV_OFFLINE"}
for key in ("SSL_CERT_FILE", "SSL_CERT_DIR", "NIX_SSL_CERT_FILE"):
if key in os.environ:
seed_env[key] = os.environ[key]
seeded = subprocess.run(
[interpreter, "-I", "-B", "-c",
"import sys; from pathlib import Path; sys.path.insert(0, sys.argv[1]); "
"from pm.runtime_stage import stage_runtime; "
"stage_runtime(Path(sys.argv[2]), Path(sys.argv[3]), Path(sys.argv[4]), "
"project=Path(sys.argv[1]) / 'pm', offline=False)",
str(core), uv, interpreter, str(seed)],
cwd=tmp_path, env=seed_env, capture_output=True, text=True, timeout=180,
)
assert seeded.returncode == 0, seeded.stdout + seeded.stderr
shutil.rmtree(seed)
assert not (hermes_home / "installs").exists()
docroot, base_url = served
# Both the shell bootstrap and PM's fallback downloader stay on loopback.
(core / "pm" / "artifact-mirror.json").write_text(
json.dumps({"origin": base_url, "prefix": "mirror/"}, indent=2) + "\n",
encoding="utf-8",
)
calls = tmp_path / "uv-calls"
uv_script = (
f"#!{bash}\n"
f"printf '%s\\n' \"$*\" >> {shlex.quote(str(calls))}\n"
'if [ "$1 $2" = "python install" ]; then exit 0; fi\n'
f'if [ "$1 $2" = "python find" ]; then printf \'%s\\n\' {shlex.quote(interpreter)}; exit 0; fi\n'
f"exec {shlex.quote(uv)} --offline \"$@\"\n"
)
_, uv_digest = make_tar(docroot, "uv.tar.gz", {"uv-fixture/uv": uv_script})
python_script = f'#!{bash}\nexec {shlex.quote(interpreter)} "$@"\n'
lock = Lockfile(core / "pm" / "lock.json")
# Replace shipped pins, not PM implementations or its package registry.
lock.path.unlink()
lock = Lockfile(lock.path)
target = current_target()
version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}+fixture"
def pin_python(revision):
filename = f"python-{revision}.tar.gz"
_, digest = make_tar(docroot, filename, {
"python/bin/python3": python_script,
"python/revision": revision,
})
lock.set_pin("python", version, {target: {"url": f"{base_url}/{filename}", "sha256": digest}})
lock.save()
return digest
lock.set_pin("uv", "fixture", {target: {"url": f"{base_url}/uv.tar.gz", "sha256": uv_digest}})
first_digest = pin_python("first")
def activate(*, succeeds=True):
# No `set -e`: inspect source's status and prove the caller survives.
script = '''
prior_path=$PATH
prior_pythonpath=${PYTHONPATH-}
source "$1/activate"
status=$?
if [ "$status" != 0 ]; then
test "$PATH" = "$prior_path" || exit 91
test "${PYTHONPATH-}" = "$prior_pythonpath" || exit 92
test -z "${__HERMES_ACTIVATED-}" || exit 93
printf 'CALLER_SURVIVED:%s\\n' "$status"
exit "$status"
fi
python3 -c 'import activation_dep, json, os; print(json.dumps({"version": activation_dep.__version__, "module": activation_dep.__file__, "pythonpath": os.environ["PYTHONPATH"]}))' || exit 94
deactivate
test "$PATH" = "$prior_path" || exit 95
test "${PYTHONPATH-}" = "$prior_pythonpath" || exit 96
'''
result = subprocess.run(
[bash, "--noprofile", "--norc", "-c", script, "activation-test", str(core)],
cwd=tmp_path, env=env, capture_output=True, text=True, timeout=90,
)
assert _snapshot(protected) == untouched, result.stdout + result.stderr
assert (result.returncode == 0) is succeeds, result.stdout + result.stderr
return result
def selection():
records = list(hermes_home.glob("installs/*/facts.json"))
assert len(records) == 1
return json.loads(records[0].read_text())["packages"]["venv"]
def operations(name):
return [line for line in calls.read_text().splitlines() if line.split()[0] == name]
def app_syncs():
return [line for line in operations("sync") if "--frozen --all-packages" in line]
cold = activate()
first = selection()
probe = json.loads(cold.stdout)
assert probe["version"] == "1.0"
assert Path(probe["module"]).is_relative_to(Path(first["environment"]))
assert probe["pythonpath"].split(os.pathsep)[0] == str(core)
# Cold activation builds both PM's isolated runtime and the app environment.
assert "Preparing the isolated PM runtime" in cold.stderr
assert len(app_syncs()) == 1
facts = json.loads((runtime / "facts.json").read_text())["packages"]
assert facts["python"]["artifacts"] == [first_digest]
assert facts["uv"]["artifacts"] == [uv_digest]
# Also catch chmod of an existing .env, not just unwanted first creation.
(core / ".env").write_text("FIXTURE_ONLY=existing\n", encoding="utf-8")
(core / ".env").chmod(0o644)
untouched = _snapshot(protected)
activate()
assert selection() == first
# The first warm launch rebinds PM from bootstrap Python to its managed
# interpreter. The application selection is unchanged; later launches reuse both.
assert len(app_syncs()) == 1
prepared = operations("sync")
activate()
assert selection() == first
assert operations("sync") == prepared
assert len([line for line in operations("python") if line.startswith("python install ")]) == 3
second_digest = pin_python("second")
activate()
second = selection()
assert second["stamp"] != first["stamp"]
assert second["environment"] != first["environment"]
assert Path(first["environment"]).is_dir()
assert len(app_syncs()) == 2
facts = json.loads((runtime / "facts.json").read_text())["packages"]
assert facts["python"]["artifacts"] == [second_digest]
assert (core / "uv.lock").read_bytes() == dependency_lock
# A real uv failure must cross PM -> setup -> source without selection or
# caller mutation. An invalid local lock fails deterministically offline.
(core / "uv.lock").write_text("not valid TOML [\n", encoding="utf-8")
failed = activate(succeeds=False)
assert "pm install failed" in failed.stderr
assert "setup failed" in failed.stderr
assert "CALLER_SURVIVED:" in failed.stdout
assert selection() == second
assert len(app_syncs()) == 3
assert len([line for line in operations("python") if line.startswith("python install ")]) == 5