mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Streamed responses no longer insert real newlines at terminal width — logical lines are emitted whole and the terminal soft-wraps them, so highlight-copy rejoins the full line (emulators only keep linebreaks the app actually printed). This is the CLI equivalent of the TUI's selection copy, which reads logical source lines from its screen buffer. TTFT perception is preserved by mirroring the unfinished line's tail into the spinner status text instead of chunk-printing. /copy now prefers OSC 52 when running over SSH (SSH_CONNECTION / SSH_TTY / SSH_CLIENT) — native tools there write the REMOTE clipboard, which is never what the user wants. The CLI's OSC 52 writer also gains tmux/screen DCS passthrough wrapping, mirroring the TUI's wrapForMultiplexer. Fixes #31528 for the CLI surface. Sabotage-verified: restoring the old chunk emitter fails 3 of the new tests (hard-wrap detection, spinner mirror, unbreakable-run split).
114 lines
4 KiB
Python
114 lines
4 KiB
Python
"""Tests for CLI /copy command."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from cli import HermesCLI
|
|
|
|
|
|
def _make_cli() -> HermesCLI:
|
|
cli_obj = HermesCLI.__new__(HermesCLI)
|
|
cli_obj.config = {}
|
|
cli_obj.console = MagicMock()
|
|
cli_obj.agent = None
|
|
cli_obj.conversation_history = []
|
|
cli_obj.session_id = "sess-copy-test"
|
|
cli_obj._pending_input = MagicMock()
|
|
cli_obj._app = None
|
|
return cli_obj
|
|
|
|
|
|
def test_copy_copies_latest_assistant_message():
|
|
cli_obj = _make_cli()
|
|
cli_obj.conversation_history = [
|
|
{"role": "user", "content": "hi"},
|
|
{"role": "assistant", "content": "first"},
|
|
{"role": "assistant", "content": "latest"},
|
|
]
|
|
|
|
with patch("hermes_cli.clipboard.write_clipboard_text", return_value=True) as mock_copy:
|
|
result = cli_obj.process_command("/copy")
|
|
|
|
assert result is True
|
|
mock_copy.assert_called_once_with("latest")
|
|
|
|
|
|
def test_copy_with_index_uses_requested_assistant_message():
|
|
cli_obj = _make_cli()
|
|
cli_obj.conversation_history = [
|
|
{"role": "assistant", "content": "one"},
|
|
{"role": "assistant", "content": "two"},
|
|
]
|
|
|
|
with patch("hermes_cli.clipboard.write_clipboard_text", return_value=True) as mock_copy:
|
|
cli_obj.process_command("/copy 1")
|
|
|
|
mock_copy.assert_called_once_with("one")
|
|
|
|
|
|
def test_copy_strips_reasoning_blocks_before_copy():
|
|
cli_obj = _make_cli()
|
|
cli_obj.conversation_history = [
|
|
{
|
|
"role": "assistant",
|
|
"content": "<REASONING_SCRATCHPAD>internal</REASONING_SCRATCHPAD>\nVisible answer",
|
|
}
|
|
]
|
|
|
|
with patch("hermes_cli.clipboard.write_clipboard_text", return_value=True) as mock_copy:
|
|
cli_obj.process_command("/copy")
|
|
|
|
mock_copy.assert_called_once_with("Visible answer")
|
|
|
|
|
|
def test_copy_falls_back_to_osc52_when_native_tools_fail():
|
|
cli_obj = _make_cli()
|
|
cli_obj.conversation_history = [{"role": "assistant", "content": "hello"}]
|
|
|
|
with patch("hermes_cli.clipboard.write_clipboard_text", return_value=False), \
|
|
patch("hermes_cli.clipboard.is_remote_shell_session", return_value=False), \
|
|
patch.object(cli_obj, "_write_osc52_clipboard") as mock_osc52:
|
|
cli_obj.process_command("/copy")
|
|
|
|
mock_osc52.assert_called_once_with("hello")
|
|
|
|
|
|
def test_copy_prefers_osc52_in_ssh_sessions():
|
|
"""Over SSH, native tools write the REMOTE clipboard — OSC 52 reaches
|
|
the local terminal instead (#31528)."""
|
|
cli_obj = _make_cli()
|
|
cli_obj.conversation_history = [{"role": "assistant", "content": "remote answer"}]
|
|
|
|
with patch("hermes_cli.clipboard.write_clipboard_text", return_value=True) as mock_native, \
|
|
patch("hermes_cli.clipboard.is_remote_shell_session", return_value=True), \
|
|
patch.object(cli_obj, "_write_osc52_clipboard") as mock_osc52:
|
|
cli_obj.process_command("/copy")
|
|
|
|
mock_osc52.assert_called_once_with("remote answer")
|
|
mock_native.assert_not_called()
|
|
|
|
|
|
def test_copy_native_first_when_local():
|
|
cli_obj = _make_cli()
|
|
cli_obj.conversation_history = [{"role": "assistant", "content": "local answer"}]
|
|
|
|
with patch("hermes_cli.clipboard.write_clipboard_text", return_value=True) as mock_native, \
|
|
patch("hermes_cli.clipboard.is_remote_shell_session", return_value=False), \
|
|
patch.object(cli_obj, "_write_osc52_clipboard") as mock_osc52:
|
|
cli_obj.process_command("/copy")
|
|
|
|
mock_native.assert_called_once_with("local answer")
|
|
mock_osc52.assert_not_called()
|
|
|
|
|
|
def test_copy_invalid_index_does_not_copy():
|
|
cli_obj = _make_cli()
|
|
cli_obj.conversation_history = [{"role": "assistant", "content": "only"}]
|
|
|
|
with patch("hermes_cli.clipboard.write_clipboard_text") as mock_copy, \
|
|
patch.object(cli_obj, "_write_osc52_clipboard") as mock_osc52, \
|
|
patch("cli._cprint") as mock_print:
|
|
cli_obj.process_command("/copy 99")
|
|
|
|
mock_copy.assert_not_called()
|
|
mock_osc52.assert_not_called()
|
|
assert any("Invalid response number" in str(call) for call in mock_print.call_args_list)
|