hermes-agent/tests/hermes_cli/test_plugin_cli_registration.py
Teknium 2367c6ffd5
test: remove 169 change-detector tests across 21 files (#11472)
First pass of test-suite reduction to address flaky CI and bloat.

Removed tests that fall into these change-detector patterns:

1. Source-grep tests (tests/gateway/test_feishu.py, test_email.py): tests
   that call inspect.getsource() on production modules and grep for string
   literals. Break on any refactor/rename even when behavior is correct.

2. Platform enum tautologies (every gateway/test_X.py): assertions like
   `Platform.X.value == 'x'` duplicated across ~9 adapter test files.

3. Toolset/PLATFORM_HINTS/setup-wizard registry-presence checks: tests that
   only verify a key exists in a dict. Data-layout tests, not behavior.

4. Argparse wiring tests (test_argparse_flag_propagation, test_subparser_routing
   _fallback): tests that do parser.parse_args([...]) then assert args.field.
   Tests Python's argparse, not our code.

5. Pure dispatch tests (test_plugins_cmd.TestPluginsCommandDispatch): patch
   cmd_X, call plugins_command with matching action, assert mock called.
   Tests the if/elif chain, not behavior.

6. Kwarg-to-mock verification (test_auxiliary_client ~45 tests,
   test_web_tools_config, test_gemini_cloudcode, test_retaindb_plugin): tests
   that mock the external API client, call our function, and assert exact
   kwargs. Break on refactor even when behavior is preserved.

7. Schedule-internal "function-was-called" tests (acp/test_server scheduling
   tests): tests that patch own helper method, then assert it was called.

Kept behavioral tests throughout: error paths (pytest.raises), security
tests (path traversal, SSRF, redaction), message alternation invariants,
provider API format conversion, streaming logic, memory contract, real
config load/merge tests.

Net reduction: 169 tests removed. 38 empty classes cleaned up.

Collected before: 12,522 tests
Collected after:  12,353 tests
2026-04-17 01:05:09 -07:00

189 lines
7.1 KiB
Python

"""Tests for plugin CLI registration system.
Covers:
- PluginContext.register_cli_command()
- PluginManager._cli_commands storage
- get_plugin_cli_commands() convenience function
- Memory plugin CLI discovery (discover_plugin_cli_commands)
- Honcho register_cli() builds correct argparse tree
"""
import argparse
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from hermes_cli.plugins import (
PluginContext,
PluginManager,
PluginManifest,
)
# ── PluginContext.register_cli_command ─────────────────────────────────────
class TestRegisterCliCommand:
def _make_ctx(self):
mgr = PluginManager()
manifest = PluginManifest(name="test-plugin")
return PluginContext(manifest, mgr), mgr
def test_registers_command(self):
ctx, mgr = self._make_ctx()
setup = MagicMock()
handler = MagicMock()
ctx.register_cli_command(
name="mycmd",
help="Do something",
setup_fn=setup,
handler_fn=handler,
description="Full description",
)
assert "mycmd" in mgr._cli_commands
entry = mgr._cli_commands["mycmd"]
assert entry["name"] == "mycmd"
assert entry["help"] == "Do something"
assert entry["setup_fn"] is setup
assert entry["handler_fn"] is handler
assert entry["plugin"] == "test-plugin"
def test_overwrites_on_duplicate(self):
ctx, mgr = self._make_ctx()
ctx.register_cli_command("x", "first", MagicMock())
ctx.register_cli_command("x", "second", MagicMock())
assert mgr._cli_commands["x"]["help"] == "second"
def test_handler_optional(self):
ctx, mgr = self._make_ctx()
ctx.register_cli_command("nocb", "test", MagicMock())
assert mgr._cli_commands["nocb"]["handler_fn"] is None
# ── Memory plugin CLI discovery ───────────────────────────────────────────
class TestMemoryPluginCliDiscovery:
def test_discovers_active_plugin_with_register_cli(self, tmp_path, monkeypatch):
"""Only the active memory provider's CLI commands are discovered."""
plugin_dir = tmp_path / "testplugin"
plugin_dir.mkdir()
(plugin_dir / "__init__.py").write_text("pass\n")
(plugin_dir / "cli.py").write_text(
"def register_cli(subparser):\n"
" subparser.add_argument('--test')\n"
"\n"
"def testplugin_command(args):\n"
" pass\n"
)
(plugin_dir / "plugin.yaml").write_text(
"name: testplugin\ndescription: A test plugin\n"
)
# Also create a second plugin that should NOT be discovered
other_dir = tmp_path / "otherplugin"
other_dir.mkdir()
(other_dir / "__init__.py").write_text("pass\n")
(other_dir / "cli.py").write_text(
"def register_cli(subparser):\n"
" subparser.add_argument('--other')\n"
)
import plugins.memory as pm
original_dir = pm._MEMORY_PLUGINS_DIR
mod_key = "plugins.memory.testplugin.cli"
sys.modules.pop(mod_key, None)
monkeypatch.setattr(pm, "_MEMORY_PLUGINS_DIR", tmp_path)
# Set testplugin as the active provider
monkeypatch.setattr(pm, "_get_active_memory_provider", lambda: "testplugin")
try:
cmds = pm.discover_plugin_cli_commands()
finally:
monkeypatch.setattr(pm, "_MEMORY_PLUGINS_DIR", original_dir)
sys.modules.pop(mod_key, None)
# Only testplugin should be discovered, not otherplugin
assert len(cmds) == 1
assert cmds[0]["name"] == "testplugin"
assert cmds[0]["help"] == "A test plugin"
assert callable(cmds[0]["setup_fn"])
assert cmds[0]["handler_fn"].__name__ == "testplugin_command"
def test_returns_nothing_when_no_active_provider(self, tmp_path, monkeypatch):
"""No commands when memory.provider is not set in config."""
plugin_dir = tmp_path / "testplugin"
plugin_dir.mkdir()
(plugin_dir / "__init__.py").write_text("pass\n")
(plugin_dir / "cli.py").write_text(
"def register_cli(subparser):\n pass\n"
)
import plugins.memory as pm
original_dir = pm._MEMORY_PLUGINS_DIR
monkeypatch.setattr(pm, "_MEMORY_PLUGINS_DIR", tmp_path)
monkeypatch.setattr(pm, "_get_active_memory_provider", lambda: None)
try:
cmds = pm.discover_plugin_cli_commands()
finally:
monkeypatch.setattr(pm, "_MEMORY_PLUGINS_DIR", original_dir)
assert len(cmds) == 0
def test_skips_plugin_without_register_cli(self, tmp_path, monkeypatch):
"""An active plugin with cli.py but no register_cli returns nothing."""
plugin_dir = tmp_path / "noplugin"
plugin_dir.mkdir()
(plugin_dir / "__init__.py").write_text("pass\n")
(plugin_dir / "cli.py").write_text("def some_other_fn():\n pass\n")
import plugins.memory as pm
original_dir = pm._MEMORY_PLUGINS_DIR
monkeypatch.setattr(pm, "_MEMORY_PLUGINS_DIR", tmp_path)
monkeypatch.setattr(pm, "_get_active_memory_provider", lambda: "noplugin")
try:
cmds = pm.discover_plugin_cli_commands()
finally:
monkeypatch.setattr(pm, "_MEMORY_PLUGINS_DIR", original_dir)
sys.modules.pop("plugins.memory.noplugin.cli", None)
assert len(cmds) == 0
def test_skips_plugin_without_cli_py(self, tmp_path, monkeypatch):
"""An active provider without cli.py returns nothing."""
plugin_dir = tmp_path / "nocli"
plugin_dir.mkdir()
(plugin_dir / "__init__.py").write_text("pass\n")
import plugins.memory as pm
original_dir = pm._MEMORY_PLUGINS_DIR
monkeypatch.setattr(pm, "_MEMORY_PLUGINS_DIR", tmp_path)
monkeypatch.setattr(pm, "_get_active_memory_provider", lambda: "nocli")
try:
cmds = pm.discover_plugin_cli_commands()
finally:
monkeypatch.setattr(pm, "_MEMORY_PLUGINS_DIR", original_dir)
assert len(cmds) == 0
# ── Honcho register_cli ──────────────────────────────────────────────────
# ── ProviderCollector no-op ──────────────────────────────────────────────
class TestProviderCollectorCliNoop:
def test_register_cli_command_is_noop(self):
"""_ProviderCollector.register_cli_command is a no-op (doesn't crash)."""
from plugins.memory import _ProviderCollector
collector = _ProviderCollector()
collector.register_cli_command(
name="test", help="test", setup_fn=lambda s: None
)
# Should not store anything — CLI is discovered via file convention
assert not hasattr(collector, "_cli_commands")