Files
box/tests/test_copy_actions.py
T
operator b7e45010c3 feat(tui): clean transcript style, right-click context menus, rate limits & dual copy
- Add clean line-by-line transcript style toggle ('b' key / /clean / /boxed)
- Implement right-click context menus for chat list, fleet agent panel, and transcript
- Add cooldown mode lock bypass (2-second double-confirm force sync)
- Implement dual copy support: clean text (strip reply metadata) vs full context
- Add clickable [📋 Copy] and [📑+ Context] buttons to message headers
- Add unit test suites for transcript cleaning, context menus, rate limits, and copy actions
2026-10-06 07:55:27 +00:00

375 lines
17 KiB
Python

#!/usr/bin/env python3
"""
test_copy_actions.py — Unit and integration tests for:
1. Multi-environment clipboard engine (desktop utilities, OSC 52, internal buffer).
2. Transcript formatting engine (Markdown structured headers, roles, seq, id).
3. UI-first button approach:
- Clickable [📋 Copy All] in thread header.
- Clickable [📋 Copy] button on each message header.
4. Normal mode hotkeys:
- 'y': copy latest/active message to clipboard.
- 'Y': copy entire chat transcript to clipboard.
5. Slash commands:
- '/copy', '/yank', '/cp': copy full transcript.
- '/copy last', '/copy message': copy latest message.
"""
import curses
import importlib.util
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
REPO_ROOT = Path(__file__).resolve().parent.parent
MUSE_TUI_PATH = REPO_ROOT / "bin" / "muse-tui.py"
spec = importlib.util.spec_from_file_location("muse_tui", MUSE_TUI_PATH)
muse_tui = importlib.util.module_from_spec(spec)
sys.modules["muse_tui"] = muse_tui
spec.loader.exec_module(muse_tui)
MuseTUI = muse_tui.MuseTUI
copy_to_clipboard = muse_tui.copy_to_clipboard
strip_message_metadata = muse_tui.strip_message_metadata
class TestCopyActions(unittest.TestCase):
def setUp(self):
self.mock_stdscr = MagicMock()
self.mock_stdscr.getmaxyx.return_value = (30, 100)
self.tui = MuseTUI(self.mock_stdscr, initial_mode="muse", initial_node="muse")
self.tui.safe_addstr = MagicMock()
# Seed conversation history for testing
self.messages = [
{
"role": "user",
"text": "Hello muse, check system status",
"message_id": "usr-msg-12345678",
"seq": 1,
},
{
"role": "assistant",
"text": "All services nominal.\nCDP port 9222 active.",
"message_id": "ast-msg-87654321",
"seq": 2,
},
{
"role": "user",
"text": "Please summarize logs",
"message_id": "usr-msg-99999999",
"seq": 3,
},
]
self.tui.data.history_cache[("muse", "thread-abc")] = list(self.messages)
self.tui.data.active_node = "muse"
self.tui.data.active_thread_id = "thread-abc"
self.tui.data.active_thread_title = "Diagnostics"
def test_strip_message_metadata(self):
"""Verify strip_message_metadata strips reply headers and unquotes quoted lines."""
raw = (
"> Replying to ASSISTANT (seq:12620):\n"
"> Relevant to the timer question: the loop scout just surfaced 6 unanswered jobs.\n"
"> So part of getting work done is already queued.\n\n"
"yes, proceed with spawning 5 sub agents to assist you ; continue at all costs"
)
expected = (
"Relevant to the timer question: the loop scout just surfaced 6 unanswered jobs.\n"
"So part of getting work done is already queued.\n\n"
"yes, proceed with spawning 5 sub agents to assist you ; continue at all costs"
)
self.assertEqual(strip_message_metadata(raw), expected)
# Message without reply tags remains unchanged
no_tags = "Standard message without reply headers."
self.assertEqual(strip_message_metadata(no_tags), no_tags)
def test_copy_to_clipboard_osc52(self):
"""Verify copy_to_clipboard formats and flushes OSC 52 sequence."""
with patch("sys.stdout.write") as mock_write, patch("sys.stdout.flush"):
ok = copy_to_clipboard("test-copy-payload")
self.assertTrue(ok)
# Verify OSC 52 sequence was written
args = [call.args[0] for call in mock_write.call_args_list]
osc_written = any("\033]52;c;" in a for a in args)
self.assertTrue(osc_written)
def test_get_formatted_transcript_content(self):
"""Verify structured Markdown output contains thread title, roles, seq, id, and text."""
formatted = self.tui.get_formatted_transcript()
self.assertIn("# Chat Transcript: Diagnostics", formatted)
self.assertIn("Node: MUSE | Thread ID: thread-abc | Total Messages: 3", formatted)
self.assertIn("### [YOU / OPERATOR] id:usr-msg-", formatted)
self.assertIn("Hello muse, check system status", formatted)
self.assertIn("### [MUSE / MUSE AGENT] seq:2 id:ast-msg-", formatted)
self.assertIn("All services nominal.", formatted)
self.assertIn("Please summarize logs", formatted)
def test_copy_transcript_to_clipboard(self):
"""Verify copy_transcript_to_clipboard updates internal buffer and sets success toast."""
with patch.object(muse_tui, "copy_to_clipboard", return_value=True):
ok = self.tui.copy_transcript_to_clipboard()
self.assertTrue(ok)
self.assertIn("# Chat Transcript: Diagnostics", self.tui.clipboard_buf)
self.assertIn("Copied full transcript (3 msgs", self.tui.toast_msg)
def test_copy_message_to_clipboard_latest(self):
"""Verify copy_message_to_clipboard defaults to latest message."""
with patch.object(muse_tui, "copy_to_clipboard", return_value=True):
ok = self.tui.copy_message_to_clipboard()
self.assertTrue(ok)
self.assertEqual(self.tui.clipboard_buf, "Please summarize logs")
self.assertIn("Copied clean message to clipboard", self.tui.toast_msg)
def test_copy_message_to_clipboard_with_and_without_context(self):
"""Verify with_context toggles between metadata stripping and raw text."""
raw = "> Replying to ASSISTANT (seq:1):\n> Quoted header\n\nActual response body"
with patch.object(muse_tui, "copy_to_clipboard", return_value=True):
# Clean text (without context)
self.tui.copy_message_to_clipboard(raw, with_context=False)
self.assertEqual(self.tui.clipboard_buf, "Quoted header\n\nActual response body")
self.assertIn("clean message", self.tui.toast_msg)
# Raw text (with context)
self.tui.copy_message_to_clipboard(raw, with_context=True)
self.assertEqual(self.tui.clipboard_buf, raw)
self.assertIn("message with context", self.tui.toast_msg)
def test_copy_message_to_clipboard_explicit(self):
"""Verify copy_message_to_clipboard with explicit message text."""
with patch.object(muse_tui, "copy_to_clipboard", return_value=True):
custom_text = "Specific arbitrary message payload"
ok = self.tui.copy_message_to_clipboard(custom_text)
self.assertTrue(ok)
self.assertEqual(self.tui.clipboard_buf, custom_text)
def test_render_transcript_buttons(self):
"""Verify [📋 Copy All], [📋 Copy] and [📑+ Context] buttons are rendered and registered."""
self.tui._render_transcript(y=1, x=24, h=25, w=76)
# Check [📋 Copy All] bounds registered
self.assertIsNotNone(self.tui._btn_copy_all_bounds)
btn_y, x_start, x_end = self.tui._btn_copy_all_bounds
self.assertEqual(btn_y, 1)
self.assertGreater(x_end, x_start)
# Verify button string was rendered to screen at btn coordinates
calls = [c for c in self.tui.safe_addstr.call_args_list if "[📋 Copy All]" in str(c)]
self.assertTrue(len(calls) > 0, "[📋 Copy All] must be rendered to stdscr")
win_arg, call_y, call_x, call_text, call_attr = calls[0].args
self.assertEqual(call_y, 1)
self.assertEqual(call_x, x_start)
# Check individual message copy buttons registered
self.assertGreater(len(self.tui._msg_copy_buttons), 0)
self.assertGreater(len(self.tui._msg_copy_context_buttons), 0)
for (my, mx1, mx2, raw_text) in self.tui._msg_copy_buttons:
self.assertGreater(mx2, mx1)
self.assertIn(raw_text, [m["text"] for m in self.messages])
for (my, mx1, mx2, raw_text) in self.tui._msg_copy_context_buttons:
self.assertGreater(mx2, mx1)
self.assertIn(raw_text, [m["text"] for m in self.messages])
def test_render_transcript_narrow_screen_retains_copy_button(self):
"""Verify [📋 Copy All] is retained next to message count even with long title & narrow width."""
self.tui.data.active_thread_title = "Extremely Long Comprehensive Diagnostics Thread Title Exceeding Width"
self.tui._render_transcript(y=1, x=10, h=20, w=48)
self.assertIsNotNone(self.tui._btn_copy_all_bounds)
btn_y, x_start, x_end = self.tui._btn_copy_all_bounds
self.assertEqual(btn_y, 1)
self.assertLessEqual(x_end, 10 + 48, "Button must stay within transcript boundary")
calls = [c for c in self.tui.safe_addstr.call_args_list if "[📋 Copy All]" in str(c)]
self.assertTrue(len(calls) > 0)
def test_mouse_click_copy_all(self):
"""Verify clicking [📋 Copy All] copies full transcript to clipboard."""
self.tui._render_transcript(y=1, x=24, h=25, w=76)
btn_y, x_start, x_end = self.tui._btn_copy_all_bounds
click_x = (x_start + x_end) // 2
with patch.object(self.tui, "copy_transcript_to_clipboard") as mock_copy_all:
handled = self.tui._handle_mouse(click_x, btn_y, curses.BUTTON1_CLICKED)
self.assertTrue(handled)
mock_copy_all.assert_called_once()
def test_mouse_click_message_copy(self):
"""Verify clicking an individual [📋 Copy] button copies that specific message without context."""
self.tui._render_transcript(y=1, x=24, h=25, w=76)
self.assertGreater(len(self.tui._msg_copy_buttons), 0)
first_btn = self.tui._msg_copy_buttons[0]
msg_y, x_start, x_end, raw_text = first_btn
click_x = (x_start + x_end) // 2
with patch.object(self.tui, "copy_message_to_clipboard") as mock_copy_msg:
handled = self.tui._handle_mouse(click_x, msg_y, curses.BUTTON1_CLICKED)
self.assertTrue(handled)
mock_copy_msg.assert_called_once_with(raw_text, with_context=False)
def test_mouse_click_message_copy_context(self):
"""Verify clicking an individual [📑+ Context] button copies that message with full context."""
self.tui._render_transcript(y=1, x=24, h=25, w=76)
self.assertGreater(len(self.tui._msg_copy_context_buttons), 0)
first_btn = self.tui._msg_copy_context_buttons[0]
msg_y, x_start, x_end, raw_text = first_btn
click_x = (x_start + x_end) // 2
with patch.object(self.tui, "copy_message_to_clipboard") as mock_copy_msg:
handled = self.tui._handle_mouse(click_x, msg_y, curses.BUTTON1_CLICKED)
self.assertTrue(handled)
mock_copy_msg.assert_called_once_with(raw_text, with_context=True)
def test_normal_hotkey_y(self):
"""Verify 'y' in Normal mode copies latest message."""
self.tui.editor_mode = "NORMAL"
with patch.object(self.tui, "copy_message_to_clipboard") as mock_copy_msg:
handled = self.tui._handle_normal_key(ord("y"))
self.assertTrue(handled)
mock_copy_msg.assert_called_once()
def test_normal_hotkey_Y(self):
"""Verify 'Y' in Normal mode copies full transcript."""
self.tui.editor_mode = "NORMAL"
with patch.object(self.tui, "copy_transcript_to_clipboard") as mock_copy_all:
handled = self.tui._handle_normal_key(ord("Y"))
self.assertTrue(handled)
mock_copy_all.assert_called_once()
def test_slash_command_copy_all(self):
"""Verify '/copy' or '/yank' copies full transcript."""
with patch.object(self.tui, "copy_transcript_to_clipboard") as mock_copy_all:
self.tui._execute_input_line("/copy")
mock_copy_all.assert_called_once()
with patch.object(self.tui, "copy_transcript_to_clipboard") as mock_copy_all:
self.tui._execute_input_line("/yank")
mock_copy_all.assert_called_once()
def test_slash_command_copy_last(self):
"""Verify '/copy last' copies latest message."""
with patch.object(self.tui, "copy_message_to_clipboard") as mock_copy_msg:
self.tui._execute_input_line("/copy last")
mock_copy_msg.assert_called_once()
def test_enter_visual_copy_mode(self):
"""Pressing 'v' enters Visual Copy Mode with selection on visible line."""
self.tui.editor_mode = "NORMAL"
self.tui.copy_mode = False
self.tui._render_transcript(y=1, x=24, h=25, w=76)
ret = self.tui._handle_normal_key(ord('v'))
self.assertTrue(ret)
self.assertTrue(self.tui.copy_mode)
self.assertEqual(self.tui.focus_pane, "transcript")
self.assertIsNotNone(self.tui.visual_sel_start)
self.assertEqual(self.tui.visual_sel_start, self.tui.copy_cursor_line)
self.assertIn("VISUAL line copy mode", self.tui.toast_msg)
def test_enter_visual_copy_mode_V(self):
"""Pressing 'V' on transcript enters visual copy mode."""
self.tui.editor_mode = "NORMAL"
self.tui.focus_pane = "transcript"
self.tui.copy_mode = False
self.tui._render_transcript(y=1, x=24, h=25, w=76)
ret = self.tui._handle_normal_key(ord('V'))
self.assertTrue(ret)
self.assertTrue(self.tui.copy_mode)
self.assertIsNotNone(self.tui.visual_sel_start)
self.assertIn("VISUAL line copy mode", self.tui.toast_msg)
def test_message_context_menu_copy_clean_and_context(self):
"""Verify message_context modal options 2/y (clean) and 3/Y (context) work."""
target_msg = {
"role": "assistant",
"text": "> Replying to USER (seq:4):\n> Previous line\n\nClean response text",
"message_id": "msg-xyz-123",
"seq": 5,
}
self.tui.context_message = target_msg
self.tui.modal = "message_context"
with patch.object(self.tui, "copy_message_to_clipboard") as mock_copy:
# Key '2' or 'y' copies clean text (with_context=False)
self.tui._handle_modal_key(ord('2'))
mock_copy.assert_called_with(target_msg["text"], with_context=False)
self.tui.context_message = target_msg
self.tui.modal = "message_context"
with patch.object(self.tui, "copy_message_to_clipboard") as mock_copy:
# Key '3' or 'Y' copies with context (with_context=True)
self.tui._handle_modal_key(ord('3'))
mock_copy.assert_called_with(target_msg["text"], with_context=True)
def test_copy_mode_navigation_and_yank_with_border_stripping(self):
"""Navigating with 'j'/'k' and yanking with 'y' copies clean text without box borders."""
self.tui._render_transcript(y=1, x=24, h=25, w=76)
self.tui.copy_mode = True
self.tui.visual_sel_start = 2
self.tui.copy_cursor_line = 2
# Move down to line 4
self.tui._handle_copy_mode_key(ord('j'))
self.assertEqual(self.tui.copy_cursor_line, 3)
self.tui._handle_copy_mode_key(ord('j'))
self.assertEqual(self.tui.copy_cursor_line, 4)
# Yank selection with 'y'
with patch.object(muse_tui, "copy_to_clipboard", return_value=True) as mock_clip:
self.tui._handle_copy_mode_key(ord('y'))
self.assertFalse(self.tui.copy_mode)
self.assertIsNone(self.tui.visual_sel_start)
mock_clip.assert_called_once()
copied = mock_clip.call_args[0][0]
# Ensure vertical box lines '│ ' are stripped
self.assertNotIn("│ ", copied)
self.assertIn("Copied", self.tui.toast_msg)
def test_mouse_drag_select_and_release_copy(self):
"""Dragging mouse over transcript lines highlights and copies text on release."""
self.tui._render_transcript(y=1, x=24, h=25, w=76)
# 1. Mouse down at screen y=5, x=30 (inside transcript)
self.tui._handle_mouse(mx=30, my=5, bstate=curses.BUTTON1_PRESSED)
self.assertTrue(self.tui.mouse_dragging)
self.assertTrue(self.tui.copy_mode)
start_line = self.tui.visual_sel_start
self.assertIsNotNone(start_line)
# 2. Mouse drag to y=8
self.tui._handle_mouse(mx=30, my=8, bstate=curses.REPORT_MOUSE_POSITION)
self.assertEqual(self.tui.visual_sel_start, start_line)
self.assertNotEqual(self.tui.copy_cursor_line, start_line)
# 3. Mouse release
with patch.object(muse_tui, "copy_to_clipboard", return_value=True) as mock_clip:
self.tui._handle_mouse(mx=30, my=8, bstate=curses.BUTTON1_RELEASED)
self.assertFalse(self.tui.mouse_dragging)
self.assertFalse(self.tui.copy_mode)
mock_clip.assert_called_once()
copied = mock_clip.call_args[0][0]
self.assertGreater(len(copied), 0)
self.assertIn("Copied selection", self.tui.toast_msg)
def test_copy_mode_escape_exits(self):
"""Esc or 'q' clears selection and exits copy mode."""
self.tui.copy_mode = True
self.tui.visual_sel_start = 5
self.tui.copy_cursor_line = 8
# First Esc clears selection
self.tui._handle_copy_mode_key(27)
self.assertIsNone(self.tui.visual_sel_start)
self.assertTrue(self.tui.copy_mode)
# Second Esc exits copy mode
self.tui._handle_copy_mode_key(27)
self.assertFalse(self.tui.copy_mode)
if __name__ == "__main__":
unittest.main()