Files
hermes-agent/tests/plugins/test_memory_hook_registration.py
Teknium 5bd439d3ed refactor(plugins): own dual-kind hook fallback in the ledger mixin
Move the general-vs-memory hook ownership logic out of the memory collector into
PluginLedgerMixin (_drop_fallback_hooks / _register_fallback_hook) so the collector
and the loader each call one manager method instead of reaching into manager privates.
Hoist hashlib to module scope. Trim the new suite to the three invariant cases
(run-once across load orders, distinct sources not suppressed, re-exported register).
2026-09-06 13:36:12 -07:00

115 lines
4.3 KiB
Python

"""Exercise dual-kind plugins through the real general and memory loaders."""
import textwrap
import pytest
from hermes_cli.plugins import get_plugin_manager
from plugins.memory import load_memory_provider
def _install(home, monkeypatch, *, label="first", enabled=True):
home.mkdir(parents=True, exist_ok=True)
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setenv("HERMES_BUNDLED_PLUGINS", str(home / "empty"))
monkeypatch.delenv("HERMES_ENABLE_PROJECT_PLUGINS", raising=False)
monkeypatch.chdir(home)
(home / "config.yaml").write_text(
f"plugins:\n enabled: {'[dual]' if enabled else '[]'}\nmemory:\n provider: dual\n"
)
plugin = home / "plugins" / "dual"
plugin.mkdir(parents=True)
(plugin / "plugin.yaml").write_text("name: dual\nversion: 1.0.0\nkind: standalone\n")
(plugin / "values.py").write_text(f"LABEL = {label!r}\n")
(plugin / "__init__.py").write_text(textwrap.dedent('''\
from agent.memory_provider import MemoryProvider
from .values import LABEL
class Provider(MemoryProvider):
name = "dual"
def is_available(self): return True
def initialize(self, session_id, **kwargs): pass
def get_tool_schemas(self): return []
def make_hook(label):
def callback(**kwargs):
return {"context": label}
return callback
def register(ctx):
ctx.register_memory_provider(Provider())
ctx.register_hook("pre_llm_call", make_hook(LABEL))
ctx.register_hook("pre_llm_call", make_hook("second"))
'''))
return get_plugin_manager()
def _contexts(manager):
return manager.invoke_hook("pre_llm_call", session_id="")
@pytest.mark.parametrize("order", ["plugin-first", "memory-first", "memory-only"])
def test_dual_kind_plugin_hooks_run_once(tmp_path, monkeypatch, order):
manager = _install(tmp_path, monkeypatch, enabled=order != "memory-only")
try:
if order == "plugin-first":
manager.discover_and_load()
provider = load_memory_provider("dual")
assert provider is not None and provider.name == "dual"
if order == "memory-first":
manager.discover_and_load()
expected = [{"context": "first"}, {"context": "second"}]
assert _contexts(manager) == expected
# New provider instances must not append another fallback hook group.
assert load_memory_provider("dual") is not provider
assert _contexts(manager) == expected
manager.unload()
assert _contexts(manager) == []
assert not manager._memory_hook_registrations
if order != "memory-only":
manager.discover_and_load(force=True)
assert load_memory_provider("dual") is not None
assert _contexts(manager) == expected
finally:
manager.unload()
def test_same_name_different_sources_are_not_suppressed(tmp_path, monkeypatch):
import shutil
home = tmp_path / "home"
manager = _install(home, monkeypatch)
project = tmp_path / "project"
source = project / ".hermes" / "plugins" / "dual"
shutil.copytree(home / "plugins" / "dual", source)
(source / "values.py").write_text('LABEL = "project"\n')
monkeypatch.chdir(project)
monkeypatch.setenv("HERMES_ENABLE_PROJECT_PLUGINS", "1")
try:
assert load_memory_provider("dual") is not None
manager.discover_and_load()
assert _contexts(manager) == [
{"context": "first"}, {"context": "second"},
{"context": "project"}, {"context": "second"},
]
finally:
manager.unload()
@pytest.mark.parametrize("order", ["plugin-first", "memory-first"])
def test_reexported_register_uses_plugin_source(tmp_path, monkeypatch, order):
manager = _install(tmp_path, monkeypatch)
plugin = tmp_path / "plugins" / "dual"
original = plugin / "__init__.py"
(plugin / "implementation.py").write_text(original.read_text())
original.write_text("from .implementation import register, Provider # MemoryProvider\n")
try:
if order == "plugin-first":
manager.discover_and_load()
assert load_memory_provider("dual") is not None
if order == "memory-first":
manager.discover_and_load()
assert _contexts(manager) == [{"context": "first"}, {"context": "second"}]
finally:
manager.unload()