Something went wrong. Try again.
The agent that grows with you
Something went wrong. Try again.
13 kB · 357 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358"""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 registrykeys, and the new gate logic (bundled backends auto-load; user backendsstill opt-in; exclusive kind skipped; unknown kinds → standalone warning)."""
from __future__ import annotations
from pathlib import Pathfrom typing import Any, Dict
import pytestimport yaml
from hermes_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 agent import image_gen_registry from 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 agent import 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()