mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: implement register_command() on plugin context (#10626)
Complete the half-built plugin slash command system. The dispatch code in cli.py and gateway/run.py already called get_plugin_command_handler() but the registration side was never implemented. Changes: - Add register_command() to PluginContext — stores handler, description, and plugin name; normalizes names; rejects conflicts with built-in commands - Add _plugin_commands dict to PluginManager - Add commands_registered tracking on LoadedPlugin - Add get_plugin_command_handler() and get_plugin_commands() module-level convenience functions - Fix commands.py to use actual plugin description in Telegram bot menu (was hardcoded 'Plugin command') - Add plugin commands to SlashCommandCompleter autocomplete - Show command count in /plugins display - 12 new tests covering registration, conflict detection, normalization, handler dispatch, and introspection Closes #10495
This commit is contained in:
parent
df714add9d
commit
498b995c13
4 changed files with 246 additions and 6 deletions
|
|
@ -18,6 +18,8 @@ from hermes_cli.plugins import (
|
|||
PluginManager,
|
||||
PluginManifest,
|
||||
get_plugin_manager,
|
||||
get_plugin_command_handler,
|
||||
get_plugin_commands,
|
||||
get_pre_tool_call_block_message,
|
||||
discover_plugins,
|
||||
invoke_hook,
|
||||
|
|
@ -605,7 +607,160 @@ class TestPreLlmCallTargetRouting:
|
|||
assert "plain text C" in _plugin_user_context
|
||||
|
||||
|
||||
# NOTE: TestPluginCommands removed – register_command() was never implemented
|
||||
# in PluginContext (hermes_cli/plugins.py). The tests referenced _plugin_commands,
|
||||
# commands_registered, get_plugin_command_handler, and GATEWAY_KNOWN_COMMANDS
|
||||
# integration — all of which are unimplemented features.
|
||||
# ── TestPluginCommands ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestPluginCommands:
|
||||
"""Tests for plugin slash command registration via register_command()."""
|
||||
|
||||
def test_register_command_basic(self):
|
||||
"""register_command() stores handler, description, and plugin name."""
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name="test-plugin", source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
|
||||
handler = lambda args: f"echo {args}"
|
||||
ctx.register_command("mycmd", handler, description="My custom command")
|
||||
|
||||
assert "mycmd" in mgr._plugin_commands
|
||||
entry = mgr._plugin_commands["mycmd"]
|
||||
assert entry["handler"] is handler
|
||||
assert entry["description"] == "My custom command"
|
||||
assert entry["plugin"] == "test-plugin"
|
||||
|
||||
def test_register_command_normalizes_name(self):
|
||||
"""Names are lowercased, stripped, and leading slashes removed."""
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name="test-plugin", source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
|
||||
ctx.register_command("/MyCmd ", lambda a: a, description="test")
|
||||
assert "mycmd" in mgr._plugin_commands
|
||||
assert "/MyCmd " not in mgr._plugin_commands
|
||||
|
||||
def test_register_command_empty_name_rejected(self, caplog):
|
||||
"""Empty name after normalization is rejected with a warning."""
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name="test-plugin", source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ctx.register_command("", lambda a: a)
|
||||
assert len(mgr._plugin_commands) == 0
|
||||
assert "empty name" in caplog.text
|
||||
|
||||
def test_register_command_builtin_conflict_rejected(self, caplog):
|
||||
"""Commands that conflict with built-in names are rejected."""
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name="test-plugin", source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ctx.register_command("help", lambda a: a)
|
||||
assert "help" not in mgr._plugin_commands
|
||||
assert "conflicts" in caplog.text.lower()
|
||||
|
||||
def test_register_command_default_description(self):
|
||||
"""Missing description defaults to 'Plugin command'."""
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name="test-plugin", source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
|
||||
ctx.register_command("status-cmd", lambda a: a)
|
||||
assert mgr._plugin_commands["status-cmd"]["description"] == "Plugin command"
|
||||
|
||||
def test_get_plugin_command_handler_found(self):
|
||||
"""get_plugin_command_handler() returns the handler for a registered command."""
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name="test-plugin", source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
|
||||
handler = lambda args: f"result: {args}"
|
||||
ctx.register_command("mycmd", handler, description="test")
|
||||
|
||||
with patch("hermes_cli.plugins._plugin_manager", mgr):
|
||||
result = get_plugin_command_handler("mycmd")
|
||||
assert result is handler
|
||||
|
||||
def test_get_plugin_command_handler_not_found(self):
|
||||
"""get_plugin_command_handler() returns None for unregistered commands."""
|
||||
mgr = PluginManager()
|
||||
with patch("hermes_cli.plugins._plugin_manager", mgr):
|
||||
assert get_plugin_command_handler("nonexistent") is None
|
||||
|
||||
def test_get_plugin_commands_returns_dict(self):
|
||||
"""get_plugin_commands() returns the full commands dict."""
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name="test-plugin", source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
ctx.register_command("cmd-a", lambda a: a, description="A")
|
||||
ctx.register_command("cmd-b", lambda a: a, description="B")
|
||||
|
||||
with patch("hermes_cli.plugins._plugin_manager", mgr):
|
||||
cmds = get_plugin_commands()
|
||||
assert "cmd-a" in cmds
|
||||
assert "cmd-b" in cmds
|
||||
assert cmds["cmd-a"]["description"] == "A"
|
||||
|
||||
def test_commands_tracked_on_loaded_plugin(self, tmp_path, monkeypatch):
|
||||
"""Commands registered during discover_and_load() are tracked on LoadedPlugin."""
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
_make_plugin_dir(
|
||||
plugins_dir, "cmd-plugin",
|
||||
register_body=(
|
||||
'ctx.register_command("mycmd", lambda a: "ok", description="Test")'
|
||||
),
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
|
||||
|
||||
mgr = PluginManager()
|
||||
mgr.discover_and_load()
|
||||
|
||||
loaded = mgr._plugins["cmd-plugin"]
|
||||
assert loaded.enabled
|
||||
assert "mycmd" in loaded.commands_registered
|
||||
|
||||
def test_commands_in_list_plugins_output(self, tmp_path, monkeypatch):
|
||||
"""list_plugins() includes command count."""
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
_make_plugin_dir(
|
||||
plugins_dir, "cmd-plugin",
|
||||
register_body=(
|
||||
'ctx.register_command("mycmd", lambda a: "ok", description="Test")'
|
||||
),
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
|
||||
|
||||
mgr = PluginManager()
|
||||
mgr.discover_and_load()
|
||||
|
||||
info = mgr.list_plugins()
|
||||
assert len(info) == 1
|
||||
assert info[0]["commands"] == 1
|
||||
|
||||
def test_handler_receives_raw_args(self):
|
||||
"""The handler is called with the raw argument string."""
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name="test-plugin", source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
|
||||
received = []
|
||||
ctx.register_command("echo", lambda args: received.append(args) or "ok")
|
||||
|
||||
handler = mgr._plugin_commands["echo"]["handler"]
|
||||
handler("hello world")
|
||||
assert received == ["hello world"]
|
||||
|
||||
def test_multiple_plugins_register_different_commands(self):
|
||||
"""Multiple plugins can each register their own commands."""
|
||||
mgr = PluginManager()
|
||||
|
||||
for plugin_name, cmd_name in [("plugin-a", "cmd-a"), ("plugin-b", "cmd-b")]:
|
||||
manifest = PluginManifest(name=plugin_name, source="user")
|
||||
ctx = PluginContext(manifest, mgr)
|
||||
ctx.register_command(cmd_name, lambda a: a, description=f"From {plugin_name}")
|
||||
|
||||
assert "cmd-a" in mgr._plugin_commands
|
||||
assert "cmd-b" in mgr._plugin_commands
|
||||
assert mgr._plugin_commands["cmd-a"]["plugin"] == "plugin-a"
|
||||
assert mgr._plugin_commands["cmd-b"]["plugin"] == "plugin-b"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue