#!/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()