cai/tests/commands/test_command_parallel.py

344 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Test suite for the parallel command functionality.
Tests all handle methods and input possibilities for the parallel command.
"""
import os
import sys
import pytest
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.parallel import ParallelCommand, ParallelConfig, PARALLEL_CONFIGS
from cai.repl.commands.base import Command
class TestParallelCommand:
"""Test cases for ParallelCommand."""
@pytest.fixture(autouse=True)
def setup_and_cleanup(self):
"""Setup and cleanup for each test."""
# Clear parallel configs before each test
PARALLEL_CONFIGS.clear()
# Set up test environment
os.environ['CAI_TELEMETRY'] = 'false'
os.environ['CAI_TRACING'] = 'false'
yield
# Cleanup after each test
PARALLEL_CONFIGS.clear()
@pytest.fixture
def parallel_command(self):
"""Create a ParallelCommand instance for testing."""
return ParallelCommand()
def test_command_initialization(self, parallel_command):
"""Test that ParallelCommand initializes correctly."""
assert parallel_command.name == "/parallel"
assert parallel_command.description == "Configure multiple agents to run in parallel with different settings"
assert parallel_command.aliases == ["/par", "/p"]
# Check subcommands are registered
expected_subcommands = ["add", "list", "clear", "remove"]
assert set(parallel_command.get_subcommands()) == set(expected_subcommands)
def test_parallel_config_initialization(self):
"""Test ParallelConfig initialization."""
config = ParallelConfig("test_agent", "gpt-4", "Test prompt")
assert config.agent_name == "test_agent"
assert config.model == "gpt-4"
assert config.prompt == "Test prompt"
# Test default values
config_default = ParallelConfig("test_agent")
assert config_default.agent_name == "test_agent"
assert config_default.model is None
assert config_default.prompt is None
def test_parallel_config_str_representation(self):
"""Test ParallelConfig string representation."""
# Test with all parameters
config = ParallelConfig("test_agent", "gpt-4", "Test prompt")
str_repr = str(config)
assert "Agent: test_agent" in str_repr
assert "model: gpt-4" in str_repr
assert "prompt: 'Test prompt'" in str_repr
# Test with long prompt (should be truncated)
long_prompt = "This is a very long prompt that should be truncated when displayed"
config_long = ParallelConfig("test_agent", "gpt-4", long_prompt)
str_repr_long = str(config_long)
assert "..." in str_repr_long
# Test with minimal parameters
config_minimal = ParallelConfig("test_agent")
str_repr_minimal = str(config_minimal)
assert "Agent: test_agent" in str_repr_minimal
assert "model:" not in str_repr_minimal
assert "prompt:" not in str_repr_minimal
@patch('cai.repl.commands.parallel.get_available_agents')
def test_handle_add_valid_agent(self, mock_get_agents, parallel_command):
"""Test adding a valid agent to parallel config."""
# Mock available agents
mock_get_agents.return_value = {
"test_agent": Mock(),
"another_agent": Mock()
}
# Test basic add
result = parallel_command.handle_add(["test_agent"])
assert result is True
assert len(PARALLEL_CONFIGS) == 1
assert PARALLEL_CONFIGS[0].agent_name == "test_agent"
assert PARALLEL_CONFIGS[0].model is None
assert PARALLEL_CONFIGS[0].prompt is None
@patch('cai.repl.commands.parallel.get_available_agents')
def test_handle_add_with_model_and_prompt(self, mock_get_agents, parallel_command):
"""Test adding agent with model and prompt parameters."""
mock_get_agents.return_value = {"test_agent": Mock()}
args = ["test_agent", "--model", "gpt-4", "--prompt", "Custom prompt"]
result = parallel_command.handle_add(args)
assert result is True
assert len(PARALLEL_CONFIGS) == 1
config = PARALLEL_CONFIGS[0]
assert config.agent_name == "test_agent"
assert config.model == "gpt-4"
assert config.prompt == "Custom prompt"
@patch('cai.repl.commands.parallel.get_available_agents')
def test_handle_add_invalid_agent(self, mock_get_agents, parallel_command):
"""Test adding an invalid agent name."""
mock_get_agents.return_value = {"valid_agent": Mock()}
result = parallel_command.handle_add(["invalid_agent"])
assert result is False
assert len(PARALLEL_CONFIGS) == 0
def test_handle_add_no_args(self, parallel_command):
"""Test add command with no arguments."""
result = parallel_command.handle_add([])
assert result is False
assert len(PARALLEL_CONFIGS) == 0
@patch('cai.repl.commands.parallel.get_available_agents')
def test_handle_add_multiple_agents(self, mock_get_agents, parallel_command):
"""Test adding multiple agents."""
mock_get_agents.return_value = {
"agent1": Mock(),
"agent2": Mock(),
"agent3": Mock()
}
# Add first agent
result1 = parallel_command.handle_add(["agent1", "--model", "gpt-4"])
assert result1 is True
# Add second agent
result2 = parallel_command.handle_add(["agent2", "--prompt", "Second prompt"])
assert result2 is True
# Add third agent
result3 = parallel_command.handle_add(["agent3"])
assert result3 is True
assert len(PARALLEL_CONFIGS) == 3
assert PARALLEL_CONFIGS[0].agent_name == "agent1"
assert PARALLEL_CONFIGS[1].agent_name == "agent2"
assert PARALLEL_CONFIGS[2].agent_name == "agent3"
def test_handle_list_empty(self, parallel_command):
"""Test listing when no parallel configs exist."""
result = parallel_command.handle_list([])
assert result is True
assert len(PARALLEL_CONFIGS) == 0
def test_handle_list_with_configs(self, parallel_command):
"""Test listing existing parallel configs."""
# Add some configs
PARALLEL_CONFIGS.append(ParallelConfig("agent1", "gpt-4", "Prompt 1"))
PARALLEL_CONFIGS.append(ParallelConfig("agent2", None, None))
PARALLEL_CONFIGS.append(ParallelConfig("agent3", "claude", "Long prompt"))
result = parallel_command.handle_list([])
assert result is True
def test_handle_clear_empty(self, parallel_command):
"""Test clearing empty parallel configs."""
result = parallel_command.handle_clear([])
assert result is True
assert len(PARALLEL_CONFIGS) == 0
def test_handle_clear_with_configs(self, parallel_command):
"""Test clearing existing parallel configs."""
# Add some configs
PARALLEL_CONFIGS.append(ParallelConfig("agent1"))
PARALLEL_CONFIGS.append(ParallelConfig("agent2"))
PARALLEL_CONFIGS.append(ParallelConfig("agent3"))
assert len(PARALLEL_CONFIGS) == 3
result = parallel_command.handle_clear([])
assert result is True
assert len(PARALLEL_CONFIGS) == 0
def test_handle_remove_valid_index(self, parallel_command):
"""Test removing a config by valid index."""
# Add some configs
PARALLEL_CONFIGS.append(ParallelConfig("agent1"))
PARALLEL_CONFIGS.append(ParallelConfig("agent2"))
PARALLEL_CONFIGS.append(ParallelConfig("agent3"))
# Remove the second config (index 2)
result = parallel_command.handle_remove(["2"])
assert result is True
assert len(PARALLEL_CONFIGS) == 2
assert PARALLEL_CONFIGS[0].agent_name == "agent1"
assert PARALLEL_CONFIGS[1].agent_name == "agent3"
def test_handle_remove_invalid_index(self, parallel_command):
"""Test removing with invalid index."""
PARALLEL_CONFIGS.append(ParallelConfig("agent1"))
# Test invalid numeric index
result1 = parallel_command.handle_remove(["5"])
assert result1 is False
assert len(PARALLEL_CONFIGS) == 1
# Test negative index
result2 = parallel_command.handle_remove(["-1"])
assert result2 is False
assert len(PARALLEL_CONFIGS) == 1
# Test non-numeric index
result3 = parallel_command.handle_remove(["invalid"])
assert result3 is False
assert len(PARALLEL_CONFIGS) == 1
def test_handle_remove_no_args(self, parallel_command):
"""Test remove command with no arguments."""
PARALLEL_CONFIGS.append(ParallelConfig("agent1"))
result = parallel_command.handle_remove([])
assert result is False
assert len(PARALLEL_CONFIGS) == 1
def test_command_base_functionality(self, parallel_command):
"""Test that the command inherits from base Command properly."""
assert isinstance(parallel_command, Command)
assert parallel_command.name == "/parallel"
assert "/par" in parallel_command.aliases
assert "/p" in parallel_command.aliases
@patch('cai.repl.commands.parallel.get_available_agents')
def test_handle_main_command_routing(self, mock_get_agents, parallel_command):
"""Test that main handle method routes to correct subcommands."""
mock_get_agents.return_value = {"test_agent": Mock()}
# Test routing to add
result1 = parallel_command.handle(["add", "test_agent"])
assert result1 is True
assert len(PARALLEL_CONFIGS) == 1
# Test routing to list
result2 = parallel_command.handle(["list"])
assert result2 is True
# Test routing to clear
result3 = parallel_command.handle(["clear"])
assert result3 is True
assert len(PARALLEL_CONFIGS) == 0
def test_handle_unknown_subcommand(self, parallel_command):
"""Test handling of unknown subcommands."""
# This will use the default handle method from base class
# which should route to handle_unknown_subcommand
result = parallel_command.handle(["unknown_subcommand"])
assert result is False
def test_handle_no_args(self, parallel_command):
"""Test handling when no arguments provided."""
# The base handle method should route to handle_no_args
result = parallel_command.handle([])
assert result is False
@pytest.mark.integration
class TestParallelCommandIntegration:
"""Integration tests for parallel command functionality."""
@pytest.fixture(autouse=True)
def setup_integration(self):
"""Setup for integration tests."""
PARALLEL_CONFIGS.clear()
yield
PARALLEL_CONFIGS.clear()
@patch('cai.repl.commands.parallel.get_available_agents')
def test_full_workflow(self, mock_get_agents):
"""Test a complete workflow of adding, listing, and removing configs."""
mock_get_agents.return_value = {
"agent1": Mock(),
"agent2": Mock(),
"agent3": Mock()
}
cmd = ParallelCommand()
# Start with empty configs
assert len(PARALLEL_CONFIGS) == 0
# Add multiple configs
cmd.handle(["add", "agent1", "--model", "gpt-4"])
cmd.handle(["add", "agent2", "--prompt", "Test prompt"])
cmd.handle(["add", "agent3", "--model", "claude", "--prompt", "Another prompt"])
assert len(PARALLEL_CONFIGS) == 3
# List configs (should not change count)
cmd.handle(["list"])
assert len(PARALLEL_CONFIGS) == 3
# Remove one config
cmd.handle(["remove", "2"])
assert len(PARALLEL_CONFIGS) == 2
# Clear all configs
cmd.handle(["clear"])
assert len(PARALLEL_CONFIGS) == 0
@patch('cai.repl.commands.parallel.get_available_agents')
def test_edge_case_combinations(self, mock_get_agents):
"""Test edge cases and unusual parameter combinations."""
mock_get_agents.return_value = {"test_agent": Mock()}
cmd = ParallelCommand()
# Test partial parameters
result1 = cmd.handle(["add", "test_agent", "--model"])
assert result1 is True # Should still work with incomplete args
# Test empty string parameters
result2 = cmd.handle(["add", "test_agent", "--prompt", ""])
assert result2 is True
# Test parameters in different order
result3 = cmd.handle(["add", "test_agent", "--prompt", "Test", "--model", "gpt-4"])
assert result3 is True
assert len(PARALLEL_CONFIGS) == 3
if __name__ == '__main__':
pytest.main([__file__, "-v"])