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