File size: 5,053 Bytes
9aa5185 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | """Tests for protected HermesCLI TUI extension hooks.
Verifies that wrapper CLIs can extend the TUI via:
- _get_extra_tui_widgets()
- _register_extra_tui_keybindings()
- _build_tui_layout_children()
without overriding run().
"""
from __future__ import annotations
import importlib
import sys
from unittest.mock import MagicMock, patch
from prompt_toolkit.key_binding import KeyBindings
def _make_cli(**kwargs):
"""Create a HermesCLI with prompt_toolkit stubs (same pattern as test_cli_init)."""
_clean_config = {
"model": {
"default": "anthropic/claude-opus-4.6",
"base_url": "https://openrouter.ai/api/v1",
"provider": "auto",
},
"display": {"compact": False, "tool_progress": "all"},
"agent": {},
"terminal": {"env_type": "local"},
}
clean_env = {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}
prompt_toolkit_stubs = {
"prompt_toolkit": MagicMock(),
"prompt_toolkit.history": MagicMock(),
"prompt_toolkit.styles": MagicMock(),
"prompt_toolkit.patch_stdout": MagicMock(),
"prompt_toolkit.application": MagicMock(),
"prompt_toolkit.layout": MagicMock(),
"prompt_toolkit.layout.processors": MagicMock(),
"prompt_toolkit.filters": MagicMock(),
"prompt_toolkit.layout.dimension": MagicMock(),
"prompt_toolkit.layout.menus": MagicMock(),
"prompt_toolkit.widgets": MagicMock(),
"prompt_toolkit.key_binding": MagicMock(),
"prompt_toolkit.completion": MagicMock(),
"prompt_toolkit.formatted_text": MagicMock(),
"prompt_toolkit.auto_suggest": MagicMock(),
}
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict(
"os.environ", clean_env, clear=False
):
import cli as _cli_mod
_cli_mod = importlib.reload(_cli_mod)
with patch.object(_cli_mod, "get_tool_definitions", return_value=[]), patch.dict(
_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}
):
return _cli_mod.HermesCLI(**kwargs)
class TestExtensionHookDefaults:
def test_extra_tui_widgets_default_empty(self):
cli = _make_cli()
assert cli._get_extra_tui_widgets() == []
def test_register_extra_tui_keybindings_default_noop(self):
cli = _make_cli()
kb = KeyBindings()
result = cli._register_extra_tui_keybindings(kb, input_area=None)
assert result is None
assert kb.bindings == []
def test_build_tui_layout_children_returns_all_widgets_in_order(self):
cli = _make_cli()
children = cli._build_tui_layout_children(
sudo_widget="sudo",
secret_widget="secret",
approval_widget="approval",
clarify_widget="clarify",
spinner_widget="spinner",
spacer="spacer",
status_bar="status",
input_rule_top="top-rule",
image_bar="image-bar",
input_area="input-area",
input_rule_bot="bottom-rule",
voice_status_bar="voice-status",
completions_menu="completions-menu",
)
# First element is Window(height=0), rest are the named widgets
assert children[1:] == [
"sudo", "secret", "approval", "clarify", "spinner",
"spacer", "status", "top-rule", "image-bar", "input-area",
"bottom-rule", "voice-status", "completions-menu",
]
class TestExtensionHookSubclass:
def test_extra_widgets_inserted_before_status_bar(self):
cli = _make_cli()
# Monkey-patch to simulate subclass override
cli._get_extra_tui_widgets = lambda: ["radio-menu", "mini-player"]
children = cli._build_tui_layout_children(
sudo_widget="sudo",
secret_widget="secret",
approval_widget="approval",
clarify_widget="clarify",
spinner_widget="spinner",
spacer="spacer",
status_bar="status",
input_rule_top="top-rule",
image_bar="image-bar",
input_area="input-area",
input_rule_bot="bottom-rule",
voice_status_bar="voice-status",
completions_menu="completions-menu",
)
# Extra widgets should appear between spacer and status bar
spacer_idx = children.index("spacer")
status_idx = children.index("status")
assert children[spacer_idx + 1] == "radio-menu"
assert children[spacer_idx + 2] == "mini-player"
assert children[spacer_idx + 3] == "status"
assert status_idx == spacer_idx + 3
def test_extra_keybindings_can_add_bindings(self):
cli = _make_cli()
kb = KeyBindings()
def _custom_hook(kb, *, input_area):
@kb.add("f2")
def _toggle(event):
return None
cli._register_extra_tui_keybindings = _custom_hook
cli._register_extra_tui_keybindings(kb, input_area=None)
assert len(kb.bindings) == 1
|