"""PM's resolver must not depend on the application it is repairing.""" import hashlib import json import os from pathlib import Path import shutil import subprocess import sys import pytest def test_pm_runtime_discovers_plugins_without_application_dependencies(tmp_path, monkeypatch): from pm.runtime import prepare_runtime uv = shutil.which("uv") if uv is None: pytest.skip("uv is required for the real dependency-runtime test") home = tmp_path / "home" home.mkdir() monkeypatch.setenv("HOME", str(tmp_path)) monkeypatch.setenv("HERMES_HOME", str(home)) monkeypatch.setenv("HERMES_RUNTIME_DIR", str(home / "tools")) (home / "config.yaml").write_text("plugins:\n enabled: []\n", encoding="utf-8") repo = Path(__file__).resolve().parents[2] python = prepare_runtime(Path(uv), Path(sys.executable), tmp_path / "runtime") env = {k: v for k, v in os.environ.items() if not k.startswith(("PYTHON", "UV_"))} env.update(HERMES_HOME=str(home), HERMES_RUNTIME_DIR=str(home / "tools")) # Import real PM, including its production plugin-discovery chain. code = f""" import importlib.util, json, sys sys.path.insert(0, {str(repo)!r}) from pm.workspace import enabled_member_dirs from pm.plugins_state import read_home_selection from pathlib import Path assert read_home_selection(Path({str(home)!r}))["plugins"]["enabled"] == [] assert enabled_member_dirs() == [] assert importlib.util.find_spec("openai") is None assert importlib.util.find_spec("yaml") is None print(json.dumps({{"prefix": sys.prefix, "yaml": importlib.util.find_spec("ruamel.yaml").origin}})) """ result = subprocess.run([str(python), "-I", "-B", "-c", code], env=env, capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr report = json.loads(result.stdout) assert Path(report["yaml"]).is_relative_to(Path(report["prefix"])) assert prepare_runtime(Path(uv), Path(sys.executable), tmp_path / "runtime", offline=True) == python # Repair PM itself from its own lock, without trusting an existing marker. (Path(report["yaml"]).parent / "main.py").unlink() repaired = prepare_runtime(Path(uv), Path(sys.executable), tmp_path / "runtime", offline=True) assert repaired != python checked = subprocess.run([str(repaired), "-I", "-B", "-c", code], env=env, capture_output=True, text=True, timeout=30) assert checked.returncode == 0, checked.stdout + checked.stderr def test_cold_worker_bootstrap_reuses_the_requests_cache(tmp_path, monkeypatch): import pm from hermes_constants import get_default_hermes_root from pm import client, runtime from pm.runtime_stage import stage_runtime uv = shutil.which("uv") assert uv, "the bootstrap cache contract requires real uv" tools = Path(uv), Path(sys.executable) cache = tmp_path / "shared-cache" monkeypatch.setenv("HOME", str(tmp_path / "home")) monkeypatch.setenv("USERPROFILE", str(tmp_path / "home")) monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home/.hermes")) monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "tools")) monkeypatch.setattr("pm.paths.repo_root", lambda: tmp_path / "project") monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: tools) monkeypatch.setattr(client, "is_runtime", lambda: False) stage_runtime(*tools, tmp_path / "warmup", cache=cache) shutil.rmtree(tmp_path / "warmup") def offline_stage(*args, **kwargs): # A fresh manager must use the populated explicit cache, not download # its dependencies again under the bundle's isolated HOME. kwargs["offline"] = True return stage_runtime(*args, **kwargs) monkeypatch.setattr("pm.runtime_stage.stage_runtime", offline_stage) worker = Path(client.__file__).with_name("worker.py") script = ( "import runpy, sys; from pathlib import Path; " f"sys.path.insert(0, {str(worker.parent.parent)!r}); import pm._uv; " f"pm._uv._toolchain = lambda **kwargs: (Path({uv!r}), Path({sys.executable!r})); " f"runpy.run_path({str(worker)!r}, run_name='__main__')" ) def command(*args, **kwargs): prepared = runtime.runtime_command(*args, **kwargs) return [*prepared[:3], "-c", script] monkeypatch.setattr(client, "runtime_command", command) before = dict(os.environ) pm.prune_cache(cache) assert cache.is_dir() assert not (get_default_hermes_root() / "cache/uv").exists(), "bootstrap created an unshared private cache" assert dict(os.environ) == before @pytest.mark.platforms("macos", "windows") def test_sealed_worker_command_uses_only_its_recorded_site(tmp_path, monkeypatch): from pm import paths from pm.runtime import runtime_command repo = tmp_path / "payload" / "hermes-agent" repo.mkdir(parents=True) (repo.parent / "manifest.json").write_text('{"repo":"hermes-agent"}') runtime = repo.parent / "pm-runtime" site = runtime / "site" site.mkdir(parents=True) base = repo.parent / "python" if os.name == "nt": shutil.copytree(Path(sys.base_prefix), base) python = base / "python.exe" else: # The guard resolves symlinks (a python linked outside the payload IS an escape), so the # interpreter is a real copy inside the payload. A relocatable/framework build locates its # stdlib beside the executable: supply the host's library tree the way the bundle test does. (base / "bin").mkdir(parents=True) python = base / "bin" / "python" shutil.copy2(Path(sys._base_executable).resolve(), python) (base / "lib").symlink_to(Path(sys.base_prefix) / "lib", target_is_directory=True) (runtime / "pm-runtime.json").write_text(json.dumps({ "python": os.path.relpath(python, runtime), "sitePackages": "site", })) script = repo / "probe.py" script.write_text("import sys,json; print(json.dumps(sys.path))") monkeypatch.setattr(paths, "repo_root", lambda: repo) command = runtime_command(script) result = subprocess.run(command, cwd=tmp_path, capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stderr entries = json.loads(result.stdout) assert str(site) in entries assert not any(Path(entry).name in {"site-packages", "dist-packages"} for entry in entries) def test_one_store_through_symlinked_homes_keeps_one_pm_runtime(tmp_path, monkeypatch): """#123798: a store reached through symlinks (a per-task HERMES_HOME whose tools/ links back, a home under /home -> /var/home) is ONE PM runtime: every spelling reuses the selected generation, and a record written before canonicalization is not re-staged.""" from pm import runtime project = tmp_path / "project" project.mkdir() (project / "pyproject.toml").write_text("[project]\nname = 'fixture'\n") (project / "uv.lock").write_text("version = 1\n") real = tmp_path / "volume" / "tools" (real / "python" / "bin").mkdir(parents=True) (real / "python" / "bin" / "python3.14").touch() home, task = tmp_path / "home", tmp_path / "task" try: (real / "python" / "bin" / "python3").symlink_to("python3.14") # as the pinned CPython ships for linked in (home, task): linked.mkdir() (linked / "tools").symlink_to(real, target_is_directory=True) except OSError as exc: pytest.skip(f"symlinks unavailable: {exc}") def pre_fix_identity(python: Path) -> str: # the selected.json format earlier releases wrote digest = hashlib.sha256() for name in ("pyproject.toml", "uv.lock"): digest.update((project / name).read_bytes() + b"\0") digest.update(str(python).encode()) return digest.hexdigest() staged = [] def stage(uv, python, environment, **_): staged.append(python) environment.mkdir(parents=True) return runtime._python(environment) monkeypatch.setattr("pm.runtime_stage.stage_runtime", stage) monkeypatch.setattr(runtime, "_validate", lambda python, env: "") monkeypatch.setattr(runtime, "_hold_for_children", lambda environment: None) root = tmp_path / "pm-runtime" old = root / "generations" / "old" old.mkdir(parents=True) (old / "pm-runtime.json").write_text("{}") (root / "selected.json").write_text(json.dumps( {"inputs": pre_fix_identity(home / "tools" / "python" / "bin" / "python3"), "generation": "generations/old"})) def launch(tools: Path, bootstrap: bool = False) -> Path: return runtime.prepare_runtime(tools / "uv", tools / "python" / "bin" / "python3", root, project=project, bootstrap=bootstrap) assert launch(home / "tools") == runtime._python(old) current = launch(task / "tools", bootstrap=True) # a per-task home that was already churning re-stages once for tools in (home / "tools", task / "tools", real, home / "tools"): assert launch(tools) == current assert len(staged) == 1