Files
hermes-agent/tests/hermes_cli/test_venv_sync.py
2026-10-10 15:20:13 +02:00

182 lines
6.2 KiB
Python

"""venv_sync must work on trees where the venv does not exist yet.
It is the stdlib-only-at-import pre-venv entry point: the installers call
it on a fresh clone before any dependency is importable, and post_update
calls it after a tree swap when the venv is not trustworthy. Its
behaviour is driven through PM's public client; package resolution and
publication belong to PM, not this CLI entry point.
"""
from __future__ import annotations
import json
import subprocess
import sys
import textwrap
from pathlib import Path
import pytest
from hermes_cli import venv_sync
REPO_ROOT = Path(__file__).resolve().parents[2]
def _run_bare(snippet: str) -> subprocess.CompletedProcess:
program = f"import sys\nsys.path.insert(0, {str(REPO_ROOT)!r})\n" + textwrap.dedent(snippet)
return subprocess.run([sys.executable, '-I', '-S', '-c', program],
capture_output=True, text=True, cwd=REPO_ROOT, timeout=120, check=False)
def test_bare_import_and_passive_paths(tmp_path):
result = _run_bare(f"""
from pathlib import Path
from hermes_cli import venv_sync
assert 'pm' not in sys.modules
root = Path({str(tmp_path)!r})
assert venv_sync.sync(root, check=True)['state'] == 'failed'
(root / 'install-stamp.json').write_text('{{"updateMechanism":"external"}}')
assert venv_sync.sync(root) == {{'state': 'sealed', 'ok': True}}
assert venv_sync.prepare_launch(root, []) is None
assert 'pm.install' not in sys.modules
""")
assert result.returncode == 0, result.stderr
def test_bare_harness_rejects_third_party_import():
result = _run_bare('import requests')
assert result.returncode != 0 and 'ModuleNotFoundError' in result.stderr
def _checkout(tmp_path: Path, name: str = "co") -> Path:
root = tmp_path / name
(root / ".git").mkdir(parents=True)
(root / "pyproject.toml").write_text("[project]\nname='x'\n")
(root / "uv.lock").write_text("lock-v1\n")
return root
def _wire_pm(monkeypatch, *, current=False, error=None):
import pm
calls = []
monkeypatch.setattr(pm, "venv_is_current", lambda *, project_root: current)
def sync(*, explicit, project_root, evict_incompatible_plugins):
assert evict_incompatible_plugins, "an update sync must disable misfit plugins, not fail"
calls.append((project_root, explicit))
if error:
raise pm.InstallError("venv", error)
monkeypatch.setattr(pm, "sync_venv", sync)
return calls
class TestCheckoutSync:
@pytest.mark.parametrize("foreign", [False, True])
def test_each_root_uses_the_public_pm_transaction(self, tmp_path, monkeypatch, foreign):
root = _checkout(tmp_path)
monkeypatch.setattr(venv_sync, "_project_root", lambda: root)
calls = _wire_pm(monkeypatch)
assert venv_sync.sync(root if foreign else None) == {"state": "synced", "ok": True}
assert calls == [(root, True)]
def test_check_is_passive(self, tmp_path, monkeypatch):
root = _checkout(tmp_path)
calls = _wire_pm(monkeypatch)
before = set(tmp_path.rglob("*"))
assert venv_sync.sync(root, check=True) == {"state": "would-sync", "ok": True}
assert calls == []
assert set(tmp_path.rglob("*")) == before
def test_a_failed_sync_is_reported_and_retried(self, tmp_path, monkeypatch):
root = _checkout(tmp_path)
calls = _wire_pm(monkeypatch, error="resolution failed")
for _ in range(2):
out = venv_sync.sync(root)
assert out["state"] == "failed" and not out["ok"]
assert "resolution failed" in out["detail"]
assert calls == [(root, True), (root, True)]
class TestSealedTrees:
def test_a_sealed_tree_is_a_clean_noop(self, tmp_path, monkeypatch):
"""The desktop payload and nix bundle must not fail, must not sync."""
root = tmp_path / "sealed"
root.mkdir()
(root / "install-stamp.json").write_text(
json.dumps({"commit": "abc123", "payload": "full", "updateMechanism": "electron-updater"})
)
calls = _wire_pm(monkeypatch)
out = venv_sync.sync(root)
assert out == {"state": "sealed", "ok": True}
assert calls == []
def test_a_dev_tree_with_both_stamp_and_git_is_a_checkout(
self, tmp_path, monkeypatch
):
root = _checkout(tmp_path)
(root / "install-stamp.json").write_text(
json.dumps({"commit": "abc", "updateMechanism": "electron-updater"})
)
_wire_pm(monkeypatch)
assert venv_sync.sync(root)["state"] == "synced"
def test_a_stamp_without_update_mechanism_is_a_build_lane_bug(self, tmp_path):
root = tmp_path / "sealed"
root.mkdir()
(root / "install-stamp.json").write_text(json.dumps({"commit": "abc"}))
with pytest.raises(RuntimeError, match="updateMechanism"):
venv_sync.sync(root)
class TestCliContract:
def test_json_output_and_exit_codes(self, tmp_path):
"""post_update and the installers read exactly this."""
root = tmp_path / "sealed"
root.mkdir()
(root / "install-stamp.json").write_text(
json.dumps({"commit": "x", "updateMechanism": "electron-updater"})
)
proc = subprocess.run(
[
sys.executable,
"-m",
"hermes_cli.venv_sync",
"--project-root",
str(root),
"--json",
],
capture_output=True,
text=True,
cwd=REPO_ROOT,
check=False,
)
assert proc.returncode == 0, proc.stderr
assert json.loads(proc.stdout) == {"state": "sealed", "ok": True}
def test_failure_exits_nonzero(self, tmp_path):
proc = subprocess.run(
[
sys.executable,
"-m",
"hermes_cli.venv_sync",
"--project-root",
str(tmp_path), # empty dir: no pyproject, no stamp
"--json",
],
capture_output=True,
text=True,
cwd=REPO_ROOT,
check=False,
)
assert proc.returncode == 1
assert json.loads(proc.stdout)["state"] == "failed"