Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions tests/tools/test_lazy_deps_managed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Managed-install guard in :func:`tools.lazy_deps.ensure` (#48628).

A package-manager install (NixOS, and anything else shipping Hermes from a
read-only store) cannot receive lazy pip installs: the venv's site-packages
lives in the store, so the uv -> pip -> ensurepip ladder burns ~15s
bootstrapping ensurepip only to fail. ``ensure()`` must fail fast instead.
"""

import pytest

from tools import lazy_deps
from tools.lazy_deps import FeatureUnavailable


FEATURE = "provider.anthropic"


@pytest.fixture(autouse=True)
def _missing_and_installable(monkeypatch):
"""Reach the guard: deps missing, installs allowed, no durable target.

``_allow_lazy_installs`` is patched explicitly so the suite does not
depend on the host's ~/.hermes/config.yaml (a local
``allow_lazy_installs: false`` otherwise short-circuits with a different
rejection reason).
"""
monkeypatch.setattr(lazy_deps, "feature_missing", lambda _f: ("some-pkg==1.0",))
monkeypatch.setattr(lazy_deps, "_allow_lazy_installs", lambda: True)
monkeypatch.setattr(lazy_deps, "_lazy_install_target", lambda: None)


def _no_installer(monkeypatch):
"""Fail loudly if the guard lets execution reach the install ladder."""
def _boom(*_a, **_kw):
raise AssertionError("guard let execution reach the install ladder")

monkeypatch.setattr(lazy_deps.subprocess, "run", _boom)


def test_nixos_install_fails_fast_without_touching_the_installer(monkeypatch):
monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: "nixos")
_no_installer(monkeypatch)

with pytest.raises(FeatureUnavailable) as excinfo:
lazy_deps.ensure(FEATURE, prompt=False)

assert "nixos" in excinfo.value.reason
# refresh_active_features classifies by this prefix — anything else is
# reported to the user as a hard failure instead of a skip.
assert excinfo.value.reason.startswith("unsupported ")


def test_reason_is_classified_as_skipped_not_failed(monkeypatch):
"""The wording contract with refresh_active_features, pinned directly."""
monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: "nixos")

with pytest.raises(FeatureUnavailable) as excinfo:
lazy_deps.ensure(FEATURE, prompt=False)

assert excinfo.value.reason.startswith("unsupported "), (
"refresh_active_features would report this as failed: rather than skipped:"
)


def test_unmanaged_install_is_not_blocked_by_the_guard(monkeypatch):
"""On a normal pip install the guard must be transparent."""
monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: None)

with pytest.raises(FeatureUnavailable) as excinfo:
lazy_deps.ensure(FEATURE, prompt=False)

# Whatever stops the install here, it must NOT be the managed guard.
assert "managed installs" not in excinfo.value.reason


def test_durable_install_target_overrides_the_guard(monkeypatch, tmp_path):
"""The container deployment sets HERMES_MANAGED *and* a writable target.

Dockerfile sets HERMES_LAZY_INSTALL_TARGET and the NixOS container module
passes HERMES_MANAGED=true; blocking there would break that deployment.
"""
monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: "nixos")
monkeypatch.setattr(lazy_deps, "_lazy_install_target", lambda: tmp_path)

with pytest.raises(FeatureUnavailable) as excinfo:
lazy_deps.ensure(FEATURE, prompt=False)

assert "nixos" not in excinfo.value.reason.lower(), (
"durable-target installs must not be blocked by the managed guard"
)


def test_platform_unsupported_takes_precedence(monkeypatch):
"""A platform-specific reason is more actionable than 'managed install'.

Also required for consistency: refresh_active_features pre-checks
_unsupported_feature_reason before calling ensure().
"""
monkeypatch.setattr("hermes_cli.config.get_managed_system", lambda: "nixos")
monkeypatch.setattr(
lazy_deps, "_unsupported_feature_reason", lambda _f: "unsupported on win32"
)

with pytest.raises(FeatureUnavailable) as excinfo:
lazy_deps.ensure(FEATURE, prompt=False)

assert excinfo.value.reason == "unsupported on win32"


def test_unreadable_config_fails_open(monkeypatch):
"""A broken config must not block installs on a normal pip install."""
def _raise():
raise RuntimeError("config unreadable")

monkeypatch.setattr("hermes_cli.config.get_managed_system", _raise)

with pytest.raises(FeatureUnavailable) as excinfo:
lazy_deps.ensure(FEATURE, prompt=False)

assert "managed" not in excinfo.value.reason.lower()
29 changes: 29 additions & 0 deletions tools/lazy_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,35 @@ def ensure(feature: str, *, prompt: bool = True) -> None:
if unsupported:
raise FeatureUnavailable(feature, missing, unsupported)

# Package-manager installs (NixOS, and any other distro that ships Hermes
# from a read-only store) cannot receive lazy pip installs: the venv's
# site-packages lives in the store, so the uv -> pip -> ensurepip ladder
# below burns ~15s bootstrapping ensurepip only to fail on a read-only
# target. Fail fast with an actionable message instead.
#
# Skipped when a durable install target is configured: the container
# deployment sets HERMES_MANAGED=true *and* HERMES_LAZY_INSTALL_TARGET
# (a writable volume), where lazy installs legitimately work.
#
# The reason string starts with "unsupported " on purpose:
# refresh_active_features classifies FeatureUnavailable by that prefix and
# reports anything else as a hard failure rather than a skip.
if _lazy_install_target() is None:
try:
from hermes_cli.config import get_managed_system

managed_by = get_managed_system()
except Exception:
managed_by = "" # config unreadable — proceed with the install
if managed_by:
raise FeatureUnavailable(
feature, missing,
f"unsupported on {managed_by}-managed installs: this build's "
f"packages come from {managed_by}, so Hermes cannot install "
f"them at runtime. Add the dependencies for {feature!r} via "
f"{managed_by} (or run a pip/uv install of Hermes instead)."
)

# Validate every spec against the allowlist + safety regex. Belt and
# braces — the keys-in-LAZY_DEPS check above already constrains this.
for spec in missing:
Expand Down
Loading