mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
test(plugins): cover _discover_all_plugins recursion + cross-link loader
Add a TestDiscoverAllPlugins class covering the six cases the recursive scan needs to handle: - flat plugin uses its manifest ``name:`` as the key - category-namespaced plugin keys off ``<category>/<dirname>`` even when the manifest ``name:`` is bare (regression test for the original bug — ``plugins/observability/langfuse/`` with ``name: langfuse`` must surface as ``observability/langfuse``, not ``langfuse``) - user-installed plugin overrides bundled on key collision - depth cap: anything below ``<root>/<category>/<plugin>/`` is ignored - bundled ``memory/`` and ``context_engine/`` are skipped (they have their own loaders), but user plugins under those category names are still scanned Also add an in-source comment next to the key derivation pointing at the loader's matching line (``PluginManager._parse_manifest`` in plugins.py:1027-1028), so future renames of one site flag the other. Both items raised in Copilot review on #27161. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
21be7025c5
commit
5cbe0b1c4f
2 changed files with 116 additions and 0 deletions
|
|
@ -396,6 +396,117 @@ class TestCmdList:
|
|||
cmd_list()
|
||||
|
||||
|
||||
# ── _discover_all_plugins tests ───────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDiscoverAllPlugins:
|
||||
"""Exercise the recursive scan that powers ``hermes plugins list``.
|
||||
|
||||
Mirrors the layouts the runtime loader handles
|
||||
(:meth:`PluginManager._scan_directory_level`): flat plugins at the root,
|
||||
category-namespaced plugins one level deeper, and user-overrides-bundled
|
||||
on key collision.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _write_plugin(root: Path, segments: list, manifest_name: str = None) -> None:
|
||||
plugin_dir = root
|
||||
for seg in segments:
|
||||
plugin_dir = plugin_dir / seg
|
||||
plugin_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest = {
|
||||
"name": manifest_name or segments[-1],
|
||||
"version": "0.1.0",
|
||||
"description": f"Test plugin {'/'.join(segments)}",
|
||||
}
|
||||
(plugin_dir / "plugin.yaml").write_text(yaml.dump(manifest))
|
||||
|
||||
def _entries_by_key(self, tmp_path, monkeypatch) -> dict:
|
||||
from hermes_cli import plugins_cmd
|
||||
bundled = tmp_path / "bundled"
|
||||
user = tmp_path / "user"
|
||||
bundled.mkdir()
|
||||
user.mkdir()
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.plugins.get_bundled_plugins_dir", lambda: bundled
|
||||
)
|
||||
monkeypatch.setattr(plugins_cmd, "_plugins_dir", lambda: user)
|
||||
return bundled, user, lambda: {
|
||||
e[0]: e for e in plugins_cmd._discover_all_plugins()
|
||||
}
|
||||
|
||||
def test_flat_plugin_uses_manifest_name_as_key(self, tmp_path, monkeypatch):
|
||||
bundled, _, discover = self._entries_by_key(tmp_path, monkeypatch)
|
||||
self._write_plugin(bundled, ["disk-cleanup"])
|
||||
|
||||
entries = discover()
|
||||
assert "disk-cleanup" in entries
|
||||
assert entries["disk-cleanup"][3] == "bundled"
|
||||
|
||||
def test_category_namespaced_plugin_uses_path_derived_key(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""Regression test for the original bug — ``observability/langfuse``
|
||||
and ``image_gen/openai`` must surface under their path-derived key,
|
||||
not vanish because the category directory has no ``plugin.yaml``."""
|
||||
bundled, _, discover = self._entries_by_key(tmp_path, monkeypatch)
|
||||
# langfuse's real manifest declares ``name: langfuse`` (bare), but it
|
||||
# lives under ``observability/`` — the key must reflect the path.
|
||||
self._write_plugin(
|
||||
bundled, ["observability", "langfuse"], manifest_name="langfuse"
|
||||
)
|
||||
self._write_plugin(bundled, ["image_gen", "openai"])
|
||||
|
||||
entries = discover()
|
||||
assert "observability/langfuse" in entries
|
||||
assert "image_gen/openai" in entries
|
||||
# Bare manifest name must NOT leak through as a top-level key.
|
||||
assert "langfuse" not in entries
|
||||
assert "openai" not in entries
|
||||
|
||||
def test_user_overrides_bundled_on_key_collision(self, tmp_path, monkeypatch):
|
||||
bundled, user, discover = self._entries_by_key(tmp_path, monkeypatch)
|
||||
self._write_plugin(bundled, ["observability", "langfuse"])
|
||||
self._write_plugin(user, ["observability", "langfuse"])
|
||||
|
||||
entries = discover()
|
||||
assert entries["observability/langfuse"][3] == "user"
|
||||
|
||||
def test_depth_cap_skips_third_level(self, tmp_path, monkeypatch):
|
||||
"""Anything deeper than ``<root>/<category>/<plugin>/`` is ignored,
|
||||
matching the loader's depth cap."""
|
||||
bundled, _, discover = self._entries_by_key(tmp_path, monkeypatch)
|
||||
# plugins/a/b/c/plugin.yaml — too deep, must NOT be discovered.
|
||||
self._write_plugin(bundled, ["a", "b", "c"])
|
||||
|
||||
entries = discover()
|
||||
assert not any(k.startswith("a/") for k in entries), entries
|
||||
|
||||
def test_bundled_memory_and_context_engine_skipped(self, tmp_path, monkeypatch):
|
||||
"""``plugins/memory/`` and ``plugins/context_engine/`` use their own
|
||||
loaders; bundled entries inside them must not appear in the general
|
||||
list (matches the pre-refactor skip set)."""
|
||||
bundled, _, discover = self._entries_by_key(tmp_path, monkeypatch)
|
||||
self._write_plugin(bundled, ["memory", "honcho"])
|
||||
self._write_plugin(bundled, ["context_engine", "compressor"])
|
||||
self._write_plugin(bundled, ["observability", "langfuse"])
|
||||
|
||||
entries = discover()
|
||||
assert "memory/honcho" not in entries
|
||||
assert "context_engine/compressor" not in entries
|
||||
assert "observability/langfuse" in entries
|
||||
|
||||
def test_user_memory_subdir_is_still_scanned(self, tmp_path, monkeypatch):
|
||||
"""The memory/context_engine skip only applies to *bundled* — a user
|
||||
plugin at ``~/.hermes/plugins/memory/<x>/`` should still be discovered
|
||||
so the user can see what they installed."""
|
||||
bundled, user, discover = self._entries_by_key(tmp_path, monkeypatch)
|
||||
self._write_plugin(user, ["memory", "my-custom-store"])
|
||||
|
||||
entries = discover()
|
||||
assert "memory/my-custom-store" in entries
|
||||
|
||||
|
||||
# ── _copy_example_files tests ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue