/ tests / cli / test_cli_external_editor.py
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