fix(tests): resolve 10 CI failures across hooks, tiktoken, plugins (#3848)
test_hooks.py (7 failures): Built-in boot-md hook was always loaded
by _register_builtin_hooks(), adding +1 to every expected hook count.
Mock out built-in registration in TestDiscoverAndLoad so tests isolate
user-hook discovery logic.
test_tool_token_estimation.py (2 failures): tiktoken is not in
core/[all] dependencies. The estimation function gracefully returns {}
when tiktoken is missing, but tests expected non-empty results. Added
skipif markers for tests that need tiktoken.
test_plugins_cmd.py (1 failure): bare 'hermes plugins' now dispatches
to cmd_toggle() (interactive curses UI) instead of cmd_list(). Updated
test to match the new behavior.
This commit is contained in:
@@ -29,13 +29,18 @@ class TestHookRegistryInit:
|
||||
assert reg._handlers == {}
|
||||
|
||||
|
||||
def _patch_no_builtins(reg):
|
||||
"""Suppress built-in hook registration so tests only exercise user-hook discovery."""
|
||||
return patch.object(reg, "_register_builtin_hooks")
|
||||
|
||||
|
||||
class TestDiscoverAndLoad:
|
||||
def test_loads_valid_hook(self, tmp_path):
|
||||
_create_hook(tmp_path, "my-hook", '["agent:start"]',
|
||||
"def handle(event_type, context):\n pass\n")
|
||||
|
||||
reg = HookRegistry()
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path):
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path), _patch_no_builtins(reg):
|
||||
reg.discover_and_load()
|
||||
|
||||
assert len(reg.loaded_hooks) == 1
|
||||
@@ -48,7 +53,7 @@ class TestDiscoverAndLoad:
|
||||
(hook_dir / "handler.py").write_text("def handle(e, c): pass\n")
|
||||
|
||||
reg = HookRegistry()
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path):
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path), _patch_no_builtins(reg):
|
||||
reg.discover_and_load()
|
||||
|
||||
assert len(reg.loaded_hooks) == 0
|
||||
@@ -59,7 +64,7 @@ class TestDiscoverAndLoad:
|
||||
(hook_dir / "HOOK.yaml").write_text("name: bad\nevents: ['agent:start']\n")
|
||||
|
||||
reg = HookRegistry()
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path):
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path), _patch_no_builtins(reg):
|
||||
reg.discover_and_load()
|
||||
|
||||
assert len(reg.loaded_hooks) == 0
|
||||
@@ -71,7 +76,7 @@ class TestDiscoverAndLoad:
|
||||
(hook_dir / "handler.py").write_text("def handle(e, c): pass\n")
|
||||
|
||||
reg = HookRegistry()
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path):
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path), _patch_no_builtins(reg):
|
||||
reg.discover_and_load()
|
||||
|
||||
assert len(reg.loaded_hooks) == 0
|
||||
@@ -83,14 +88,14 @@ class TestDiscoverAndLoad:
|
||||
(hook_dir / "handler.py").write_text("def something_else(): pass\n")
|
||||
|
||||
reg = HookRegistry()
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path):
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path), _patch_no_builtins(reg):
|
||||
reg.discover_and_load()
|
||||
|
||||
assert len(reg.loaded_hooks) == 0
|
||||
|
||||
def test_nonexistent_hooks_dir(self, tmp_path):
|
||||
reg = HookRegistry()
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path / "nonexistent"):
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path / "nonexistent"), _patch_no_builtins(reg):
|
||||
reg.discover_and_load()
|
||||
|
||||
assert len(reg.loaded_hooks) == 0
|
||||
@@ -102,7 +107,7 @@ class TestDiscoverAndLoad:
|
||||
"def handle(e, c): pass\n")
|
||||
|
||||
reg = HookRegistry()
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path):
|
||||
with patch("gateway.hooks.HOOKS_DIR", tmp_path), _patch_no_builtins(reg):
|
||||
reg.discover_and_load()
|
||||
|
||||
assert len(reg.loaded_hooks) == 2
|
||||
|
||||
@@ -4,10 +4,20 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
# tiktoken is not in core/[all] deps — skip estimation tests when unavailable
|
||||
_has_tiktoken = True
|
||||
try:
|
||||
import tiktoken # noqa: F401
|
||||
except ImportError:
|
||||
_has_tiktoken = False
|
||||
|
||||
_needs_tiktoken = pytest.mark.skipif(not _has_tiktoken, reason="tiktoken not installed")
|
||||
|
||||
|
||||
# ─── Token Estimation Tests ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@_needs_tiktoken
|
||||
def test_estimate_tool_tokens_returns_positive_counts():
|
||||
"""_estimate_tool_tokens should return a non-empty dict with positive values."""
|
||||
from hermes_cli.tools_config import _estimate_tool_tokens, _tool_token_cache
|
||||
@@ -26,6 +36,7 @@ def test_estimate_tool_tokens_returns_positive_counts():
|
||||
assert count > 0, f"Tool {name} has non-positive token count: {count}"
|
||||
|
||||
|
||||
@_needs_tiktoken
|
||||
def test_estimate_tool_tokens_is_cached():
|
||||
"""Second call should return the same cached dict object."""
|
||||
import hermes_cli.tools_config as tc
|
||||
@@ -60,6 +71,7 @@ def test_estimate_tool_tokens_returns_empty_when_tiktoken_unavailable(monkeypatc
|
||||
tc._tool_token_cache = None
|
||||
|
||||
|
||||
@_needs_tiktoken
|
||||
def test_estimate_tool_tokens_covers_known_tools():
|
||||
"""Should include schemas for well-known tools like terminal, web_search."""
|
||||
import hermes_cli.tools_config as tc
|
||||
|
||||
@@ -150,11 +150,11 @@ class TestPluginsCommandDispatch:
|
||||
plugins_command(args)
|
||||
mock_list.assert_called_once()
|
||||
|
||||
@patch("hermes_cli.plugins_cmd.cmd_list")
|
||||
def test_none_falls_through_to_list(self, mock_list):
|
||||
@patch("hermes_cli.plugins_cmd.cmd_toggle")
|
||||
def test_none_falls_through_to_toggle(self, mock_toggle):
|
||||
args = self._make_args(None)
|
||||
plugins_command(args)
|
||||
mock_list.assert_called_once()
|
||||
mock_toggle.assert_called_once()
|
||||
|
||||
@patch("hermes_cli.plugins_cmd.cmd_install")
|
||||
def test_install_dispatches(self, mock_install):
|
||||
|
||||
Reference in New Issue
Block a user