test_cli_skin_integration.py
1 from types import SimpleNamespace 2 from unittest.mock import MagicMock, patch 3 4 from cli import HermesCLI, _build_compact_banner, _rich_text_from_ansi 5 from hermes_cli.skin_engine import get_active_skin, set_active_skin 6 7 8 def _make_cli_stub(): 9 cli = HermesCLI.__new__(HermesCLI) 10 cli._sudo_state = None 11 cli._secret_state = None 12 cli._approval_state = None 13 cli._clarify_state = None 14 cli._clarify_freetext = False 15 cli._command_running = False 16 cli._agent_running = False 17 cli._voice_recording = False 18 cli._voice_processing = False 19 cli._voice_mode = False 20 cli._command_spinner_frame = lambda: "โณ" 21 cli._tui_style_base = { 22 "prompt": "#fff", 23 "input-area": "#fff", 24 "input-rule": "#aaa", 25 "prompt-working": "#888 italic", 26 } 27 cli._app = SimpleNamespace(style=None) 28 cli._invalidate = MagicMock() 29 return cli 30 31 32 class TestCliSkinPromptIntegration: 33 def test_default_prompt_fragments_use_default_symbol(self): 34 cli = _make_cli_stub() 35 36 set_active_skin("default") 37 assert cli._get_tui_prompt_fragments() == [("class:prompt", "โฏ ")] 38 39 def test_ares_prompt_fragments_use_skin_symbol(self): 40 cli = _make_cli_stub() 41 42 set_active_skin("ares") 43 assert cli._get_tui_prompt_fragments() == [("class:prompt", "โ ")] 44 45 def test_secret_prompt_fragments_preserve_secret_state(self): 46 cli = _make_cli_stub() 47 cli._secret_state = {"response_queue": object()} 48 49 set_active_skin("ares") 50 assert cli._get_tui_prompt_fragments() == [("class:sudo-prompt", "๐ โ ")] 51 52 def test_icon_only_skin_symbol_still_visible_in_special_states(self): 53 cli = _make_cli_stub() 54 cli._secret_state = {"response_queue": object()} 55 56 with patch("hermes_cli.skin_engine.get_active_prompt_symbol", return_value="โ "): 57 assert cli._get_tui_prompt_fragments() == [("class:sudo-prompt", "๐ โ ")] 58 59 def test_build_tui_style_dict_uses_skin_overrides(self): 60 cli = _make_cli_stub() 61 62 set_active_skin("ares") 63 skin = get_active_skin() 64 style_dict = cli._build_tui_style_dict() 65 66 assert style_dict["prompt"] == skin.get_color("prompt") 67 assert style_dict["input-rule"] == skin.get_color("input_rule") 68 assert style_dict["prompt-working"] == f"{skin.get_color('banner_dim')} italic" 69 assert style_dict["status-bar"] == ( 70 f"bg:{skin.get_color('status_bar_bg')} {skin.get_color('status_bar_text')}" 71 ) 72 assert style_dict["approval-title"] == f"{skin.get_color('ui_warn')} bold" 73 74 def test_apply_tui_skin_style_updates_running_app(self): 75 cli = _make_cli_stub() 76 77 set_active_skin("ares") 78 assert cli._apply_tui_skin_style() is True 79 assert cli._app.style is not None 80 cli._invalidate.assert_called_once_with(min_interval=0.0) 81 82 def test_handle_skin_command_refreshes_live_tui(self, capsys): 83 cli = _make_cli_stub() 84 85 with patch("cli.save_config_value", return_value=True): 86 cli._handle_skin_command("/skin ares") 87 88 output = capsys.readouterr().out 89 assert "Skin set to: ares (saved)" in output 90 assert "Prompt + TUI colors updated." in output 91 assert cli._app.style is not None 92 93 94 class TestCompactBannerSkinIntegration: 95 def test_default_compact_banner_keeps_legacy_nous_hermes_branding(self): 96 set_active_skin("default") 97 98 with patch("cli.shutil.get_terminal_size", return_value=SimpleNamespace(columns=90)), \ 99 patch.dict(_build_compact_banner.__globals__, {"format_banner_version_label": lambda: "Hermes Agent v0.1.0 (test)"}): 100 banner = _build_compact_banner() 101 102 assert "NOUS HERMES" in banner 103 104 def test_poseidon_compact_banner_uses_skin_branding_instead_of_nous_hermes(self): 105 set_active_skin("poseidon") 106 107 with patch("cli.shutil.get_terminal_size", return_value=SimpleNamespace(columns=90)), \ 108 patch.dict(_build_compact_banner.__globals__, {"format_banner_version_label": lambda: "Hermes Agent v0.1.0 (test)"}): 109 banner = _build_compact_banner() 110 111 assert "Poseidon Agent" in banner 112 assert "NOUS HERMES" not in banner 113 114 def test_poseidon_compact_banner_uses_skin_colors(self): 115 set_active_skin("poseidon") 116 skin = get_active_skin() 117 118 with patch("cli.shutil.get_terminal_size", return_value=SimpleNamespace(columns=90)), \ 119 patch.dict(_build_compact_banner.__globals__, {"format_banner_version_label": lambda: "Hermes Agent v0.1.0 (test)"}): 120 banner = _build_compact_banner() 121 122 assert skin.get_color("banner_border") in banner 123 assert skin.get_color("banner_title") in banner 124 assert skin.get_color("banner_dim") in banner 125 126 def test_compact_banner_shows_version_label(self): 127 set_active_skin("default") 128 129 with patch("cli.shutil.get_terminal_size", return_value=SimpleNamespace(columns=90)), \ 130 patch.dict(_build_compact_banner.__globals__, {"format_banner_version_label": lambda: "Hermes Agent v1.0 (test) ยท upstream abc12345"}): 131 banner = _build_compact_banner() 132 133 assert "upstream abc12345" in banner 134 135 136 class TestAnsiRichTextHelper: 137 def test_preserves_literal_brackets(self): 138 text = _rich_text_from_ansi("[notatag] literal") 139 assert text.plain == "[notatag] literal" 140 141 def test_strips_ansi_but_keeps_plain_text(self): 142 text = _rich_text_from_ansi("\x1b[31mred\x1b[0m") 143 assert text.plain == "red"