hermes-agent/tests/hermes_cli/test_plugin_scanner_recursion.py
alt-glitch a1e667b9f2 fix(restructure): fix test regressions from import rewrite
Fix variable name breakage (run_agent, hermes_constants, etc.) where
import rewriter changed 'import X' to 'import hermes_agent.Y' but
test code still referenced 'X' as a variable name.

Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where
single files became directories.

Fix hardcoded file paths in tests pointing to old locations.
Fix tool registry to discover tools in subpackage directories.
Fix stale import in hermes_agent/tools/__init__.py.

Part of #14182, #14183
2026-04-23 12:05:10 +05:30

357 lines
13 KiB
Python

"""Tests for PR1 pluggable image gen: scanner recursion, kinds, path keys.
Covers ``_scan_directory`` recursion into category namespaces
(``plugins/image_gen/openai/``), ``kind`` parsing, path-derived registry
keys, and the new gate logic (bundled backends auto-load; user backends
still opt-in; exclusive kind skipped; unknown kinds → standalone warning).
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
import pytest
import yaml
from hermes_agent.cli.plugins import PluginManager, PluginManifest
# ── Helpers ────────────────────────────────────────────────────────────────
def _write_plugin(
root: Path,
segments: list[str],
*,
manifest_extra: Dict[str, Any] | None = None,
register_body: str = "pass",
) -> Path:
"""Create a plugin dir at ``root/<segments...>/`` with plugin.yaml + __init__.py.
``segments`` lets tests build both flat (``["my-plugin"]``) and
category-namespaced (``["image_gen", "openai"]``) layouts.
"""
plugin_dir = root
for seg in segments:
plugin_dir = plugin_dir / seg
plugin_dir.mkdir(parents=True, exist_ok=True)
manifest = {
"name": segments[-1],
"version": "0.1.0",
"description": f"Test plugin {'/'.join(segments)}",
}
if manifest_extra:
manifest.update(manifest_extra)
(plugin_dir / "plugin.yaml").write_text(yaml.dump(manifest))
(plugin_dir / "__init__.py").write_text(
f"def register(ctx):\n {register_body}\n"
)
return plugin_dir
def _enable(hermes_home: Path, name: str) -> None:
"""Append ``name`` to ``plugins.enabled`` in ``<hermes_home>/config.yaml``."""
cfg_path = hermes_home / "config.yaml"
cfg: dict = {}
if cfg_path.exists():
try:
cfg = yaml.safe_load(cfg_path.read_text()) or {}
except Exception:
cfg = {}
plugins_cfg = cfg.setdefault("plugins", {})
enabled = plugins_cfg.setdefault("enabled", [])
if isinstance(enabled, list) and name not in enabled:
enabled.append(name)
cfg_path.write_text(yaml.safe_dump(cfg))
# ── Scanner recursion ──────────────────────────────────────────────────────
class TestCategoryNamespaceRecursion:
def test_category_namespace_discovered(self, tmp_path, monkeypatch):
"""``<root>/image_gen/openai/plugin.yaml`` is discovered with key
``image_gen/openai`` when the ``image_gen`` parent has no manifest."""
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
user_plugins = hermes_home / "plugins"
_write_plugin(user_plugins, ["image_gen", "openai"])
_enable(hermes_home, "image_gen/openai")
mgr = PluginManager()
mgr.discover_and_load()
assert "image_gen/openai" in mgr._plugins
loaded = mgr._plugins["image_gen/openai"]
assert loaded.manifest.key == "image_gen/openai"
assert loaded.manifest.name == "openai"
assert loaded.enabled is True
def test_flat_plugin_key_matches_name(self, tmp_path, monkeypatch):
"""Flat plugins keep their bare name as the key (back-compat)."""
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
user_plugins = hermes_home / "plugins"
_write_plugin(user_plugins, ["my-plugin"])
_enable(hermes_home, "my-plugin")
mgr = PluginManager()
mgr.discover_and_load()
assert "my-plugin" in mgr._plugins
assert mgr._plugins["my-plugin"].manifest.key == "my-plugin"
def test_depth_cap_two(self, tmp_path, monkeypatch):
"""Plugins nested three levels deep are not discovered.
``<root>/a/b/c/plugin.yaml`` should NOT be picked up — cap is
two segments.
"""
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
user_plugins = hermes_home / "plugins"
_write_plugin(user_plugins, ["a", "b", "c"])
mgr = PluginManager()
mgr.discover_and_load()
non_bundled = [
k for k, p in mgr._plugins.items()
if p.manifest.source != "bundled"
]
assert non_bundled == []
def test_category_dir_with_manifest_is_leaf(self, tmp_path, monkeypatch):
"""If ``image_gen/plugin.yaml`` exists, ``image_gen`` itself IS the
plugin and its children are ignored."""
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
user_plugins = hermes_home / "plugins"
# parent has a manifest → stop recursing
_write_plugin(user_plugins, ["image_gen"])
# child also has a manifest — should NOT be found because we stop
# at the parent.
_write_plugin(user_plugins, ["image_gen", "openai"])
_enable(hermes_home, "image_gen")
_enable(hermes_home, "image_gen/openai")
mgr = PluginManager()
mgr.discover_and_load()
# The bundled plugins/image_gen/openai/ exists in the repo — filter
# it out so we're only asserting on the user-dir layout.
user_plugins_in_registry = {
k for k, p in mgr._plugins.items() if p.manifest.source != "bundled"
}
assert "image_gen" in user_plugins_in_registry
assert "image_gen/openai" not in user_plugins_in_registry
# ── Kind parsing ───────────────────────────────────────────────────────────
class TestKindField:
def test_default_kind_is_standalone(self, tmp_path, monkeypatch):
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
_write_plugin(hermes_home / "plugins", ["p1"])
_enable(hermes_home, "p1")
mgr = PluginManager()
mgr.discover_and_load()
assert mgr._plugins["p1"].manifest.kind == "standalone"
@pytest.mark.parametrize("kind", ["backend", "exclusive", "standalone"])
def test_valid_kinds_parsed(self, kind, tmp_path, monkeypatch):
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
_write_plugin(
hermes_home / "plugins",
["p1"],
manifest_extra={"kind": kind},
)
# Not all kinds auto-load, but manifest should parse.
_enable(hermes_home, "p1")
mgr = PluginManager()
mgr.discover_and_load()
assert "p1" in mgr._plugins
assert mgr._plugins["p1"].manifest.kind == kind
def test_unknown_kind_falls_back_to_standalone(self, tmp_path, monkeypatch, caplog):
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
_write_plugin(
hermes_home / "plugins",
["p1"],
manifest_extra={"kind": "bogus"},
)
_enable(hermes_home, "p1")
with caplog.at_level("WARNING"):
mgr = PluginManager()
mgr.discover_and_load()
assert mgr._plugins["p1"].manifest.kind == "standalone"
assert any(
"unknown kind" in rec.getMessage() for rec in caplog.records
)
# ── Gate logic ─────────────────────────────────────────────────────────────
class TestBackendGate:
def test_user_backend_still_gated_by_enabled(self, tmp_path, monkeypatch):
"""User-installed ``kind: backend`` plugins still require opt-in —
they're not trusted by default."""
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
user_plugins = hermes_home / "plugins"
_write_plugin(
user_plugins,
["image_gen", "fancy"],
manifest_extra={"kind": "backend"},
)
# Do NOT opt in.
mgr = PluginManager()
mgr.discover_and_load()
loaded = mgr._plugins["image_gen/fancy"]
assert loaded.enabled is False
assert "not enabled" in (loaded.error or "")
def test_user_backend_loads_when_enabled(self, tmp_path, monkeypatch):
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
user_plugins = hermes_home / "plugins"
_write_plugin(
user_plugins,
["image_gen", "fancy"],
manifest_extra={"kind": "backend"},
)
_enable(hermes_home, "image_gen/fancy")
mgr = PluginManager()
mgr.discover_and_load()
assert mgr._plugins["image_gen/fancy"].enabled is True
def test_exclusive_kind_skipped(self, tmp_path, monkeypatch):
"""``kind: exclusive`` plugins are recorded but not loaded — the
category's own discovery system handles them (memory today)."""
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
_write_plugin(
hermes_home / "plugins",
["some-backend"],
manifest_extra={"kind": "exclusive"},
)
_enable(hermes_home, "some-backend")
mgr = PluginManager()
mgr.discover_and_load()
loaded = mgr._plugins["some-backend"]
assert loaded.enabled is False
assert "exclusive" in (loaded.error or "")
# ── Bundled backend auto-load (integration with real bundled plugin) ────────
class TestBundledBackendAutoLoad:
def test_bundled_image_gen_openai_autoloads(self, tmp_path, monkeypatch):
"""The bundled ``plugins/image_gen/openai/`` plugin loads without
any opt-in — it's ``kind: backend`` and shipped in-repo."""
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
mgr = PluginManager()
mgr.discover_and_load()
assert "image_gen/openai" in mgr._plugins
loaded = mgr._plugins["image_gen/openai"]
assert loaded.manifest.source == "bundled"
assert loaded.manifest.kind == "backend"
assert loaded.enabled is True, f"error: {loaded.error}"
# ── PluginContext.register_image_gen_provider ───────────────────────────────
class TestRegisterImageGenProvider:
def test_accepts_valid_provider(self, tmp_path, monkeypatch):
from hermes_agent.agent.image_gen import registry as image_gen_registry
from hermes_agent.agent.image_gen.provider import ImageGenProvider
image_gen_registry._reset_for_tests()
class FakeProvider(ImageGenProvider):
@property
def name(self) -> str:
return "fake-test"
def generate(self, prompt, aspect_ratio="landscape", **kw):
return {"success": True, "image": "test://fake"}
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
plugin_dir = _write_plugin(
hermes_home / "plugins",
["my-img-plugin"],
register_body=(
"from agent.image_gen_provider import ImageGenProvider\n"
" class P(ImageGenProvider):\n"
" @property\n"
" def name(self): return 'fake-ctx'\n"
" def generate(self, prompt, aspect_ratio='landscape', **kw):\n"
" return {'success': True, 'image': 'x://y'}\n"
" ctx.register_image_gen_provider(P())"
),
)
_enable(hermes_home, "my-img-plugin")
mgr = PluginManager()
mgr.discover_and_load()
assert mgr._plugins["my-img-plugin"].enabled is True
assert image_gen_registry.get_provider("fake-ctx") is not None
image_gen_registry._reset_for_tests()
def test_rejects_non_provider(self, tmp_path, monkeypatch, caplog):
from hermes_agent.agent.image_gen import registry as image_gen_registry
image_gen_registry._reset_for_tests()
import os
hermes_home = Path(os.environ["HERMES_HOME"]) # set by hermetic conftest fixture
_write_plugin(
hermes_home / "plugins",
["bad-img-plugin"],
register_body="ctx.register_image_gen_provider('not a provider')",
)
_enable(hermes_home, "bad-img-plugin")
with caplog.at_level("WARNING"):
mgr = PluginManager()
mgr.discover_and_load()
# Plugin loaded (register returned normally) but nothing was
# registered in the provider registry.
assert mgr._plugins["bad-img-plugin"].enabled is True
assert image_gen_registry.get_provider("not a provider") is None
image_gen_registry._reset_for_tests()