From a883977b125d4d780fc9d837b8a5e3515b328afc Mon Sep 17 00:00:00 2001 From: Mike Smith Date: Sun, 2 Aug 2026 00:25:15 -0400 Subject: [PATCH] test(plugins): activation-contract coverage for entry-point classification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- hermes_cli/plugins.py | 15 +++++ tests/hermes_cli/test_plugins.py | 94 ++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index 06e0b3aa3562b..6823a9c11819e 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -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() diff --git a/tests/hermes_cli/test_plugins.py b/tests/hermes_cli/test_plugins.py index 15352fb87a022..608cbc4b21121 100644 --- a/tests/hermes_cli/test_plugins.py +++ b/tests/hermes_cli/test_plugins.py @@ -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