"""Tests for CLI background command TUI refresh behavior. Ensures the TUI is properly refreshed before printing background task output to prevent spinner/status bar overlap (#2718). """ import threading from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest from cli import HermesCLI def _make_cli(): """Create a minimal HermesCLI instance for testing.""" cli_obj = HermesCLI.__new__(HermesCLI) cli_obj.model = "test-model" cli_obj._background_tasks = {} cli_obj._background_task_counter = 0 cli_obj.conversation_history = [] cli_obj.agent = None cli_obj._app = None return cli_obj class TestBackgroundCommandTuiRefresh: """Tests for TUI refresh in background command output.""" def test_invalidate_called_before_success_output(self): """App.invalidate() is called before printing background success output.""" cli_obj = _make_cli() mock_app = MagicMock() cli_obj._app = mock_app # Track call order call_order = [] original_invalidate = mock_app.invalidate def track_invalidate(): call_order.append("invalidate") return original_invalidate() mock_app.invalidate = track_invalidate # Patch print to track when it's called with patch("builtins.print") as mock_print: mock_print.side_effect = lambda *args, **kwargs: call_order.append("print") # Simulate the background task output code path if cli_obj._app: cli_obj._app.invalidate() import time time.sleep(0.01) # reduced for test print() # Verify invalidate was called before print assert call_order[0] == "invalidate" assert "print" in call_order def test_invalidate_called_before_error_output(self): """App.invalidate() is called before printing background error output.""" cli_obj = _make_cli() mock_app = MagicMock() cli_obj._app = mock_app call_order = [] mock_app.invalidate.side_effect = lambda: call_order.append("invalidate") with patch("builtins.print") as mock_print: mock_print.side_effect = lambda *args, **kwargs: call_order.append("print") # Simulate error path if cli_obj._app: cli_obj._app.invalidate() import time time.sleep(0.01) print() assert call_order[0] == "invalidate" assert "print" in call_order def test_no_crash_when_app_is_none(self): """No crash when _app is None (non-TUI mode).""" cli_obj = _make_cli() cli_obj._app = None # This should not raise if cli_obj._app: cli_obj._app.invalidate() # If we get here without exception, test passes def test_background_task_thread_safety(self): """Background task tracking is thread-safe.""" cli_obj = _make_cli() # Simulate adding and removing background tasks task_id = "test_task_1" cli_obj._background_tasks[task_id] = MagicMock() assert task_id in cli_obj._background_tasks # Clean up cli_obj._background_tasks.pop(task_id, None) assert task_id not in cli_obj._background_tasks