test(plugins): activation-contract coverage for entry-point classification
Documents and tests the routing contract the sweeper review asked about: classification records the manifest but does not activate anything. - model-provider test now exercises providers.get_provider_profile() against the pip-only name (None today — providers discovery is directory-based) and asserts the module never leaks into sys.modules via that path. - new test for the mnemosyne shape: a pip entry point duplicating a same-name directory provider. The pip copy is classified exclusive and never imported; the directory copy still activates through plugins.memory discovery, exactly once. - _classify_entrypoint_kind docstring now states the activation contract explicitly: pip-only providers were equally unactivatable pre-change (both destination systems are directory-only; the hermes_agent.memory_providers entry-point group has no consumers), so classification only removes the wasted import. Entry-point activation is tracked upstream (#40644 for memory); this change is its prerequisite, preventing double import once it lands.
This commit is contained in:
parent
450bd0930a
commit
a883977b12
|
|
@ -4218,6 +4218,21 @@ class PluginManager:
|
|||
of its parent packages (see ``_resolve_module_source``); only
|
||||
the first 8192 chars are scanned, mirroring the directory-plugin
|
||||
heuristic. Unresolvable or non-Python modules stay ``standalone``.
|
||||
|
||||
Activation contract: this method only decides whether the general
|
||||
manager imports the module — it does not activate anything.
|
||||
Memory and model providers activate through their own systems
|
||||
(``memory.provider`` config via ``plugins/memory`` directory
|
||||
discovery; ``providers/`` lazy directory discovery). Both are
|
||||
directory-based today, so a pip-only provider is recorded for
|
||||
introspection but not activatable until those systems gain
|
||||
entry-point discovery (tracked for memory: #40644). That is not
|
||||
a regression: pre-change such a provider was equally
|
||||
unactivatable — it was merely imported first, at full cost
|
||||
(e.g. fastembed -> onnxruntime), and logged
|
||||
``no register() function``. Classification removes the cost
|
||||
without changing the activation surface, and is the prerequisite
|
||||
that prevents double-import once entry-point activation lands.
|
||||
"""
|
||||
try:
|
||||
module_name = ep.value.split(":", 1)[0].strip()
|
||||
|
|
|
|||
|
|
@ -576,6 +576,100 @@ class TestPluginLoading:
|
|||
)
|
||||
assert entry.module is None
|
||||
assert "fakeprovider" not in sys.modules
|
||||
# Routing contract: classification records the manifest but does
|
||||
# not fabricate activation. providers/ discovery is directory-based
|
||||
# today, so a pip-only provider is not activatable via
|
||||
# get_provider_profile() — and it must not leak into sys.modules
|
||||
# through the providers path either (no double import).
|
||||
from providers import get_provider_profile
|
||||
|
||||
assert get_provider_profile("fakeprovider") is None
|
||||
assert "fakeprovider" not in sys.modules
|
||||
|
||||
def test_entrypoint_duplicate_does_not_block_directory_provider_activation(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""The mnemosyne shape: a pip entry point duplicating a same-name
|
||||
directory provider.
|
||||
|
||||
The pip copy is classified ``exclusive`` (recorded, never
|
||||
imported); the directory copy must still activate through
|
||||
memory-provider discovery, exactly once. Classification must not
|
||||
interfere with the real activation path.
|
||||
"""
|
||||
from importlib.metadata import EntryPoint
|
||||
from types import SimpleNamespace
|
||||
|
||||
# Same-name pip entry point (the duplicate).
|
||||
ep_dir = tmp_path / "ep_modules"
|
||||
ep_dir.mkdir()
|
||||
(ep_dir / "mempalace_dup.py").write_text(
|
||||
"class MemPalaceProvider:\n"
|
||||
" pass\n"
|
||||
"def register_memory_provider(name, cls):\n"
|
||||
" pass\n"
|
||||
)
|
||||
monkeypatch.syspath_prepend(str(ep_dir))
|
||||
ep = EntryPoint(
|
||||
name="mempalace_dup",
|
||||
value="mempalace_dup:register",
|
||||
group=ENTRY_POINTS_GROUP,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.plugins.importlib.metadata.entry_points",
|
||||
lambda: SimpleNamespace(
|
||||
select=lambda group: [ep] if group == ENTRY_POINTS_GROUP else []
|
||||
),
|
||||
)
|
||||
|
||||
# Same-name directory provider under $HERMES_HOME/plugins/.
|
||||
hermes_home = tmp_path / "hermes_test"
|
||||
plugins_dir = hermes_home / "plugins"
|
||||
provider_dir = plugins_dir / "mempalace_dup"
|
||||
provider_dir.mkdir(parents=True)
|
||||
(provider_dir / "__init__.py").write_text(
|
||||
"from agent.memory_provider import MemoryProvider\n"
|
||||
"class MyProvider(MemoryProvider):\n"
|
||||
" @property\n"
|
||||
" def name(self): return 'mempalace_dup'\n"
|
||||
" def is_available(self): return True\n"
|
||||
" def initialize(self, **kw): pass\n"
|
||||
" def sync_turn(self, *a, **kw): pass\n"
|
||||
" def get_tool_schemas(self): return []\n"
|
||||
" def handle_tool_call(self, *a, **kw): return '{}'\n"
|
||||
)
|
||||
(provider_dir / "plugin.yaml").write_text(
|
||||
"name: mempalace_dup\ndescription: dup\n"
|
||||
)
|
||||
(hermes_home / "config.yaml").write_text(
|
||||
yaml.safe_dump({"plugins": {"enabled": ["mempalace_dup"]}})
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setattr(
|
||||
"plugins.memory._get_user_plugins_dir", lambda: plugins_dir
|
||||
)
|
||||
|
||||
mgr = PluginManager()
|
||||
mgr.discover_and_load()
|
||||
|
||||
# Pip duplicate: classified exclusive, recorded, never imported.
|
||||
entry = mgr._plugins["mempalace_dup"]
|
||||
assert entry.manifest.kind == "exclusive", (
|
||||
f"Expected auto-coerced kind='exclusive', got {entry.manifest.kind}"
|
||||
)
|
||||
assert entry.module is None
|
||||
assert "mempalace_dup" not in sys.modules
|
||||
|
||||
# Directory copy still activates through memory-provider discovery,
|
||||
# exactly once.
|
||||
from plugins.memory import discover_memory_providers, load_memory_provider
|
||||
|
||||
names = [n for n, _, _ in discover_memory_providers()]
|
||||
assert names.count("mempalace_dup") == 1
|
||||
p = load_memory_provider("mempalace_dup")
|
||||
assert p is not None
|
||||
assert p.name == "mempalace_dup"
|
||||
assert p.is_available()
|
||||
|
||||
def test_entrypoint_dotted_name_never_imports_parent_package(
|
||||
self, tmp_path, monkeypatch
|
||||
|
|
|
|||
Loading…
Reference in New Issue