mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-10 03:22:05 +00:00
fix(cli): recover classic CLI output after resize
This commit is contained in:
parent
17687911b7
commit
76074d9ee6
5 changed files with 389 additions and 21 deletions
|
|
@ -13,6 +13,7 @@ from unittest.mock import MagicMock
|
|||
|
||||
import pytest
|
||||
|
||||
import cli as cli_mod
|
||||
from cli import HermesCLI
|
||||
|
||||
|
||||
|
|
@ -33,10 +34,18 @@ class TestForceFullRedraw:
|
|||
# Simulate HermesCLI before the TUI has ever been constructed.
|
||||
bare_cli._force_full_redraw() # must not raise
|
||||
|
||||
def test_sends_full_clear_and_invalidates(self, bare_cli):
|
||||
def test_sends_full_clear_replays_then_invalidates(self, bare_cli, monkeypatch):
|
||||
app = MagicMock()
|
||||
out = app.renderer.output
|
||||
bare_cli._app = app
|
||||
events = []
|
||||
out.reset_attributes.side_effect = lambda: events.append("reset_attrs")
|
||||
out.erase_screen.side_effect = lambda: events.append("erase")
|
||||
out.cursor_goto.side_effect = lambda *_: events.append("home")
|
||||
out.flush.side_effect = lambda: events.append("flush")
|
||||
app.renderer.reset.side_effect = lambda **_: events.append("renderer_reset")
|
||||
monkeypatch.setattr(cli_mod, "_replay_output_history", lambda: events.append("replay"))
|
||||
app.invalidate.side_effect = lambda: events.append("invalidate")
|
||||
|
||||
bare_cli._force_full_redraw()
|
||||
|
||||
|
|
@ -52,6 +61,109 @@ class TestForceFullRedraw:
|
|||
|
||||
# Must schedule a repaint.
|
||||
app.invalidate.assert_called_once()
|
||||
assert events == [
|
||||
"reset_attrs",
|
||||
"erase",
|
||||
"home",
|
||||
"flush",
|
||||
"renderer_reset",
|
||||
"replay",
|
||||
"invalidate",
|
||||
]
|
||||
|
||||
def test_resize_rebuilds_scrollback_before_prompt_toolkit_redraw(self, bare_cli, monkeypatch):
|
||||
app = MagicMock()
|
||||
out = app.renderer.output
|
||||
events = []
|
||||
out.reset_attributes.side_effect = lambda: events.append("reset_attrs")
|
||||
out.erase_screen.side_effect = lambda: events.append("erase")
|
||||
out.write_raw.side_effect = lambda text: events.append(("raw", text))
|
||||
out.cursor_goto.side_effect = lambda *_: events.append("home")
|
||||
out.flush.side_effect = lambda: events.append("flush")
|
||||
app.renderer.reset.side_effect = lambda **_: events.append("renderer_reset")
|
||||
monkeypatch.setattr(cli_mod, "_replay_output_history", lambda: events.append("replay"))
|
||||
original_on_resize = lambda: events.append("original_resize")
|
||||
|
||||
bare_cli._recover_after_resize(app, original_on_resize)
|
||||
|
||||
assert events == [
|
||||
"reset_attrs",
|
||||
"erase",
|
||||
("raw", "\x1b[3J"),
|
||||
"home",
|
||||
"flush",
|
||||
"renderer_reset",
|
||||
"replay",
|
||||
"original_resize",
|
||||
]
|
||||
app.invalidate.assert_not_called()
|
||||
|
||||
def test_force_redraw_uses_full_screen_clear_without_scrollback_clear(self, bare_cli):
|
||||
app = MagicMock()
|
||||
bare_cli._app = app
|
||||
|
||||
bare_cli._force_full_redraw()
|
||||
|
||||
app.renderer.output.erase_screen.assert_called_once()
|
||||
app.renderer.output.cursor_goto.assert_called_once_with(0, 0)
|
||||
app.renderer.output.write_raw.assert_not_called()
|
||||
|
||||
def test_resize_recovery_is_debounced(self, bare_cli, monkeypatch):
|
||||
timers = []
|
||||
calls = []
|
||||
|
||||
class FakeTimer:
|
||||
def __init__(self, delay, callback):
|
||||
self.delay = delay
|
||||
self.callback = callback
|
||||
self.cancelled = False
|
||||
self.daemon = False
|
||||
timers.append(self)
|
||||
|
||||
def start(self):
|
||||
calls.append(("start", self.delay))
|
||||
|
||||
def cancel(self):
|
||||
self.cancelled = True
|
||||
calls.append(("cancel", self.delay))
|
||||
|
||||
def fire(self):
|
||||
self.callback()
|
||||
|
||||
app = MagicMock()
|
||||
app.loop.call_soon_threadsafe.side_effect = lambda cb: cb()
|
||||
monkeypatch.setattr(cli_mod.threading, "Timer", FakeTimer)
|
||||
monkeypatch.setattr(
|
||||
bare_cli,
|
||||
"_recover_after_resize",
|
||||
lambda _app, _orig: calls.append(("recover", _orig())),
|
||||
)
|
||||
|
||||
original_one = lambda: "first"
|
||||
original_two = lambda: "second"
|
||||
|
||||
bare_cli._schedule_resize_recovery(app, original_one, delay=0.25)
|
||||
assert bare_cli._resize_recovery_pending is True
|
||||
bare_cli._schedule_resize_recovery(app, original_two, delay=0.25)
|
||||
|
||||
assert len(timers) == 2
|
||||
assert timers[0].cancelled is True
|
||||
timers[0].fire()
|
||||
assert ("recover", "first") not in calls
|
||||
|
||||
timers[1].fire()
|
||||
assert ("recover", "second") in calls
|
||||
assert bare_cli._resize_recovery_pending is False
|
||||
|
||||
def test_invalidate_is_suppressed_while_resize_recovery_is_pending(self, bare_cli):
|
||||
app = MagicMock()
|
||||
bare_cli._app = app
|
||||
bare_cli._last_invalidate = 0.0
|
||||
bare_cli._resize_recovery_pending = True
|
||||
|
||||
bare_cli._invalidate(min_interval=0)
|
||||
|
||||
app.invalidate.assert_not_called()
|
||||
|
||||
def test_swallows_renderer_exceptions(self, bare_cli):
|
||||
# If the renderer blows up for any reason, the helper must not
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue