test_cli_external_editor.py
1 """Tests for CLI external-editor support.""" 2 3 from unittest.mock import patch 4 5 from cli import HermesCLI 6 7 8 class _FakeBuffer: 9 def __init__(self, text=""): 10 self.calls = [] 11 self.text = text 12 self.cursor_position = len(text) 13 14 def open_in_editor(self, validate_and_handle=False): 15 self.calls.append(validate_and_handle) 16 17 18 class _FakeApp: 19 def __init__(self): 20 self.current_buffer = _FakeBuffer() 21 22 23 def _make_cli(with_app=True): 24 cli_obj = HermesCLI.__new__(HermesCLI) 25 cli_obj._app = _FakeApp() if with_app else None 26 cli_obj._command_running = False 27 cli_obj._command_status = "" 28 cli_obj._command_display = "" 29 cli_obj._sudo_state = None 30 cli_obj._secret_state = None 31 cli_obj._approval_state = None 32 cli_obj._clarify_state = None 33 cli_obj._skip_paste_collapse = False 34 return cli_obj 35 36 def test_open_external_editor_uses_prompt_toolkit_buffer_editor(): 37 cli_obj = _make_cli() 38 39 assert cli_obj._open_external_editor() is True 40 assert cli_obj._app.current_buffer.calls == [False] 41 42 43 def test_open_external_editor_rejects_when_no_tui(): 44 cli_obj = _make_cli(with_app=False) 45 46 with patch("cli._cprint") as mock_cprint: 47 assert cli_obj._open_external_editor() is False 48 49 assert mock_cprint.called 50 assert "interactive cli" in str(mock_cprint.call_args).lower() 51 52 53 def test_open_external_editor_rejects_modal_prompts(): 54 cli_obj = _make_cli() 55 cli_obj._approval_state = {"selected": 0} 56 57 with patch("cli._cprint") as mock_cprint: 58 assert cli_obj._open_external_editor() is False 59 60 assert mock_cprint.called 61 assert "active prompt" in str(mock_cprint.call_args).lower() 62 63 def test_open_external_editor_uses_explicit_buffer_when_provided(): 64 cli_obj = _make_cli() 65 external_buffer = _FakeBuffer() 66 67 assert cli_obj._open_external_editor(buffer=external_buffer) is True 68 assert external_buffer.calls == [False] 69 assert cli_obj._app.current_buffer.calls == [] 70 71 72 def test_expand_paste_references_replaces_placeholder_with_file_contents(tmp_path): 73 cli_obj = _make_cli() 74 paste_file = tmp_path / "paste.txt" 75 paste_file.write_text("line one\nline two", encoding="utf-8") 76 77 text = f"before [Pasted text #1: 2 lines → {paste_file}] after" 78 expanded = cli_obj._expand_paste_references(text) 79 80 assert expanded == "before line one\nline two after" 81 82 83 def test_open_external_editor_expands_paste_placeholders_before_open(tmp_path): 84 cli_obj = _make_cli() 85 paste_file = tmp_path / "paste.txt" 86 paste_file.write_text("alpha\nbeta", encoding="utf-8") 87 buffer = _FakeBuffer(text=f"[Pasted text #1: 2 lines → {paste_file}]") 88 89 assert cli_obj._open_external_editor(buffer=buffer) is True 90 assert buffer.text == "alpha\nbeta" 91 assert buffer.cursor_position == len("alpha\nbeta") 92 assert buffer.calls == [False] 93 94 95 def test_open_external_editor_sets_skip_collapse_flag_during_expansion(tmp_path): 96 cli_obj = _make_cli() 97 paste_file = tmp_path / "paste.txt" 98 paste_file.write_text("a\nb\nc\nd\ne\nf", encoding="utf-8") 99 buffer = _FakeBuffer(text=f"[Pasted text #1: 6 lines \u2192 {paste_file}]") 100 101 # After expansion the flag should have been set (to prevent re-collapse) 102 assert cli_obj._open_external_editor(buffer=buffer) is True 103 # Flag is consumed by _on_text_changed, but since no handler is attached 104 # in tests it stays True until the handler resets it. 105 assert cli_obj._skip_paste_collapse is True