b7e45010c3
- 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
180 lines
7.3 KiB
Python
180 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
test_clean_transcript.py — Unit and integration tests for clean line-by-line transcript view:
|
|
1. Default display style is 'clean' (no box borders, no bottom rules, no vertical pipe bars).
|
|
2. Clean mode renders bullet header indicators (● AGENT, ▸ YOU) and 2-space indented body lines.
|
|
3. Boxed mode retains classic ASCII/Unicode frames (┌──, │ , └───).
|
|
4. Runtime style toggling via 'b' / 'B' hotkey in Normal mode.
|
|
5. Slash commands: /clean, /boxed, /view, /compact.
|
|
6. Selection and copy extraction (_copy_rendered_line_range) handles clean formatting seamlessly.
|
|
7. Settings persistence in ~/.config/muse-cli/tui_settings.json.
|
|
"""
|
|
import curses
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
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
|
|
|
|
|
|
class TestCleanTranscript(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()
|
|
|
|
self.messages = [
|
|
{
|
|
"role": "assistant",
|
|
"text": "Same sweep thread, routine — holding on dev-i04.\nSecond line of response.",
|
|
"message_id": "ast-msg-86648",
|
|
"seq": 86648,
|
|
},
|
|
{
|
|
"role": "user",
|
|
"text": "Roger that, continue monitoring.\n[WO:20261006-1] Active",
|
|
"message_id": "usr-msg-d183a734",
|
|
"seq": 86649,
|
|
},
|
|
]
|
|
with self.tui.data.lock:
|
|
self.tui.data.history_cache[(self.tui.data.active_node, self.tui.data.active_thread_id)] = self.messages
|
|
|
|
def test_default_style_is_clean(self):
|
|
"""Verify that newly initialized TUI defaults to 'clean' transcript style."""
|
|
self.assertEqual(self.tui.transcript_style, "clean")
|
|
|
|
def test_render_transcript_clean_mode(self):
|
|
"""In 'clean' mode, lines do NOT contain ┌──, │ , or └───, and use clean bullets."""
|
|
self.tui.transcript_style = "clean"
|
|
self.tui._transcript_cache_key = None
|
|
self.tui._render_transcript(1, 0, 25, 90)
|
|
|
|
lines = [item[0] for item in self.tui._transcript_cache_lines]
|
|
combined = "\n".join(lines)
|
|
|
|
# Must not contain box border frames
|
|
self.assertNotIn("┌──", combined)
|
|
self.assertNotIn("└──", combined)
|
|
self.assertNotIn("│ ", combined)
|
|
|
|
# Must contain clean indicators
|
|
self.assertTrue(any(l.startswith("● MUSE") for l in lines), "Agent header must start with clean bullet ●")
|
|
self.assertTrue(any(l.startswith("▸ YOU") for l in lines), "User header must start with clean prompt ▸")
|
|
|
|
# Body lines must use 2-space indentation
|
|
self.assertTrue(any(l.startswith(" Same sweep thread") for l in lines))
|
|
self.assertTrue(any(l.startswith(" Second line") for l in lines))
|
|
self.assertTrue(any(l.startswith(" Roger that") for l in lines))
|
|
|
|
def test_render_transcript_boxed_mode(self):
|
|
"""In 'boxed' mode, classic ASCII/Unicode frames (┌──, │ , └───) are rendered."""
|
|
self.tui.transcript_style = "boxed"
|
|
self.tui._transcript_cache_key = None
|
|
self.tui._render_transcript(1, 0, 25, 90)
|
|
|
|
lines = [item[0] for item in self.tui._transcript_cache_lines]
|
|
combined = "\n".join(lines)
|
|
|
|
self.assertIn("┌── [MUSE / MUSE AGENT]", combined)
|
|
self.assertIn("┌── [YOU / OPERATOR]", combined)
|
|
self.assertIn("│ Same sweep thread", combined)
|
|
self.assertIn("└──", combined)
|
|
|
|
def test_toggle_transcript_style(self):
|
|
"""toggle_transcript_style switches between clean and boxed, invalidating cache."""
|
|
self.tui.transcript_style = "clean"
|
|
self.tui._transcript_cache_key = ("some_key",)
|
|
|
|
self.tui.toggle_transcript_style()
|
|
self.assertEqual(self.tui.transcript_style, "boxed")
|
|
self.assertIsNone(self.tui._transcript_cache_key)
|
|
self.assertIn("Classic boxed", self.tui.toast_msg)
|
|
|
|
self.tui.toggle_transcript_style()
|
|
self.assertEqual(self.tui.transcript_style, "clean")
|
|
self.assertIsNone(self.tui._transcript_cache_key)
|
|
self.assertIn("Clean line-by-line", self.tui.toast_msg)
|
|
|
|
def test_normal_mode_hotkey_b_toggles_style(self):
|
|
"""Pressing 'b' or 'B' in normal mode toggles transcript style."""
|
|
self.tui.editor_mode = "NORMAL"
|
|
self.tui.copy_mode = False
|
|
self.tui.transcript_style = "clean"
|
|
|
|
handled = self.tui._handle_key(ord('b'))
|
|
self.assertTrue(handled)
|
|
self.assertEqual(self.tui.transcript_style, "boxed")
|
|
|
|
handled = self.tui._handle_key(ord('B'))
|
|
self.assertTrue(handled)
|
|
self.assertEqual(self.tui.transcript_style, "clean")
|
|
|
|
def test_slash_commands_clean_and_boxed(self):
|
|
"""Slash commands /clean, /boxed, and /view correctly set style."""
|
|
self.tui.transcript_style = "clean"
|
|
|
|
# /boxed
|
|
self.tui._execute_input_line("/boxed")
|
|
self.assertEqual(self.tui.transcript_style, "boxed")
|
|
|
|
# /clean
|
|
self.tui._execute_input_line("/clean")
|
|
self.assertEqual(self.tui.transcript_style, "clean")
|
|
|
|
# /view box
|
|
self.tui._execute_input_line("/view box")
|
|
self.assertEqual(self.tui.transcript_style, "boxed")
|
|
|
|
# /view clean
|
|
self.tui._execute_input_line("/view clean")
|
|
self.assertEqual(self.tui.transcript_style, "clean")
|
|
|
|
# /style (toggles)
|
|
self.tui._execute_input_line("/style")
|
|
self.assertEqual(self.tui.transcript_style, "boxed")
|
|
|
|
def test_copy_range_in_clean_mode(self):
|
|
"""_copy_rendered_line_range strips clean 2-space indents and action buttons."""
|
|
self.tui.transcript_style = "clean"
|
|
self.tui._transcript_cache_key = None
|
|
self.tui._render_transcript(1, 0, 25, 90)
|
|
|
|
# Total lines in cache
|
|
lines = self.tui._transcript_cache_lines
|
|
copied = self.tui._copy_rendered_line_range(0, len(lines) - 1)
|
|
|
|
self.assertNotIn("│", copied)
|
|
self.assertNotIn("[📋 Copy]", copied)
|
|
self.assertNotIn("[↩ Reply]", copied)
|
|
self.assertIn("Same sweep thread", copied)
|
|
self.assertIn("Roger that, continue monitoring.", copied)
|
|
|
|
def test_settings_persistence(self):
|
|
"""Style preference is saved to and loaded from JSON configuration."""
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
fake_cfg = Path(tmp_dir) / "tui_settings.json"
|
|
with patch("pathlib.Path.home", return_value=Path(tmp_dir)):
|
|
with patch.object(Path, "mkdir"):
|
|
with patch("builtins.open", unittest.mock.mock_open()):
|
|
self.tui.set_transcript_style("boxed")
|
|
self.assertEqual(self.tui.transcript_style, "boxed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|