106 lines
3.3 KiB
Python
106 lines
3.3 KiB
Python
|
|
"""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
|