test_cli_skin_integration.py
1 from types import SimpleNamespace 2 from unittest.mock import MagicMock, patch 3 4 from cli import HermesCLI, _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_narrow_terminals_compact_voice_prompt_fragments(self): 53 cli = _make_cli_stub() 54 cli._voice_mode = True 55 56 with patch.object(HermesCLI, "_get_tui_terminal_width", return_value=50): 57 assert cli._get_tui_prompt_fragments() == [("class:voice-prompt", "🎤 ")] 58 59 def test_narrow_terminals_compact_voice_recording_prompt_fragments(self): 60 cli = _make_cli_stub() 61 cli._voice_recording = True 62 cli._voice_recorder = SimpleNamespace(current_rms=3000) 63 64 with patch.object(HermesCLI, "_get_tui_terminal_width", return_value=50): 65 frags = cli._get_tui_prompt_fragments() 66 67 assert frags[0][0] == "class:voice-recording" 68 assert frags[0][1].startswith("●") 69 assert "❯" not in frags[0][1] 70 71 def test_icon_only_skin_symbol_still_visible_in_special_states(self): 72 cli = _make_cli_stub() 73 cli._secret_state = {"response_queue": object()} 74 75 with patch("hermes_cli.skin_engine.get_active_prompt_symbol", return_value="⚔ "): 76 assert cli._get_tui_prompt_fragments() == [("class:sudo-prompt", "🔑 ⚔ ")] 77 78 def test_build_tui_style_dict_uses_skin_overrides(self): 79 cli = _make_cli_stub() 80 81 set_active_skin("ares") 82 skin = get_active_skin() 83 style_dict = cli._build_tui_style_dict() 84 85 assert style_dict["prompt"] == skin.get_color("prompt") 86 assert style_dict["input-rule"] == skin.get_color("input_rule") 87 assert style_dict["prompt-working"] == f"{skin.get_color('banner_dim')} italic" 88 assert style_dict["approval-title"] == f"{skin.get_color('ui_warn')} bold" 89 90 def test_apply_tui_skin_style_updates_running_app(self): 91 cli = _make_cli_stub() 92 93 set_active_skin("ares") 94 assert cli._apply_tui_skin_style() is True 95 assert cli._app.style is not None 96 cli._invalidate.assert_called_once_with(min_interval=0.0) 97 98 def test_handle_skin_command_refreshes_live_tui(self, capsys): 99 cli = _make_cli_stub() 100 101 with patch("cli.save_config_value", return_value=True): 102 cli._handle_skin_command("/skin ares") 103 104 output = capsys.readouterr().out 105 assert "Skin set to: ares (saved)" in output 106 assert "Prompt + TUI colors updated." in output 107 assert cli._app.style is not None 108 109 110 class TestAnsiRichTextHelper: 111 def test_preserves_literal_brackets(self): 112 text = _rich_text_from_ansi("[notatag] literal") 113 assert text.plain == "[notatag] literal" 114 115 def test_strips_ansi_but_keeps_plain_text(self): 116 text = _rich_text_from_ansi("\x1b[31mred\x1b[0m") 117 assert text.plain == "red"