Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions tests/tui_gateway/test_shell_hooks_registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Test that tui_gateway/entry.py registers shell hooks at startup.

Regression test for GitHub issue #43823 β€” shell hooks configured in
config.yaml were silently ignored in the desktop/TUI entry point because
``register_from_config()`` was never called from ``tui_gateway/entry.py``.
"""

import importlib
import sys
from unittest.mock import MagicMock, patch

import pytest


class TestTuiEntryShellHooks:
"""Verify shell hooks are registered during TUI startup."""

def test_register_from_config_called_at_startup(self):
"""``main()`` must call ``register_from_config`` so that
declarative shell hooks (e.g. ``pre_tool_call``) fire in desktop
sessions just like they do in CLI and gateway sessions."""
# We need to mock the entire startup sequence so main() doesn't
# actually start reading stdin or writing to stdout.
mock_register = MagicMock()

with patch("tui_gateway.entry._install_sidecar_publisher"), \
patch("tui_gateway.entry.write_json", side_effect=[True, lambda *a, **k: True]) as mock_write, \
patch("hermes_cli.config.load_config", return_value={}), \
patch("agent.shell_hooks.register_from_config", mock_register), \
patch("sys.stdin", []):
# Reload to pick up the mock targets
import tui_gateway.entry as entry_mod
# main() reads from sys.stdin β€” empty list means no lines, exits loop
try:
entry_mod.main()
except SystemExit:
pass

mock_register.assert_called_once()
call_kwargs = mock_register.call_args
assert call_kwargs[1].get("accept_hooks") is False

def test_register_failure_does_not_block_startup(self):
"""If ``register_from_config`` raises, the TUI must still start."""
mock_register = MagicMock(side_effect=RuntimeError("boom"))

with patch("tui_gateway.entry._install_sidecar_publisher"), \
patch("tui_gateway.entry.write_json", side_effect=[True, lambda *a, **k: True]), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("agent.shell_hooks.register_from_config", mock_register), \
patch("sys.stdin", []):
import tui_gateway.entry as entry_mod
try:
entry_mod.main()
except SystemExit:
pass
# main() should not have crashed β€” it completed normally

mock_register.assert_called_once()
15 changes: 15 additions & 0 deletions tui_gateway/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,21 @@ def _discover_mcp_background() -> None:
global _mcp_discovery_thread
_mcp_discovery_thread = _mcp_thread

# Register declarative shell hooks from config.yaml. The TUI / desktop
# entry point has no TTY for consent prompts, so accept_hooks=False and
# the effective value is resolved from HERMES_ACCEPT_HOOKS env var or
# hooks_auto_accept in config β€” matching the gateway pattern.
# Failures are logged but must never block TUI startup.
try:
from hermes_cli.config import load_config
from agent.shell_hooks import register_from_config
register_from_config(load_config(), accept_hooks=False)
except Exception:
logger.debug(
"shell-hook registration failed at TUI startup",
exc_info=True,
)

if not write_json({
"jsonrpc": "2.0",
"method": "event",
Expand Down
Loading