/ tests / gateway / test_display_config.py
test_display_config.py
  1  """Tests for gateway.display_config — per-platform display/verbosity resolver."""
  2  import pytest
  3  
  4  
  5  # ---------------------------------------------------------------------------
  6  # Resolver: resolution order
  7  # ---------------------------------------------------------------------------
  8  
  9  class TestResolveDisplaySetting:
 10      """resolve_display_setting() resolves with correct priority."""
 11  
 12      def test_explicit_platform_override_wins(self):
 13          """display.platforms.<plat>.<key> takes top priority."""
 14          from gateway.display_config import resolve_display_setting
 15  
 16          config = {
 17              "display": {
 18                  "tool_progress": "all",
 19                  "platforms": {
 20                      "telegram": {"tool_progress": "verbose"},
 21                  },
 22              }
 23          }
 24          assert resolve_display_setting(config, "telegram", "tool_progress") == "verbose"
 25  
 26      def test_global_setting_when_no_platform_override(self):
 27          """Falls back to display.<key> when no platform override exists."""
 28          from gateway.display_config import resolve_display_setting
 29  
 30          config = {
 31              "display": {
 32                  "tool_progress": "new",
 33                  "platforms": {},
 34              }
 35          }
 36          assert resolve_display_setting(config, "telegram", "tool_progress") == "new"
 37  
 38      def test_platform_default_when_no_user_config(self):
 39          """Falls back to built-in platform default."""
 40          from gateway.display_config import resolve_display_setting
 41  
 42          # Empty config — should get built-in defaults
 43          config = {}
 44          # Telegram defaults to tier_high → "all"
 45          assert resolve_display_setting(config, "telegram", "tool_progress") == "all"
 46          # Email defaults to tier_minimal → "off"
 47          assert resolve_display_setting(config, "email", "tool_progress") == "off"
 48  
 49      def test_global_default_for_unknown_platform(self):
 50          """Unknown platforms get the global defaults."""
 51          from gateway.display_config import resolve_display_setting
 52  
 53          config = {}
 54          # Unknown platform, no config → global default "all"
 55          assert resolve_display_setting(config, "unknown_platform", "tool_progress") == "all"
 56  
 57      def test_fallback_parameter_used_last(self):
 58          """Explicit fallback is used when nothing else matches."""
 59          from gateway.display_config import resolve_display_setting
 60  
 61          config = {}
 62          # "nonexistent_key" isn't in any defaults
 63          result = resolve_display_setting(config, "telegram", "nonexistent_key", "my_fallback")
 64          assert result == "my_fallback"
 65  
 66      def test_platform_override_only_affects_that_platform(self):
 67          """Other platforms are unaffected by a specific platform override."""
 68          from gateway.display_config import resolve_display_setting
 69  
 70          config = {
 71              "display": {
 72                  "tool_progress": "all",
 73                  "platforms": {
 74                      "slack": {"tool_progress": "off"},
 75                  },
 76              }
 77          }
 78          assert resolve_display_setting(config, "slack", "tool_progress") == "off"
 79          assert resolve_display_setting(config, "telegram", "tool_progress") == "all"
 80  
 81  
 82  # ---------------------------------------------------------------------------
 83  # Backward compatibility: tool_progress_overrides
 84  # ---------------------------------------------------------------------------
 85  
 86  class TestBackwardCompat:
 87      """Legacy tool_progress_overrides is still respected as a fallback."""
 88  
 89      def test_legacy_overrides_read(self):
 90          """tool_progress_overrides is read when no platforms entry exists."""
 91          from gateway.display_config import resolve_display_setting
 92  
 93          config = {
 94              "display": {
 95                  "tool_progress": "all",
 96                  "tool_progress_overrides": {
 97                      "signal": "off",
 98                      "telegram": "verbose",
 99                  },
100              }
101          }
102          assert resolve_display_setting(config, "signal", "tool_progress") == "off"
103          assert resolve_display_setting(config, "telegram", "tool_progress") == "verbose"
104  
105      def test_new_platforms_takes_precedence_over_legacy(self):
106          """display.platforms beats tool_progress_overrides."""
107          from gateway.display_config import resolve_display_setting
108  
109          config = {
110              "display": {
111                  "tool_progress": "all",
112                  "tool_progress_overrides": {"telegram": "verbose"},
113                  "platforms": {"telegram": {"tool_progress": "new"}},
114              }
115          }
116          assert resolve_display_setting(config, "telegram", "tool_progress") == "new"
117  
118      def test_legacy_overrides_only_for_tool_progress(self):
119          """Legacy overrides don't affect other settings."""
120          from gateway.display_config import resolve_display_setting
121  
122          config = {
123              "display": {
124                  "tool_progress_overrides": {"telegram": "verbose"},
125              }
126          }
127          # show_reasoning should NOT read from tool_progress_overrides
128          assert resolve_display_setting(config, "telegram", "show_reasoning") is False
129  
130  
131  # ---------------------------------------------------------------------------
132  # YAML normalisation
133  # ---------------------------------------------------------------------------
134  
135  class TestYAMLNormalisation:
136      """YAML 1.1 quirks (bare off → False, on → True) are handled."""
137  
138      def test_tool_progress_false_normalised_to_off(self):
139          """YAML's bare `off` parses as False — normalised to 'off' string."""
140          from gateway.display_config import resolve_display_setting
141  
142          config = {"display": {"tool_progress": False}}
143          assert resolve_display_setting(config, "telegram", "tool_progress") == "off"
144  
145      def test_tool_progress_true_normalised_to_all(self):
146          """YAML's bare `on` parses as True — normalised to 'all'."""
147          from gateway.display_config import resolve_display_setting
148  
149          config = {"display": {"tool_progress": True}}
150          assert resolve_display_setting(config, "telegram", "tool_progress") == "all"
151  
152      def test_show_reasoning_string_true(self):
153          """String 'true' is normalised to bool True."""
154          from gateway.display_config import resolve_display_setting
155  
156          config = {"display": {"platforms": {"telegram": {"show_reasoning": "true"}}}}
157          assert resolve_display_setting(config, "telegram", "show_reasoning") is True
158  
159      def test_tool_preview_length_string(self):
160          """String numbers are normalised to int."""
161          from gateway.display_config import resolve_display_setting
162  
163          config = {"display": {"platforms": {"slack": {"tool_preview_length": "80"}}}}
164          assert resolve_display_setting(config, "slack", "tool_preview_length") == 80
165  
166      def test_platform_override_false_tool_progress(self):
167          """Per-platform bare off → normalised."""
168          from gateway.display_config import resolve_display_setting
169  
170          config = {"display": {"platforms": {"slack": {"tool_progress": False}}}}
171          assert resolve_display_setting(config, "slack", "tool_progress") == "off"
172  
173  
174  # ---------------------------------------------------------------------------
175  # Built-in platform defaults (tier system)
176  # ---------------------------------------------------------------------------
177  
178  class TestPlatformDefaults:
179      """Built-in defaults reflect platform capability tiers."""
180  
181      def test_high_tier_platforms(self):
182          """Telegram and Discord default to 'all' tool progress."""
183          from gateway.display_config import resolve_display_setting
184  
185          for plat in ("telegram", "discord"):
186              assert resolve_display_setting({}, plat, "tool_progress") == "all", plat
187  
188      def test_medium_tier_platforms(self):
189          """Mattermost, Matrix, Feishu, WhatsApp default to 'new' tool progress."""
190          from gateway.display_config import resolve_display_setting
191  
192          for plat in ("mattermost", "matrix", "feishu", "whatsapp"):
193              assert resolve_display_setting({}, plat, "tool_progress") == "new", plat
194  
195      def test_slack_defaults_tool_progress_off(self):
196          """Slack defaults to quiet tool progress (permanent chat noise otherwise)."""
197          from gateway.display_config import resolve_display_setting
198  
199          assert resolve_display_setting({}, "slack", "tool_progress") == "off"
200  
201      def test_low_tier_platforms(self):
202          """Signal, BlueBubbles, etc. default to 'off' tool progress."""
203          from gateway.display_config import resolve_display_setting
204  
205          for plat in ("signal", "bluebubbles", "weixin", "wecom", "dingtalk"):
206              assert resolve_display_setting({}, plat, "tool_progress") == "off", plat
207  
208      def test_minimal_tier_platforms(self):
209          """Email, SMS, webhook default to 'off' tool progress."""
210          from gateway.display_config import resolve_display_setting
211  
212          for plat in ("email", "sms", "webhook", "homeassistant"):
213              assert resolve_display_setting({}, plat, "tool_progress") == "off", plat
214  
215      def test_low_tier_streaming_defaults_to_false(self):
216          """Low-tier platforms default streaming to False."""
217          from gateway.display_config import resolve_display_setting
218  
219          assert resolve_display_setting({}, "signal", "streaming") is False
220          assert resolve_display_setting({}, "email", "streaming") is False
221  
222      def test_high_tier_streaming_defaults_to_none(self):
223          """High-tier platforms default streaming to None (follow global)."""
224          from gateway.display_config import resolve_display_setting
225  
226          assert resolve_display_setting({}, "telegram", "streaming") is None
227  
228  
229  # ---------------------------------------------------------------------------
230  # Config migration: tool_progress_overrides → display.platforms
231  # ---------------------------------------------------------------------------
232  
233  class TestConfigMigration:
234      """Version 16 migration moves tool_progress_overrides into display.platforms."""
235  
236      def test_migration_creates_platforms_entries(self, tmp_path, monkeypatch):
237          """Old overrides are migrated into display.platforms.<plat>.tool_progress."""
238          import yaml
239  
240          config_path = tmp_path / "config.yaml"
241          config = {
242              "_config_version": 15,
243              "display": {
244                  "tool_progress_overrides": {
245                      "signal": "off",
246                      "telegram": "all",
247                  },
248              },
249          }
250          config_path.write_text(yaml.dump(config), encoding="utf-8")
251  
252          monkeypatch.setenv("HERMES_HOME", str(tmp_path))
253          # Re-import to pick up the new HERMES_HOME
254          import importlib
255          import hermes_cli.config as cfg_mod
256          importlib.reload(cfg_mod)
257  
258          result = cfg_mod.migrate_config(interactive=False, quiet=True)
259          # Re-read config
260          updated = yaml.safe_load(config_path.read_text(encoding="utf-8"))
261          platforms = updated.get("display", {}).get("platforms", {})
262          assert platforms.get("signal", {}).get("tool_progress") == "off"
263          assert platforms.get("telegram", {}).get("tool_progress") == "all"
264  
265      def test_migration_preserves_existing_platforms_entries(self, tmp_path, monkeypatch):
266          """Existing display.platforms entries are NOT overwritten by migration."""
267          import yaml
268  
269          config_path = tmp_path / "config.yaml"
270          config = {
271              "_config_version": 15,
272              "display": {
273                  "tool_progress_overrides": {"telegram": "off"},
274                  "platforms": {"telegram": {"tool_progress": "verbose"}},
275              },
276          }
277          config_path.write_text(yaml.dump(config), encoding="utf-8")
278  
279          monkeypatch.setenv("HERMES_HOME", str(tmp_path))
280          import importlib
281          import hermes_cli.config as cfg_mod
282          importlib.reload(cfg_mod)
283  
284          cfg_mod.migrate_config(interactive=False, quiet=True)
285          updated = yaml.safe_load(config_path.read_text(encoding="utf-8"))
286          # Existing "verbose" should NOT be overwritten by legacy "off"
287          assert updated["display"]["platforms"]["telegram"]["tool_progress"] == "verbose"
288  
289  
290  # ---------------------------------------------------------------------------
291  # Streaming per-platform (None = follow global)
292  # ---------------------------------------------------------------------------
293  
294  class TestStreamingPerPlatform:
295      """Streaming per-platform override semantics."""
296  
297      def test_none_means_follow_global(self):
298          """When streaming is None, the caller should use global config."""
299          from gateway.display_config import resolve_display_setting
300  
301          config = {}
302          # Telegram has no streaming override in defaults → None
303          result = resolve_display_setting(config, "telegram", "streaming")
304          assert result is None  # caller should check global StreamingConfig
305  
306      def test_global_display_streaming_is_cli_only(self):
307          """display.streaming must not act as a gateway streaming override."""
308          from gateway.display_config import resolve_display_setting
309  
310          for value in (True, False):
311              config = {"display": {"streaming": value}}
312              assert resolve_display_setting(config, "telegram", "streaming") is None
313              assert resolve_display_setting(config, "discord", "streaming") is None
314  
315      def test_explicit_false_disables(self):
316          """Explicit False disables streaming for that platform."""
317          from gateway.display_config import resolve_display_setting
318  
319          config = {
320              "display": {
321                  "platforms": {"telegram": {"streaming": False}},
322              }
323          }
324          assert resolve_display_setting(config, "telegram", "streaming") is False
325  
326      def test_explicit_true_enables(self):
327          """Explicit True enables streaming for that platform."""
328          from gateway.display_config import resolve_display_setting
329  
330          config = {
331              "display": {
332                  "platforms": {"email": {"streaming": True}},
333              }
334          }
335          assert resolve_display_setting(config, "email", "streaming") is True