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