hermes-agent/tests/tools/test_lazy_deps_durable_target.py
Ben 41b9b7e719 test(lazy-deps): make durable-target tests network-free
CI test shard has no PyPI egress: the real 'pip install packaging==20.9'
in test_core_package_is_not_shadowed failed (the pypi.org reachability
probe passed but the actual install didn't), failing slice 2/6.

- Prove the anti-shadow invariant deterministically: synthesize a fake
  'packaging' in the durable target with a sentinel and assert the import
  still resolves to the core copy (TestCoreNeverShadowed). No network.
- Cover the install wire offline: stub subprocess and assert --target +
  --constraint are built in durable mode and absent in venv-scoped mode
  (TestInstallArgConstruction).
- Gate the genuine PyPI install behind HERMES_RUN_NETWORK_TESTS=1 (opt-in,
  skipped in CI) instead of a flaky reachability probe that doesn't predict
  install success.
2026-06-25 09:20:13 +10:00

312 lines
13 KiB
Python

"""Tests for the durable lazy-install target (immutable Docker images).
These cover the mechanism that lets opt-in backends lazy-install on the
sealed-venv Docker image without being able to break the agent core:
installs are redirected to a writable dir on the data volume, and that dir
is appended to the END of ``sys.path`` so the core venv always wins name
collisions.
The headline invariant — *a package in the durable store can never shadow
a core module* — is proved with a REAL install into a temp target (no
mocked pip), exercising the actual ``--target`` + sys.path-append path.
That E2E test is guarded by network availability; everything else is pure
unit logic with no network.
"""
from __future__ import annotations
import os
import subprocess
import sys
import sysconfig
from pathlib import Path
import pytest
from tools import lazy_deps as ld
# ---------------------------------------------------------------------------
# Target resolution + gating
# ---------------------------------------------------------------------------
class TestTargetResolution:
def test_no_target_when_env_unset(self, monkeypatch):
monkeypatch.delenv(ld._LAZY_TARGET_ENV, raising=False)
assert ld._lazy_install_target() is None
def test_no_target_when_env_blank(self, monkeypatch):
monkeypatch.setenv(ld._LAZY_TARGET_ENV, " ")
assert ld._lazy_install_target() is None
def test_target_resolved_when_set(self, monkeypatch, tmp_path):
monkeypatch.setenv(ld._LAZY_TARGET_ENV, str(tmp_path / "lazy"))
assert ld._lazy_install_target() == tmp_path / "lazy"
class TestGatingWithTarget:
"""``HERMES_DISABLE_LAZY_INSTALLS=1`` must STOP blocking once a durable
target is configured — the redirect is the safe path — but the config
kill switch still wins in every mode."""
def test_disable_env_blocks_without_target(self, monkeypatch):
monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1")
monkeypatch.delenv(ld._LAZY_TARGET_ENV, raising=False)
# config unreadable → fails open on the config check, but the sealed
# env var with no target still blocks.
monkeypatch.setattr(
"hermes_cli.config.load_config", lambda: {}, raising=False
)
assert ld._allow_lazy_installs() is False
def test_disable_env_allows_with_target(self, monkeypatch, tmp_path):
monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1")
monkeypatch.setenv(ld._LAZY_TARGET_ENV, str(tmp_path))
monkeypatch.setattr(
"hermes_cli.config.load_config", lambda: {}, raising=False
)
assert ld._allow_lazy_installs() is True
def test_config_killswitch_wins_even_with_target(self, monkeypatch, tmp_path):
# Explicit opt-out must disable installs even when a target exists.
monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1")
monkeypatch.setenv(ld._LAZY_TARGET_ENV, str(tmp_path))
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: {"security": {"allow_lazy_installs": False}},
raising=False,
)
assert ld._allow_lazy_installs() is False
def test_normal_mode_unaffected(self, monkeypatch):
# No sealed env, no target → default allow (unchanged behaviour).
monkeypatch.delenv("HERMES_DISABLE_LAZY_INSTALLS", raising=False)
monkeypatch.delenv(ld._LAZY_TARGET_ENV, raising=False)
monkeypatch.setattr(
"hermes_cli.config.load_config", lambda: {}, raising=False
)
assert ld._allow_lazy_installs() is True
# ---------------------------------------------------------------------------
# ABI stamp / durable-store rebuild safety
# ---------------------------------------------------------------------------
class TestAbiStamp:
def test_creates_dir_and_stamp(self, tmp_path):
target = tmp_path / "lazy"
err = ld._ensure_target_ready(target)
assert err is None
assert target.is_dir()
stamp = target / ld._TARGET_STAMP_NAME
assert stamp.read_text().strip() == ld._python_abi_tag()
def test_matching_stamp_preserves_contents(self, tmp_path):
target = tmp_path / "lazy"
ld._ensure_target_ready(target)
# Drop a fake installed package.
(target / "somepkg").mkdir()
(target / "somepkg" / "__init__.py").write_text("x = 1\n")
# Re-run with the SAME abi → contents must survive.
err = ld._ensure_target_ready(target)
assert err is None
assert (target / "somepkg" / "__init__.py").exists()
def test_mismatched_stamp_wipes_contents(self, tmp_path):
target = tmp_path / "lazy"
ld._ensure_target_ready(target)
(target / "stalepkg").mkdir()
(target / "stalepkg" / "mod.py").write_text("x = 1\n")
# Simulate an image rebuild onto a different interpreter ABI.
(target / ld._TARGET_STAMP_NAME).write_text("2.7:old-abi-tag")
err = ld._ensure_target_ready(target)
assert err is None
# Stale package wiped; stamp refreshed to current ABI.
assert not (target / "stalepkg").exists()
assert (target / ld._TARGET_STAMP_NAME).read_text().strip() == ld._python_abi_tag()
def test_readonly_target_reports_error(self, tmp_path):
# A path under a non-writable parent should surface a clean error,
# not raise.
ro_parent = tmp_path / "ro"
ro_parent.mkdir()
os.chmod(ro_parent, 0o500)
try:
err = ld._ensure_target_ready(ro_parent / "lazy")
assert err is not None
assert "not writable" in err
finally:
os.chmod(ro_parent, 0o700) # let pytest clean up
# ---------------------------------------------------------------------------
# sys.path append ordering (the core-wins invariant, unit level)
# ---------------------------------------------------------------------------
class TestSysPathAppend:
def test_target_appended_not_prepended(self, tmp_path, monkeypatch):
target = tmp_path / "lazy"
target.mkdir()
saved = list(sys.path)
try:
ld._activate_target_on_syspath(target)
assert str(target) in sys.path
# Must be at/after every pre-existing entry — i.e. core wins.
idx = sys.path.index(str(target))
assert idx >= len(saved), (
"durable target must be appended after all core entries"
)
finally:
sys.path[:] = saved
def test_activation_idempotent(self, tmp_path, monkeypatch):
target = tmp_path / "lazy"
target.mkdir()
saved = list(sys.path)
try:
ld._activate_target_on_syspath(target)
ld._activate_target_on_syspath(target)
assert sys.path.count(str(target)) == 1
finally:
sys.path[:] = saved
# ---------------------------------------------------------------------------
# Install path: arg construction (network-free) + a real install (opt-in).
# ---------------------------------------------------------------------------
class TestInstallArgConstruction:
"""Verify the durable-target install builds the right pip/uv command
WITHOUT hitting the network, by stubbing the subprocess layer. This is
the CI-safe coverage of the install path; the genuine PyPI install below
is opt-in only."""
def test_target_and_constraint_args_passed(self, tmp_path, monkeypatch):
target = tmp_path / "lazy-packages"
monkeypatch.setenv(ld._LAZY_TARGET_ENV, str(target))
# No uv on PATH → force the pip tier so we assert one known command.
monkeypatch.setattr(ld.shutil, "which", lambda _: None)
captured = {}
def fake_run(cmd, *a, **k):
# The pip --version probe must look healthy so we reach install.
if "--version" in cmd:
return subprocess.CompletedProcess(cmd, 0, "pip 24.0", "")
captured["cmd"] = cmd
return subprocess.CompletedProcess(cmd, 0, "ok", "")
monkeypatch.setattr(ld.subprocess, "run", fake_run)
# Avoid mutating the real interpreter's sys.path on success.
monkeypatch.setattr(ld, "_activate_target_on_syspath", lambda _t: None)
result = ld._venv_pip_install(("somepkg==1.2.3",))
assert result.success
cmd = captured["cmd"]
# --target points at the durable dir...
assert "--target" in cmd
assert str(target) in cmd
# ...a --constraint file pins shared deps to core...
assert "--constraint" in cmd
# ...and the spec is last.
assert cmd[-1] == "somepkg==1.2.3"
def test_no_target_args_in_venv_scoped_mode(self, monkeypatch):
# Env unset → plain venv-scoped install, no --target / --constraint.
monkeypatch.delenv(ld._LAZY_TARGET_ENV, raising=False)
monkeypatch.setattr(ld.shutil, "which", lambda _: None)
captured = {}
def fake_run(cmd, *a, **k):
if "--version" in cmd:
return subprocess.CompletedProcess(cmd, 0, "pip 24.0", "")
captured["cmd"] = cmd
return subprocess.CompletedProcess(cmd, 0, "ok", "")
monkeypatch.setattr(ld.subprocess, "run", fake_run)
result = ld._venv_pip_install(("somepkg==1.2.3",))
assert result.success
assert "--target" not in captured["cmd"]
assert "--constraint" not in captured["cmd"]
@pytest.mark.skipif(
os.environ.get("HERMES_RUN_NETWORK_TESTS") != "1",
reason="opt-in real-install test (set HERMES_RUN_NETWORK_TESTS=1); CI runs "
"the network-free arg-construction + synthetic-shadow tests instead",
)
class TestRealInstallCoreWins:
"""Genuine PyPI install into a durable target (opt-in). Proves the wire
end to end: the package lands in the target, not the core venv, and is
importable via the appended sys.path entry. Skipped by default so the
unit-test shard never depends on PyPI reachability/egress."""
def test_install_lands_in_target_and_imports(self, tmp_path, monkeypatch):
target = tmp_path / "lazy-packages"
monkeypatch.setenv(ld._LAZY_TARGET_ENV, str(target))
# 'isodate' is tiny, pure-python, and not shipped in the core venv,
# so a successful import must resolve to the durable target.
result = ld._venv_pip_install(("isodate==0.7.2",))
assert result.success, f"install failed: {result.stderr}"
# Landed in the durable target, not the core venv.
installed = list(target.glob("isodate*"))
assert installed, f"isodate not found under target {target}: {list(target.iterdir())}"
# Importable now that the target is on sys.path.
import importlib
importlib.invalidate_caches()
mod = importlib.import_module("isodate")
assert mod.__file__ is not None
assert Path(mod.__file__).is_relative_to(target)
class TestCoreNeverShadowed:
"""The headline invariant — a package in the durable store can never
shadow a core module — proved WITHOUT a network install by synthesizing
a shadow copy of a core package directly on disk in the target. This is
deterministic (no PyPI) and a stronger check: we control exactly what
the shadow contains, so a sentinel attribute proves which copy won.
"""
def test_synthetic_shadow_does_not_win(self, tmp_path, monkeypatch):
# 'packaging' is always present in the venv (transitive of the build
# toolchain). Resolve the core copy's location first.
import importlib.util
core_spec = importlib.util.find_spec("packaging")
assert core_spec is not None and core_spec.origin
core_path = Path(core_spec.origin).parent
# Plant a fake 'packaging' in the durable target with a sentinel that
# the real core copy does NOT have.
target = tmp_path / "lazy-packages"
monkeypatch.setenv(ld._LAZY_TARGET_ENV, str(target))
ld._ensure_target_ready(target)
shadow_pkg = target / "packaging"
shadow_pkg.mkdir()
(shadow_pkg / "__init__.py").write_text(
"SHADOW_SENTINEL = True\n__version__ = '0.0.0-shadow'\n"
)
assert (shadow_pkg / "__init__.py").exists(), "shadow copy must exist on disk"
# Activate the target (append-only) and re-resolve the import.
saved = list(sys.path)
try:
ld._activate_target_on_syspath(target)
import importlib
importlib.invalidate_caches()
spec_after = importlib.util.find_spec("packaging")
assert spec_after is not None and spec_after.origin
resolved = Path(spec_after.origin).parent
# Core path must still win; the shadow in the target is ignored.
assert resolved == core_path, (
f"durable-store copy shadowed core: resolved to {resolved}, "
f"expected core at {core_path}"
)
assert resolved != shadow_pkg, "import resolved to the shadow copy"
finally:
sys.path[:] = saved
sys.modules.pop("packaging", None)
importlib.invalidate_caches()