* refactor: re-architect tests to mirror the codebase
* Update tests.yml
* fix: add missing tool_error imports after registry refactor
* fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist
patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers,
causing test_code_execution tests to hit the Modal remote path.
* fix(tests): fix update_check and telegram xdist failures
- test_update_check: replace patch("hermes_cli.banner.os.getenv") with
monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os
directly, it uses get_hermes_home() from hermes_constants.
- test_telegram_conflict/approval_buttons: provide real exception classes
for telegram.error mock (NetworkError, TimedOut, BadRequest) so the
except clause in connect() doesn't fail with "catching classes that do
not inherit from BaseException" when xdist pollutes sys.modules.
* fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock
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
|