mirror of https://github.com/aliasrobotics/cai.git
518 lines
19 KiB
Python
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"]) |