2025-11-07 19:45:01 +00:00

239 lines
7.5 KiB
Python

"""Tests for the conversation visualizer and event visualization."""
import json
from rich.text import Text
from openhands_cli.tui.visualizer import (
CLIVisualizer,
)
from openhands.sdk.event import (
ActionEvent,
SystemPromptEvent,
UserRejectObservation,
)
from openhands.sdk.llm import (
MessageToolCall,
TextContent,
)
from openhands.sdk.tool import Action
class VisualizerMockAction(Action):
"""Mock action for testing."""
command: str = "test command"
working_dir: str = "/tmp"
class VisualizerCustomAction(Action):
"""Custom action with overridden visualize method."""
task_list: list[dict] = []
@property
def visualize(self) -> Text:
"""Custom visualization for task tracker."""
content = Text()
content.append("Task Tracker Action\n", style="bold")
content.append(f"Tasks: {len(self.task_list)}")
for i, task in enumerate(self.task_list):
content.append(f"\n {i + 1}. {task.get('title', 'Untitled')}")
return content
def create_tool_call(
call_id: str, function_name: str, arguments: dict
) -> MessageToolCall:
"""Helper to create a MessageToolCall."""
return MessageToolCall(
id=call_id,
name=function_name,
arguments=json.dumps(arguments),
origin="completion",
)
def test_conversation_visualizer_initialization():
"""Test DefaultConversationVisualizer can be initialized."""
visualizer = CLIVisualizer()
assert visualizer is not None
assert hasattr(visualizer, "on_event")
assert hasattr(visualizer, "_create_event_panel")
def test_visualizer_event_panel_creation():
"""Test that visualizer creates panels for different event types."""
conv_viz = CLIVisualizer()
# Test with a simple action event
action = VisualizerMockAction(command="test")
tool_call = create_tool_call("call_1", "test", {})
action_event = ActionEvent(
thought=[TextContent(text="Testing")],
action=action,
tool_name="test",
tool_call_id="call_1",
tool_call=tool_call,
llm_response_id="response_1",
)
panel = conv_viz._create_event_panel(action_event)
assert panel is not None
assert hasattr(panel, "renderable")
def test_visualizer_action_event_with_none_action_panel():
"""ActionEvent with action=None should render as 'Agent Action (Not Executed)'."""
visualizer = CLIVisualizer()
tc = create_tool_call("call_ne_1", "missing_fn", {})
action_event = ActionEvent(
thought=[TextContent(text="...")],
tool_call=tc,
tool_name=tc.name,
tool_call_id=tc.id,
llm_response_id="resp_viz_1",
action=None,
)
panel = visualizer._create_event_panel(action_event)
assert panel is not None
# Ensure it doesn't fall back to UNKNOWN
assert "UNKNOWN Event" not in str(panel.title)
# And uses the 'Agent Action (Not Executed)' title
assert "Agent Action (Not Executed)" in str(panel.title)
def test_visualizer_user_reject_observation_panel():
"""UserRejectObservation should render a dedicated panel."""
visualizer = CLIVisualizer()
event = UserRejectObservation(
tool_name="demo_tool",
tool_call_id="fc_call_1",
action_id="action_1",
rejection_reason="User rejected the proposed action.",
)
panel = visualizer._create_event_panel(event)
assert panel is not None
title = str(panel.title)
assert "UNKNOWN Event" not in title
assert "User Rejected Action" in title
# ensure the reason is part of the renderable text
renderable = panel.renderable
assert isinstance(renderable, Text)
assert "User rejected the proposed action." in renderable.plain
def test_metrics_formatting():
"""Test metrics subtitle formatting."""
from unittest.mock import MagicMock
from openhands.sdk.conversation.conversation_stats import ConversationStats
from openhands.sdk.llm.utils.metrics import Metrics
# Create conversation stats with metrics
conversation_stats = ConversationStats()
# Create metrics and add to conversation stats
metrics = Metrics(model_name="test-model")
metrics.add_cost(0.0234)
metrics.add_token_usage(
prompt_tokens=1500,
completion_tokens=500,
cache_read_tokens=300,
cache_write_tokens=0,
reasoning_tokens=200,
context_window=8000,
response_id="test_response",
)
# Add metrics to conversation stats
conversation_stats.usage_to_metrics["test_usage"] = metrics
# Create visualizer and initialize with mock state
visualizer = CLIVisualizer()
mock_state = MagicMock()
mock_state.stats = conversation_stats
visualizer.initialize(mock_state)
# Test the metrics subtitle formatting
subtitle = visualizer._format_metrics_subtitle()
assert subtitle is not None
assert "1.5K" in subtitle # Input tokens abbreviated (trailing zeros removed)
assert "500" in subtitle # Output tokens
assert "20.00%" in subtitle # Cache hit rate
assert "200" in subtitle # Reasoning tokens
assert "0.0234" in subtitle # Cost
def test_metrics_abbreviation_formatting():
"""Test number abbreviation with various edge cases."""
from unittest.mock import MagicMock
from openhands.sdk.conversation.conversation_stats import ConversationStats
from openhands.sdk.llm.utils.metrics import Metrics
test_cases = [
# (input_tokens, expected_abbr)
(999, "999"), # Below threshold
(1000, "1K"), # Exact K boundary, trailing zeros removed
(1500, "1.5K"), # K with one decimal, trailing zero removed
(89080, "89.08K"), # K with two decimals (regression test for bug)
(89000, "89K"), # K with trailing zeros removed
(1000000, "1M"), # Exact M boundary
(1234567, "1.23M"), # M with decimals
(1000000000, "1B"), # Exact B boundary
]
for tokens, expected in test_cases:
stats = ConversationStats()
metrics = Metrics(model_name="test-model")
metrics.add_token_usage(
prompt_tokens=tokens,
completion_tokens=100,
cache_read_tokens=0,
cache_write_tokens=0,
reasoning_tokens=0,
context_window=8000,
response_id="test",
)
stats.usage_to_metrics["test"] = metrics
visualizer = CLIVisualizer()
mock_state = MagicMock()
mock_state.stats = stats
visualizer.initialize(mock_state)
subtitle = visualizer._format_metrics_subtitle()
assert subtitle is not None, f"Failed for {tokens}"
assert expected in subtitle, (
f"Expected '{expected}' in subtitle for {tokens}, got: {subtitle}"
)
def test_event_base_fallback_visualize():
"""Test that Event provides fallback visualization."""
from openhands.sdk.event.base import Event
from openhands.sdk.event.types import SourceType
class UnknownEvent(Event):
source: SourceType = "agent"
event = UnknownEvent()
conv_viz = CLIVisualizer()
panel = conv_viz._create_event_panel(event)
assert "UNKNOWN Event" in str(panel.title)
def test_visualizer_does_not_render_system_prompt():
"""Test that Event provides fallback visualization."""
system_prompt_event = SystemPromptEvent(
source="agent",
system_prompt=TextContent(text="dummy"),
tools=[]
)
conv_viz = CLIVisualizer()
panel = conv_viz._create_event_panel(system_prompt_event)
assert panel is None