/ tests / gateway / test_verbose_command.py
test_verbose_command.py
  1  """Tests for gateway /verbose command (config-gated tool progress cycling)."""
  2  
  3  import asyncio
  4  from unittest.mock import AsyncMock, MagicMock
  5  
  6  import pytest
  7  import yaml
  8  
  9  import gateway.run as gateway_run
 10  from gateway.config import Platform
 11  from gateway.platforms.base import MessageEvent
 12  from gateway.session import SessionSource
 13  
 14  
 15  def _make_event(text="/verbose", platform=Platform.TELEGRAM, user_id="12345", chat_id="67890"):
 16      """Build a MessageEvent for testing."""
 17      source = SessionSource(
 18          platform=platform,
 19          user_id=user_id,
 20          chat_id=chat_id,
 21          user_name="testuser",
 22      )
 23      return MessageEvent(text=text, source=source)
 24  
 25  
 26  def _make_runner():
 27      """Create a bare GatewayRunner without calling __init__."""
 28      runner = object.__new__(gateway_run.GatewayRunner)
 29      runner.adapters = {}
 30      runner._ephemeral_system_prompt = ""
 31      runner._prefill_messages = []
 32      runner._reasoning_config = None
 33      runner._show_reasoning = False
 34      runner._provider_routing = {}
 35      runner._fallback_model = None
 36      runner._running_agents = {}
 37      runner.hooks = MagicMock()
 38      runner.hooks.emit = AsyncMock()
 39      runner.hooks.loaded_hooks = []
 40      runner._session_db = None
 41      runner._get_or_create_gateway_honcho = lambda session_key: (None, None)
 42      return runner
 43  
 44  
 45  class TestVerboseCommand:
 46      """Tests for _handle_verbose_command in the gateway."""
 47  
 48      @pytest.mark.asyncio
 49      async def test_disabled_by_default(self, tmp_path, monkeypatch):
 50          """When tool_progress_command is false, /verbose returns an info message."""
 51          hermes_home = tmp_path / "hermes"
 52          hermes_home.mkdir()
 53          config_path = hermes_home / "config.yaml"
 54          config_path.write_text("display:\n  tool_progress: all\n", encoding="utf-8")
 55  
 56          monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
 57  
 58          runner = _make_runner()
 59          result = await runner._handle_verbose_command(_make_event())
 60  
 61          assert "not enabled" in result.lower()
 62          assert "tool_progress_command" in result
 63  
 64      @pytest.mark.asyncio
 65      async def test_enabled_cycles_mode(self, tmp_path, monkeypatch):
 66          """When enabled, /verbose cycles tool_progress mode per-platform."""
 67          hermes_home = tmp_path / "hermes"
 68          hermes_home.mkdir()
 69          config_path = hermes_home / "config.yaml"
 70          config_path.write_text(
 71              "display:\n  tool_progress_command: true\n  tool_progress: all\n",
 72              encoding="utf-8",
 73          )
 74  
 75          monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
 76  
 77          runner = _make_runner()
 78          result = await runner._handle_verbose_command(_make_event())
 79  
 80          # all -> verbose
 81          assert "VERBOSE" in result
 82          assert "telegram" in result.lower()  # per-platform feedback
 83  
 84          # Verify config was saved to display.platforms.telegram
 85          saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
 86          assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "verbose"
 87  
 88      @pytest.mark.asyncio
 89      async def test_quoted_false_keeps_command_disabled(self, tmp_path, monkeypatch):
 90          """Quoted false must not enable the /verbose gateway command."""
 91          hermes_home = tmp_path / "hermes"
 92          hermes_home.mkdir()
 93          config_path = hermes_home / "config.yaml"
 94          config_path.write_text(
 95              'display:\n  tool_progress_command: "false"\n  tool_progress: all\n',
 96              encoding="utf-8",
 97          )
 98  
 99          monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
100  
101          runner = _make_runner()
102          result = await runner._handle_verbose_command(_make_event())
103  
104          assert "not enabled" in result.lower()
105          assert "tool_progress_command" in result
106  
107      @pytest.mark.asyncio
108      async def test_cycles_through_all_modes(self, tmp_path, monkeypatch):
109          """Calling /verbose repeatedly cycles through all four modes."""
110          hermes_home = tmp_path / "hermes"
111          hermes_home.mkdir()
112          config_path = hermes_home / "config.yaml"
113          config_path.write_text(
114              "display:\n  tool_progress_command: true\n  tool_progress: 'off'\n",
115              encoding="utf-8",
116          )
117  
118          monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
119          runner = _make_runner()
120  
121          # off -> new -> all -> verbose -> off
122          expected = ["new", "all", "verbose", "off"]
123          for mode in expected:
124              result = await runner._handle_verbose_command(_make_event())
125              saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
126              actual = saved["display"]["platforms"]["telegram"]["tool_progress"]
127              assert actual == mode, \
128                  f"Expected {mode}, got {actual}"
129  
130      @pytest.mark.asyncio
131      async def test_defaults_to_all_when_no_tool_progress_set(self, tmp_path, monkeypatch):
132          """When tool_progress is not in config, defaults to 'all' then cycles to verbose."""
133          hermes_home = tmp_path / "hermes"
134          hermes_home.mkdir()
135          config_path = hermes_home / "config.yaml"
136          config_path.write_text(
137              "display:\n  tool_progress_command: true\n",
138              encoding="utf-8",
139          )
140  
141          monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
142  
143          runner = _make_runner()
144          result = await runner._handle_verbose_command(_make_event())
145  
146          # Telegram default is "all" (high tier) → cycles to verbose
147          assert "VERBOSE" in result
148          saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
149          assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "verbose"
150  
151      @pytest.mark.asyncio
152      async def test_per_platform_isolation(self, tmp_path, monkeypatch):
153          """Cycling /verbose on Telegram doesn't change Slack's setting.
154  
155          Without a global tool_progress, each platform uses its built-in
156          default: Telegram = 'all' (high tier), Slack = 'off' (quiet Slack default).
157          """
158          hermes_home = tmp_path / "hermes"
159          hermes_home.mkdir()
160          config_path = hermes_home / "config.yaml"
161          # No global tool_progress → built-in platform defaults apply
162          config_path.write_text(
163              "display:\n  tool_progress_command: true\n",
164              encoding="utf-8",
165          )
166  
167          monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
168          runner = _make_runner()
169  
170          # Cycle on Telegram
171          await runner._handle_verbose_command(
172              _make_event(platform=Platform.TELEGRAM)
173          )
174          # Cycle on Slack
175          await runner._handle_verbose_command(
176              _make_event(platform=Platform.SLACK)
177          )
178  
179          saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
180          platforms = saved["display"]["platforms"]
181          # Telegram: all -> verbose (high tier default = all)
182          assert platforms["telegram"]["tool_progress"] == "verbose"
183          # Slack: off -> new (first /verbose cycle from quiet default)
184          assert platforms["slack"]["tool_progress"] == "new"
185  
186      @pytest.mark.asyncio
187      async def test_no_config_file_returns_disabled(self, tmp_path, monkeypatch):
188          """When config.yaml doesn't exist, command reports disabled."""
189          hermes_home = tmp_path / "hermes"
190          hermes_home.mkdir()
191          # No config.yaml
192  
193          monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
194  
195          runner = _make_runner()
196          result = await runner._handle_verbose_command(_make_event())
197          assert "not enabled" in result.lower()
198  
199      def test_verbose_is_in_gateway_known_commands(self):
200          """The /verbose command is recognized by the gateway dispatch."""
201          from hermes_cli.commands import GATEWAY_KNOWN_COMMANDS
202          assert "verbose" in GATEWAY_KNOWN_COMMANDS