Lazy installs OFF is now a real contract, not just 'refuse
everything': the bundle's EXACT extras list is the frozen feature set
(settled 2026-09-02 plan, task 5).
- pm/features.py: write_features()/read_features() over
enabled-features.json at the payload root (bundle-written, beside
manifest.json; at runtime store_root().parent — the same relative
location on both install kinds). installed_extras() records what
`uv sync --all-extras` ACTUALLY installed on the target: every
declared extra whose pm anchor resolves in the staged venv —
marker-gated extras show up as missing anchors, the honest
per-platform record.
- cmd_bundle: after the staged venv sync, write the features file into
the payload ('✓ enabled-features.json (N extras recorded)').
- sync_venv: when security.allow_lazy_installs is false and the file
exists, requested extras OUTSIDE the frozen set are refused with a
message naming the policy.
- Venv.apply: plugin members + lazy-off + frozen file = loud InstallError
(the bundle IS the install; never union plugin deps into it).
tests/pm/test_features.py: 7 tests (roundtrip, absent/garbage reads,
payload-root path, anchor-truth installed_extras, frozen refusal,
in-set pass). tests/pm: 162 passed 0 failed.
112 lines
3.3 KiB
Python
112 lines
3.3 KiB
Python
"""pm.features: the frozen bundle feature set (enabled-features.json).
|
|
|
|
Lazy installs OFF = the bundle's feature list is FROZEN to the file the
|
|
bundle wrote (the EXACT extras that installed on that target); pm sync
|
|
never deviates and never installs a plugin member.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import pm.features as feats
|
|
|
|
|
|
@pytest.fixture
|
|
def rooted(tmp_path, monkeypatch):
|
|
"""Point features_path at a temp runtime dir (store_root().parent)."""
|
|
store = tmp_path / "tools"
|
|
store.mkdir()
|
|
monkeypatch.setattr(
|
|
feats,
|
|
"features_path",
|
|
lambda base=None: (base if base is not None else store.parent)
|
|
/ feats.FEATURES_FILENAME,
|
|
)
|
|
return tmp_path
|
|
|
|
|
|
def test_write_then_read_roundtrip(rooted):
|
|
path = feats.write_features(["web", "acp", "web"])
|
|
assert path.is_file()
|
|
got = feats.read_features()
|
|
assert got == ["acp", "web"] # sorted, deduped
|
|
|
|
|
|
def test_read_features_none_when_absent(rooted):
|
|
assert feats.read_features() is None
|
|
|
|
|
|
def test_read_features_none_on_garbage(rooted):
|
|
feats.features_path().write_text("{ not json", encoding="utf-8")
|
|
assert feats.read_features() is None
|
|
|
|
|
|
def test_features_path_in_bundle_uses_payload_root(rooted):
|
|
payload = rooted / "payload"
|
|
payload.mkdir()
|
|
assert feats.features_path(payload) == payload / "enabled-features.json"
|
|
|
|
|
|
def test_installed_extras_reports_only_anchor_resolved(tmp_path, monkeypatch):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
(repo / "pyproject.toml").write_text(
|
|
"[project]\n"
|
|
'name = "hermes-agent"\n'
|
|
"[project.optional-dependencies]\n"
|
|
'present = ["x"]\n'
|
|
'absent = ["y"]\n',
|
|
encoding="utf-8",
|
|
)
|
|
venv = tmp_path / "venv"
|
|
site = venv / "Lib" / "site-packages"
|
|
site.mkdir(parents=True)
|
|
(site / "somepkg.py").write_text("x = 1\n", encoding="utf-8")
|
|
|
|
import pm.extras as extras_mod
|
|
|
|
monkeypatch.setattr(
|
|
extras_mod,
|
|
"ANCHORS",
|
|
{**extras_mod.ANCHORS, "present": "somepkg", "absent": "missingmod"},
|
|
)
|
|
got = feats.installed_extras(repo, venv)
|
|
assert "present" in got
|
|
assert "absent" not in got
|
|
|
|
|
|
def test_sync_venv_refuses_outside_frozen_extras(rooted, monkeypatch):
|
|
feats.write_features(["web", "acp"])
|
|
|
|
import sys
|
|
|
|
ensure_mod = sys.modules["pm.ensure"]
|
|
from pm.package import InstallError
|
|
|
|
monkeypatch.setattr(ensure_mod, "lazy_installs_allowed", lambda: False)
|
|
with pytest.raises(InstallError) as exc:
|
|
ensure_mod.sync_venv(["slack"], explicit=True)
|
|
assert "frozen" in str(exc.value) or "outside" in str(exc.value)
|
|
|
|
|
|
def test_sync_venv_allows_frozen_extras_when_lazy_off(rooted, monkeypatch):
|
|
feats.write_features(["web"])
|
|
|
|
import sys
|
|
|
|
ensure_mod = sys.modules["pm.ensure"]
|
|
|
|
# lazy off, request within the frozen set: passes the gate (may still
|
|
# no-op on the stamp — we only assert no refusal here, by making the
|
|
# stamp match so sync_venv returns early)
|
|
monkeypatch.setattr(ensure_mod, "lazy_installs_allowed", lambda: False)
|
|
venv_pkg = ensure_mod.get_package("venv")
|
|
monkeypatch.setattr(
|
|
venv_pkg, "expected_stamp", lambda extras: "stamp"
|
|
)
|
|
monkeypatch.setattr(ensure_mod, "_facts", lambda: {"venv": {"stamp": "stamp", "extras": ["web"]}})
|
|
ensure_mod.sync_venv(["web"]) # no raise
|