hermes-agent/tests/tools/test_lazy_deps_extras_mappi...

243 lines
9.9 KiB
Python

"""LAZY_DEPS maps a feature name to an extra in pyproject.toml.
Two things need a test here.
The map itself: each value must name an extra that pyproject.toml declares.
Nothing else checks that. A typo makes the feature raise FeatureUnavailable
at first use, on the one machine that enabled that backend.
The reader: extra_specs expands a `hermes-agent[x]` reference, and that code
is ours. A cycle or a silent empty result would each ship a
wrong package set. uv resolves the extras its own way and cannot catch a
fault in our reader.
Nothing here restates a version. pyproject.toml holds the specs, uv.lock
pins them, and `uv lock --check` and `uv audit` read the lockfile.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from tools import lazy_deps as ld # noqa: E402
class TestFeatureExtraMapping:
def test_every_feature_maps_to_a_declared_extra(self):
"""A value in LAZY_DEPS must name an extra that pyproject declares.
This is the whole contract of the map. A typo here raises
FeatureUnavailable at first use of that backend, and only on a
machine that enabled it.
"""
declared = set(ld._optional_dependencies())
missing = {
feature: extra
for feature, extra in ld.LAZY_DEPS.items()
if extra not in declared
}
assert not missing, (
f"LAZY_DEPS names extras that pyproject.toml does not declare: "
f"{missing}"
)
def test_every_feature_resolves_to_at_least_one_spec(self):
"""An extra can exist and still be empty after composition.
An empty result installs nothing and reports success, so the backend
stays broken with no error to read.
"""
empty = [f for f in ld.LAZY_DEPS if not ld.feature_specs(f)]
assert not empty, f"features that resolve to no packages: {empty}"
class TestExtraComposition:
"""extra_specs expands `hermes-agent[x]`. That expansion is our code."""
def test_self_references_resolve(self):
"""[messaging] contains [telegram], so its specs must appear."""
composed = set(ld.extra_specs("messaging"))
assert set(ld.extra_specs("telegram")) <= composed
assert not any(s.startswith("hermes-agent[") for s in composed), (
"a self-reference must be expanded, not passed to pip"
)
def test_cycles_terminate(self, monkeypatch):
"""A cycle in the extras must not hang or recurse without end."""
monkeypatch.setattr(ld, "_optional_dependencies", lambda: {
"a": ("hermes-agent[b]",),
"b": ("hermes-agent[a]",),
})
assert ld.extra_specs("a") == ()
def test_unknown_extra_resolves_to_nothing(self, monkeypatch):
monkeypatch.setattr(ld, "_optional_dependencies", lambda: {})
assert ld.extra_specs("nope") == ()
def test_a_marker_on_a_pin_survives_composition(self, monkeypatch):
"""A composed extra must keep the marker that its leaf pin carries.
[wake] contains [wake-tflite], whose pin is macOS-only. Dropping
the marker on the way out installs that package on Linux.
"""
monkeypatch.setattr(ld, "_optional_dependencies", lambda: {
"big": ("hermes-agent[small]", "pkg-c==3.0"),
"small": ("pkg-a==1.0; platform_system == 'Darwin'",),
})
assert set(ld.extra_specs("big")) == {
"pkg-a==1.0; platform_system == 'Darwin'",
"pkg-c==3.0",
}
class TestAnchorSpec:
"""The anchor identifies an extra. A shared helper cannot do that.
active_features() seeds its record from the anchors, so a wrong anchor
turns `hermes update` into an installer for backends the user never
enabled. The regression this guards: [voice] listed
`hermes-agent[audio-io]` first, expansion put sounddevice at index 0,
and one sounddevice install marked every audio feature active.
"""
def test_the_anchor_is_the_first_direct_pin(self, monkeypatch):
monkeypatch.setattr(ld, "_optional_dependencies", lambda: {
"voice": ("hermes-agent[shared]", "the-real-engine==1.0"),
"shared": ("helper==1.0",),
})
assert ld._anchor_spec("voice") == "the-real-engine==1.0"
def test_a_reference_only_extra_recurses(self, monkeypatch):
monkeypatch.setattr(ld, "_optional_dependencies", lambda: {
"outer": ("hermes-agent[inner]",),
"inner": ("real-pkg==1.0",),
})
assert ld._anchor_spec("outer") == "real-pkg==1.0"
def test_a_cycle_returns_none(self, monkeypatch):
monkeypatch.setattr(ld, "_optional_dependencies", lambda: {
"a": ("hermes-agent[b]",),
"b": ("hermes-agent[a]",),
})
assert ld._anchor_spec("a") is None
def test_every_feature_has_an_anchor(self):
missing = [
f for f in ld.LAZY_DEPS if ld._anchor_spec(ld.LAZY_DEPS[f]) is None
]
assert not missing, f"features with no anchor: {missing}"
def test_anchors_identify_their_extras(self):
"""Two features with different extras must have different anchors.
Two features that map to one extra (stt.mistral and tts.mistral)
share an anchor by design: one install serves both. Across extras,
a shared anchor means presence of one package activates a feature
the user never enabled.
"""
anchor_to_extra: dict[str, str] = {}
for extra in set(ld.LAZY_DEPS.values()):
anchor = ld._anchor_spec(extra)
assert anchor is not None
name = ld._pkg_name_from_spec(anchor)
other = anchor_to_extra.get(name)
assert other is None or other == extra, (
f"extras [{extra}] and [{other}] share the anchor package "
f"{name!r} — active_features cannot tell them apart. Put a "
f"distinctive pin first in each extra."
)
anchor_to_extra[name] = extra
class TestWheelInstallFallsBackToDistMetadata:
"""A wheel install (Nix) has no pyproject.toml beside the code.
The extras table must come from the installed dist metadata there.
Without the fallback, every lazy_deps entry point raised on Nix —
including ensure() on a feature whose packages were baked into the
sealed venv via extraDependencyGroups, which must be a no-op.
"""
@pytest.fixture
def no_pyproject(self, monkeypatch):
"""Simulate the wheel layout: no project root on disk."""
monkeypatch.setattr(ld, "_project_root", lambda: None)
ld._pyproject.cache_clear()
ld._metadata_optional_dependencies.cache_clear()
yield
ld._pyproject.cache_clear()
ld._metadata_optional_dependencies.cache_clear()
def test_the_extras_table_comes_from_metadata(self, no_pyproject):
"""The test venv has hermes-agent installed, so the real metadata
path resolves the real table.
Compared against the metadata's own Provides-Extra, not against
LAZY_DEPS: a dev venv's installed metadata lags the checkout when
pyproject gained an extra since the last sync, and that lag is not
a fault in the reader. On a real wheel install the two match.
"""
table = ld._optional_dependencies()
assert table, "dist metadata produced no extras table"
from importlib.metadata import metadata
provided = set(metadata("hermes-agent").get_all("Provides-Extra") or [])
known = provided & set(ld.LAZY_DEPS.values())
assert known, "no LAZY_DEPS extra appears in the installed metadata"
missing = {e for e in known if e not in table}
assert not missing, (
f"extras in Provides-Extra but absent from the parsed table: {missing}"
)
def test_a_baked_feature_is_a_noop_for_ensure(self, no_pyproject, monkeypatch):
"""The exact Nix regression: deps baked, ensure() must not raise."""
monkeypatch.setattr(ld, "_is_satisfied", lambda spec: True)
monkeypatch.setattr(
ld, "_venv_pip_install",
lambda *a, **kw: pytest.fail("nothing to install"),
)
ld.ensure("provider.anthropic", prompt=False) # must not raise
assert ld.is_available("provider.anthropic") is True
def test_a_marker_survives_the_metadata_round_trip(self, no_pyproject):
"""setuptools ANDs `extra == "x"` onto a pin's own marker; the
reader must strip the extra clause and keep the platform half."""
specs = ld.extra_specs("wake-tflite")
assert len(specs) == 1
req = ld._parse_spec(specs[0])
assert req is not None and req.marker is not None
assert "platform_system" in str(req.marker)
assert "extra" not in str(req.marker)
def test_composition_still_expands(self, no_pyproject):
"""Self-references survive the metadata round trip too."""
composed = set(ld.extra_specs("messaging"))
assert set(ld.extra_specs("telegram")) <= composed
assert not any(s.startswith("hermes-agent[") for s in composed)
class TestEntryPointsNeverRaise:
"""is_available and feature_install_command are used in status paths
with no try/except (wake_word.py); their contracts are bool / Optional.
"""
@pytest.fixture
def specs_unreadable(self, monkeypatch):
monkeypatch.setattr(ld, "_project_root", lambda: None)
monkeypatch.setattr(ld, "_metadata_optional_dependencies", lambda: {})
ld._pyproject.cache_clear()
yield
ld._pyproject.cache_clear()
def test_is_available_returns_false(self, specs_unreadable):
assert ld.is_available("provider.anthropic") is False
def test_feature_install_command_returns_none(self, specs_unreadable):
assert ld.feature_install_command("provider.anthropic") is None