mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-10 00:28:31 +00:00
- Remove unused imports (uuid, List, Part, TextPart) - Fix type-checking errors for task_id and context_id validation - Remove invalid AgentCard parameter (supported_content_types) - Update test expectations for JSON output conversion - Fix TaskInfo structure usage in cancel test - Update server function call signatures in tests All A2A tests now pass (34 passed, 2 skipped) Co-Authored-By: João <joao@crewai.com>
199 lines
7.7 KiB
Python
199 lines
7.7 KiB
Python
"""Tests for CrewAgentExecutor class."""
|
|
|
|
import asyncio
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
|
|
from crewai.crews.crew_output import CrewOutput
|
|
|
|
try:
|
|
from crewai.a2a import CrewAgentExecutor
|
|
from a2a.server.agent_execution import RequestContext
|
|
from a2a.server.events import EventQueue
|
|
pass # Imports handled in test methods as needed
|
|
from a2a.utils.errors import ServerError
|
|
A2A_AVAILABLE = True
|
|
except ImportError:
|
|
A2A_AVAILABLE = False
|
|
|
|
|
|
@pytest.mark.skipif(not A2A_AVAILABLE, reason="A2A integration not available")
|
|
class TestCrewAgentExecutor:
|
|
"""Test cases for CrewAgentExecutor."""
|
|
|
|
@pytest.fixture
|
|
def sample_crew(self):
|
|
"""Create a sample crew for testing."""
|
|
from unittest.mock import Mock
|
|
mock_crew = Mock()
|
|
mock_crew.agents = []
|
|
mock_crew.tasks = []
|
|
return mock_crew
|
|
|
|
@pytest.fixture
|
|
def crew_executor(self, sample_crew):
|
|
"""Create a CrewAgentExecutor for testing."""
|
|
return CrewAgentExecutor(sample_crew)
|
|
|
|
@pytest.fixture
|
|
def mock_context(self):
|
|
"""Create a mock RequestContext."""
|
|
from a2a.types import Message, Part, TextPart
|
|
context = Mock(spec=RequestContext)
|
|
context.task_id = "test-task-123"
|
|
context.context_id = "test-context-456"
|
|
context.message = Message(
|
|
messageId="msg-123",
|
|
taskId="test-task-123",
|
|
contextId="test-context-456",
|
|
role="user",
|
|
parts=[Part(root=TextPart(text="Test message"))]
|
|
)
|
|
context.get_user_input.return_value = "Test query"
|
|
return context
|
|
|
|
@pytest.fixture
|
|
def mock_event_queue(self):
|
|
"""Create a mock EventQueue."""
|
|
return Mock(spec=EventQueue)
|
|
|
|
def test_init(self, sample_crew):
|
|
"""Test CrewAgentExecutor initialization."""
|
|
executor = CrewAgentExecutor(sample_crew)
|
|
|
|
assert executor.crew == sample_crew
|
|
assert executor.supported_content_types == ['text', 'text/plain']
|
|
assert executor._running_tasks == {}
|
|
|
|
def test_init_with_custom_content_types(self, sample_crew):
|
|
"""Test CrewAgentExecutor initialization with custom content types."""
|
|
custom_types = ['text', 'application/json']
|
|
executor = CrewAgentExecutor(sample_crew, supported_content_types=custom_types)
|
|
|
|
assert executor.supported_content_types == custom_types
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_success(self, crew_executor, mock_context, mock_event_queue):
|
|
"""Test successful crew execution."""
|
|
mock_output = CrewOutput(raw="Test response", json_dict=None)
|
|
|
|
with patch.object(crew_executor, '_execute_crew_async', return_value=mock_output):
|
|
await crew_executor.execute(mock_context, mock_event_queue)
|
|
|
|
mock_event_queue.enqueue_event.assert_called_once()
|
|
|
|
assert len(crew_executor._running_tasks) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_with_validation_error(self, crew_executor, mock_event_queue):
|
|
"""Test execution with validation error."""
|
|
bad_context = Mock(spec=RequestContext)
|
|
bad_context.get_user_input.return_value = ""
|
|
|
|
with pytest.raises(ServerError):
|
|
await crew_executor.execute(bad_context, mock_event_queue)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_with_crew_error(self, crew_executor, mock_context, mock_event_queue):
|
|
"""Test execution when crew raises an error."""
|
|
with patch.object(crew_executor, '_execute_crew_async', side_effect=Exception("Crew error")):
|
|
with pytest.raises(ServerError):
|
|
await crew_executor.execute(mock_context, mock_event_queue)
|
|
|
|
mock_event_queue.enqueue_event.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_existing_task(self, crew_executor, mock_event_queue):
|
|
"""Test cancelling an existing task."""
|
|
cancel_context = Mock(spec=RequestContext)
|
|
cancel_context.task_id = "test-task-123"
|
|
|
|
async def dummy_task():
|
|
await asyncio.sleep(10)
|
|
|
|
mock_task = asyncio.create_task(dummy_task())
|
|
from crewai.a2a.crew_agent_executor import TaskInfo
|
|
from datetime import datetime
|
|
task_info = TaskInfo(task=mock_task, started_at=datetime.now())
|
|
crew_executor._running_tasks["test-task-123"] = task_info
|
|
|
|
result = await crew_executor.cancel(cancel_context, mock_event_queue)
|
|
|
|
assert result is None
|
|
assert "test-task-123" not in crew_executor._running_tasks
|
|
assert mock_task.cancelled()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_nonexistent_task(self, crew_executor, mock_event_queue):
|
|
"""Test cancelling a task that doesn't exist."""
|
|
cancel_context = Mock(spec=RequestContext)
|
|
cancel_context.task_id = "nonexistent-task"
|
|
|
|
with pytest.raises(ServerError):
|
|
await crew_executor.cancel(cancel_context, mock_event_queue)
|
|
|
|
def test_convert_output_to_parts_with_raw(self, crew_executor):
|
|
"""Test converting crew output with raw content to A2A parts."""
|
|
output = Mock()
|
|
output.raw = "Test response"
|
|
output.json_dict = None
|
|
parts = crew_executor._convert_output_to_parts(output)
|
|
|
|
assert len(parts) == 1
|
|
assert parts[0].root.text == "Test response"
|
|
|
|
def test_convert_output_to_parts_with_json(self, crew_executor):
|
|
"""Test converting crew output with JSON data to A2A parts."""
|
|
output = Mock()
|
|
output.raw = "Test response"
|
|
output.json_dict = {"key": "value"}
|
|
parts = crew_executor._convert_output_to_parts(output)
|
|
|
|
assert len(parts) == 2
|
|
assert parts[0].root.text == "Test response"
|
|
assert '"key": "value"' in parts[1].root.text
|
|
|
|
def test_convert_output_to_parts_empty(self, crew_executor):
|
|
"""Test converting empty crew output to A2A parts."""
|
|
output = ""
|
|
parts = crew_executor._convert_output_to_parts(output)
|
|
|
|
assert len(parts) == 1
|
|
assert parts[0].root.text == "Crew execution completed successfully"
|
|
|
|
def test_validate_request_valid(self, crew_executor, mock_context):
|
|
"""Test request validation with valid input."""
|
|
error = crew_executor._validate_request(mock_context)
|
|
assert error is None
|
|
|
|
def test_validate_request_empty_input(self, crew_executor):
|
|
"""Test request validation with empty input."""
|
|
context = Mock(spec=RequestContext)
|
|
context.get_user_input.return_value = ""
|
|
|
|
error = crew_executor._validate_request(context)
|
|
assert error == "Empty or missing user input"
|
|
|
|
def test_validate_request_whitespace_input(self, crew_executor):
|
|
"""Test request validation with whitespace-only input."""
|
|
context = Mock(spec=RequestContext)
|
|
context.get_user_input.return_value = " \n\t "
|
|
|
|
error = crew_executor._validate_request(context)
|
|
assert error == "Empty or missing user input"
|
|
|
|
def test_validate_request_exception(self, crew_executor):
|
|
"""Test request validation when get_user_input raises exception."""
|
|
context = Mock(spec=RequestContext)
|
|
context.get_user_input.side_effect = Exception("Input error")
|
|
|
|
error = crew_executor._validate_request(context)
|
|
assert "Failed to extract user input" in error
|
|
|
|
|
|
@pytest.mark.skipif(A2A_AVAILABLE, reason="Testing import error handling")
|
|
def test_import_error_handling():
|
|
"""Test that import errors are handled gracefully when A2A is not available."""
|
|
with pytest.raises(ImportError, match="A2A integration requires"):
|
|
pass
|