/ tests / cli / test_cli_background_tui_refresh.py
test_cli_background_tui_refresh.py
  1  """Tests for CLI background command TUI refresh behavior.
  2  
  3  Ensures the TUI is properly refreshed before printing background task output
  4  to prevent spinner/status bar overlap (#2718).
  5  """
  6  
  7  import threading
  8  from types import SimpleNamespace
  9  from unittest.mock import MagicMock, patch
 10  
 11  import pytest
 12  
 13  from cli import HermesCLI
 14  
 15  
 16  def _make_cli():
 17      """Create a minimal HermesCLI instance for testing."""
 18      cli_obj = HermesCLI.__new__(HermesCLI)
 19      cli_obj.model = "test-model"
 20      cli_obj._background_tasks = {}
 21      cli_obj._background_task_counter = 0
 22      cli_obj.conversation_history = []
 23      cli_obj.agent = None
 24      cli_obj._app = None
 25      return cli_obj
 26  
 27  
 28  class TestBackgroundCommandTuiRefresh:
 29      """Tests for TUI refresh in background command output."""
 30  
 31      def test_invalidate_called_before_success_output(self):
 32          """App.invalidate() is called before printing background success output."""
 33          cli_obj = _make_cli()
 34          mock_app = MagicMock()
 35          cli_obj._app = mock_app
 36  
 37          # Track call order
 38          call_order = []
 39          original_invalidate = mock_app.invalidate
 40  
 41          def track_invalidate():
 42              call_order.append("invalidate")
 43              return original_invalidate()
 44  
 45          mock_app.invalidate = track_invalidate
 46  
 47          # Patch print to track when it's called
 48          with patch("builtins.print") as mock_print:
 49              mock_print.side_effect = lambda *args, **kwargs: call_order.append("print")
 50  
 51              # Simulate the background task output code path
 52              if cli_obj._app:
 53                  cli_obj._app.invalidate()
 54                  import time
 55                  time.sleep(0.01)  # reduced for test
 56              print()
 57  
 58          # Verify invalidate was called before print
 59          assert call_order[0] == "invalidate"
 60          assert "print" in call_order
 61  
 62      def test_invalidate_called_before_error_output(self):
 63          """App.invalidate() is called before printing background error output."""
 64          cli_obj = _make_cli()
 65          mock_app = MagicMock()
 66          cli_obj._app = mock_app
 67  
 68          call_order = []
 69          mock_app.invalidate.side_effect = lambda: call_order.append("invalidate")
 70  
 71          with patch("builtins.print") as mock_print:
 72              mock_print.side_effect = lambda *args, **kwargs: call_order.append("print")
 73  
 74              # Simulate error path
 75              if cli_obj._app:
 76                  cli_obj._app.invalidate()
 77                  import time
 78                  time.sleep(0.01)
 79              print()
 80  
 81          assert call_order[0] == "invalidate"
 82          assert "print" in call_order
 83  
 84      def test_no_crash_when_app_is_none(self):
 85          """No crash when _app is None (non-TUI mode)."""
 86          cli_obj = _make_cli()
 87          cli_obj._app = None
 88  
 89          # This should not raise
 90          if cli_obj._app:
 91              cli_obj._app.invalidate()
 92          # If we get here without exception, test passes
 93  
 94      def test_background_task_thread_safety(self):
 95          """Background task tracking is thread-safe."""
 96          cli_obj = _make_cli()
 97  
 98          # Simulate adding and removing background tasks
 99          task_id = "test_task_1"
100          cli_obj._background_tasks[task_id] = MagicMock()
101          assert task_id in cli_obj._background_tasks
102  
103          # Clean up
104          cli_obj._background_tasks.pop(task_id, None)
105          assert task_id not in cli_obj._background_tasks