cai/tests/commands/test_command_history.py

518 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Test suite for the history command functionality.
Tests all handle methods and input possibilities for the history command.
"""
import os
import sys
import pytest
import json
from unittest.mock import patch, Mock, MagicMock
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__),
'..', '..', 'src'))
from cai.repl.commands.history import HistoryCommand
from cai.repl.commands.base import Command
class TestHistoryCommand:
"""Test cases for HistoryCommand."""
@pytest.fixture(autouse=True)
def setup_and_cleanup(self):
"""Setup and cleanup for each test."""
# Set up test environment
os.environ['CAI_TELEMETRY'] = 'false'
os.environ['CAI_TRACING'] = 'false'
yield
@pytest.fixture
def history_command(self):
"""Create a HistoryCommand instance for testing."""
return HistoryCommand()
@pytest.fixture
def sample_message_history(self):
"""Create sample message history for testing."""
return [
{
"role": "user",
"content": "Hello, can you help me with Python?"
},
{
"role": "assistant",
"content": "Of course! I'd be happy to help you with Python. What specific topic or problem would you like assistance with?"
},
{
"role": "user",
"content": "How do I create a list?"
},
{
"role": "assistant",
"content": "I'll help you create a tool to demonstrate list creation.",
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "create_example",
"arguments": '{"language": "python", "topic": "lists"}'
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_123",
"content": "# Creating lists in Python\nmy_list = [1, 2, 3]\nprint(my_list)"
},
{
"role": "assistant",
"content": "Here's how you create a list in Python: you use square brackets and separate items with commas."
}
]
@pytest.fixture
def complex_message_history(self):
"""Create complex message history with various message types."""
return [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Execute a command for me"
},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_cmd_1",
"type": "function",
"function": {
"name": "generic_linux_command",
"arguments": '{"command": "ls", "args": "-la"}'
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_cmd_1",
"content": "total 8\ndrwxr-xr-x 2 user user 4096 Jan 1 12:00 .\ndrwxr-xr-x 3 user user 4096 Jan 1 12:00 .."
},
{
"role": "assistant",
"content": "The directory listing shows two entries: the current directory (.) and parent directory (..)."
},
{
"role": "user",
"content": "Now run multiple commands"
},
{
"role": "assistant",
"content": "I'll run multiple commands for you.",
"tool_calls": [
{
"id": "call_cmd_2",
"type": "function",
"function": {
"name": "generic_linux_command",
"arguments": '{"command": "pwd"}'
}
},
{
"id": "call_cmd_3",
"type": "function",
"function": {
"name": "generic_linux_command",
"arguments": '{"command": "whoami"}'
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_cmd_2",
"content": "/home/user"
},
{
"role": "tool",
"tool_call_id": "call_cmd_3",
"content": "user"
}
]
def test_command_initialization(self, history_command):
"""Test that HistoryCommand initializes correctly."""
assert history_command.name == "/history"
assert history_command.description == "Display the conversation history"
assert history_command.aliases == ["/his"]
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_handle_no_args_with_history(self, mock_message_history,
history_command, sample_message_history):
"""Test handling with no arguments when history exists."""
mock_message_history.clear()
mock_message_history.extend(sample_message_history)
result = history_command.handle([])
assert result is True
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_handle_no_args_empty_history(self, mock_message_history, history_command):
"""Test handling with no arguments when history is empty."""
mock_message_history.clear()
result = history_command.handle([])
assert result is True
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_handle_with_args(self, mock_message_history,
history_command, sample_message_history):
"""Test handling when arguments are provided (should be ignored)."""
mock_message_history.clear()
mock_message_history.extend(sample_message_history)
result = history_command.handle(["some", "args"])
assert result is True
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_handle_complex_history(self, mock_message_history,
history_command, complex_message_history):
"""Test handling complex message history with various message types."""
mock_message_history.clear()
mock_message_history.extend(complex_message_history)
result = history_command.handle([])
assert result is True
def test_format_message_content_simple_text(self, history_command):
"""Test formatting simple text content."""
content = "This is a simple message"
tool_calls = None
result = history_command._format_message_content(content, tool_calls)
assert result == content
def test_format_message_content_long_text(self, history_command):
"""Test formatting long text content (should be truncated)."""
content = "This is a very long message that should be truncated because it exceeds the 100 character limit that is set in the formatting function for display purposes"
tool_calls = None
result = history_command._format_message_content(content, tool_calls)
assert len(result) <= 100
assert result.endswith("...")
def test_format_message_content_empty(self, history_command):
"""Test formatting empty content."""
content = ""
tool_calls = None
result = history_command._format_message_content(content, tool_calls)
assert "Empty message" in result
def test_format_message_content_none(self, history_command):
"""Test formatting None content."""
content = None
tool_calls = None
result = history_command._format_message_content(content, tool_calls)
assert "Empty message" in result
def test_format_message_content_with_tool_calls(self, history_command):
"""Test formatting content with tool calls."""
content = "I'll help you with that"
tool_calls = [
{
"id": "call_123",
"type": "function",
"function": {
"name": "test_function",
"arguments": '{"param1": "value1", "param2": "value2"}'
}
}
]
result = history_command._format_message_content(content, tool_calls)
assert "Function:" in result
assert "test_function" in result
assert "Args:" in result
def test_format_message_content_with_multiple_tool_calls(self, history_command):
"""Test formatting content with multiple tool calls."""
content = "I'll run multiple commands"
tool_calls = [
{
"id": "call_1",
"type": "function",
"function": {
"name": "function_one",
"arguments": '{"arg": "value1"}'
}
},
{
"id": "call_2",
"type": "function",
"function": {
"name": "function_two",
"arguments": '{"arg": "value2"}'
}
}
]
result = history_command._format_message_content(content, tool_calls)
assert "function_one" in result
assert "function_two" in result
assert result.count("Function:") == 2
def test_format_message_content_with_invalid_json_args(self, history_command):
"""Test formatting content with invalid JSON in tool call arguments."""
content = "Testing invalid JSON"
tool_calls = [
{
"id": "call_123",
"type": "function",
"function": {
"name": "test_function",
"arguments": "invalid json string"
}
}
]
result = history_command._format_message_content(content, tool_calls)
assert "Function:" in result
assert "test_function" in result
assert "invalid json string" in result
def test_format_message_content_with_long_tool_args(self, history_command):
"""Test formatting content with very long tool call arguments."""
content = "Testing long arguments"
long_args = json.dumps({
"very_long_parameter": "This is a very long parameter value that should be truncated when displayed in the history because it exceeds the character limit"
})
tool_calls = [
{
"id": "call_123",
"type": "function",
"function": {
"name": "test_function",
"arguments": long_args
}
}
]
result = history_command._format_message_content(content, tool_calls)
assert "Function:" in result
assert "test_function" in result
assert "..." in result # Should be truncated
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_handle_import_error(self, mock_message_history, history_command):
"""Test handling when import fails."""
# Create a new command instance and monkey patch its handle_no_args method
# to simulate an import error
test_command = HistoryCommand()
def mock_handle_no_args_with_import_error():
try:
# Simulate the import failing
raise ImportError("Mock import error")
except ImportError:
return False
# Replace the method to simulate import failure
test_command.handle_no_args = mock_handle_no_args_with_import_error
result = test_command.handle_no_args()
assert result is False
def test_command_base_functionality(self, history_command):
"""Test that the command inherits from base Command properly."""
assert isinstance(history_command, Command)
assert history_command.name == "/history"
assert "/his" in history_command.aliases
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_handle_with_corrupted_message(self, mock_message_history, history_command):
"""Test handling when message history contains corrupted data."""
# Create message history with missing or corrupted fields
corrupted_history = [
{
"role": "user",
"content": "Normal message"
},
{
# Missing role field
"content": "Message without role"
},
{
"role": "assistant",
# Missing content field
"tool_calls": [{"id": "call_1", "function": {"name": "test"}}]
},
{
"role": "tool",
"tool_call_id": "call_1",
"content": None # None content
}
]
mock_message_history.clear()
mock_message_history.extend(corrupted_history)
# Should handle gracefully without crashing
result = history_command.handle([])
assert result is True
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_handle_with_messages_parameter(self, mock_message_history,
history_command, sample_message_history):
"""Test handle method with explicit messages parameter."""
mock_message_history.clear()
mock_message_history.extend(sample_message_history)
# The handle method should work with messages parameter
result = history_command.handle([], messages=sample_message_history)
assert result is True
@pytest.mark.integration
class TestHistoryCommandIntegration:
"""Integration tests for history command functionality."""
@pytest.fixture(autouse=True)
def setup_integration(self):
"""Setup for integration tests."""
yield
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_full_conversation_history_workflow(self, mock_message_history):
"""Test a complete conversation workflow and history display."""
# Simulate a conversation building up over time
conversation_steps = [
# Step 1: Initial user message
[
{"role": "user", "content": "Hello"}
],
# Step 2: Assistant response
[
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi! How can I help you?"}
],
# Step 3: User asks for help
[
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi! How can I help you?"},
{"role": "user", "content": "Can you run a command?"}
],
# Step 4: Assistant with tool call
[
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi! How can I help you?"},
{"role": "user", "content": "Can you run a command?"},
{
"role": "assistant",
"content": "I'll run that command for you.",
"tool_calls": [
{
"id": "call_cmd",
"type": "function",
"function": {
"name": "generic_linux_command",
"arguments": '{"command": "ls"}'
}
}
]
}
],
# Step 5: Tool response
[
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi! How can I help you?"},
{"role": "user", "content": "Can you run a command?"},
{
"role": "assistant",
"content": "I'll run that command for you.",
"tool_calls": [
{
"id": "call_cmd",
"type": "function",
"function": {
"name": "generic_linux_command",
"arguments": '{"command": "ls"}'
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_cmd",
"content": "file1.txt\nfile2.txt\nfolder1/"
}
]
]
cmd = HistoryCommand()
# Test history at each step
for i, step_history in enumerate(conversation_steps):
mock_message_history.clear()
mock_message_history.extend(step_history)
result = cmd.handle([])
assert result is True, f"Failed at conversation step {i + 1}"
@patch('cai.sdk.agents.models.openai_chatcompletions.message_history')
def test_edge_case_message_combinations(self, mock_message_history):
"""Test various edge case message combinations."""
edge_cases = [
# Empty history
[],
# Only system message
[{"role": "system", "content": "You are a helpful assistant."}],
# Only user messages
[
{"role": "user", "content": "First message"},
{"role": "user", "content": "Second message"}
],
# Tool call without response
[
{"role": "user", "content": "Run command"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_incomplete",
"type": "function",
"function": {"name": "test_command", "arguments": "{}"}
}
]
}
],
# Tool response without call
[
{"role": "user", "content": "Test"},
{"role": "tool", "tool_call_id": "orphan_call", "content": "Result"}
]
]
cmd = HistoryCommand()
for i, case_history in enumerate(edge_cases):
mock_message_history.clear()
mock_message_history.extend(case_history)
result = cmd.handle([])
assert result is True, f"Failed at edge case {i + 1}"
if __name__ == '__main__':
pytest.main([__file__, "-v"])