Files
crewAI/tests/tracing/test_tracing.py
Greyson LaLonde 878c1a649a refactor: Move events module to crewai.events (#3425)
refactor(events): relocate events module & update imports

- Move events from utilities/ to top-level events/ with types/, listeners/, utils/ structure
- Update all source/tests/docs to new import paths
- Add backwards compatibility stubs in crewai.utilities.events with deprecation warnings
- Restore test mocks and fix related test imports
2025-09-02 10:06:42 -04:00

406 lines
15 KiB
Python

import os
import pytest
from unittest.mock import patch, MagicMock
from crewai import Agent, Task, Crew
from crewai.flow.flow import Flow, start
from crewai.events.listeners.tracing.trace_listener import (
TraceCollectionListener,
)
from crewai.events.listeners.tracing.trace_batch_manager import (
TraceBatchManager,
)
from crewai.events.listeners.tracing.types import TraceEvent
class TestTraceListenerSetup:
"""Test TraceListener is properly setup and collecting events"""
@pytest.fixture(autouse=True)
def mock_auth_token(self):
"""Mock authentication token for all tests in this class"""
# Need to patch all the places where get_auth_token is imported/used
with (
patch(
"crewai.cli.authentication.token.get_auth_token",
return_value="mock_token_12345",
),
patch(
"crewai.events.listeners.tracing.trace_listener.get_auth_token",
return_value="mock_token_12345",
),
patch(
"crewai.events.listeners.tracing.trace_batch_manager.get_auth_token",
return_value="mock_token_12345",
),
):
yield
@pytest.fixture(autouse=True)
def clear_event_bus(self):
"""Clear event bus listeners before and after each test"""
from crewai.events.event_bus import crewai_event_bus
# Store original handlers
original_handlers = crewai_event_bus._handlers.copy()
# Clear for test
crewai_event_bus._handlers.clear()
yield
# Restore original state
crewai_event_bus._handlers.clear()
crewai_event_bus._handlers.update(original_handlers)
@pytest.fixture(autouse=True)
def reset_tracing_singletons(self):
"""Reset tracing singleton instances between tests"""
# Reset TraceCollectionListener singleton
if hasattr(TraceCollectionListener, "_instance"):
TraceCollectionListener._instance = None
TraceCollectionListener._initialized = False
yield
# Clean up after test
if hasattr(TraceCollectionListener, "_instance"):
TraceCollectionListener._instance = None
TraceCollectionListener._initialized = False
@pytest.fixture(autouse=True)
def mock_plus_api_calls(self):
"""Mock all PlusAPI HTTP calls to avoid network requests"""
with (
patch("requests.post") as mock_post,
patch("requests.get") as mock_get,
patch("requests.put") as mock_put,
patch("requests.delete") as mock_delete,
patch.object(TraceBatchManager, "initialize_batch", return_value=None),
patch.object(
TraceBatchManager, "_finalize_backend_batch", return_value=True
),
patch.object(TraceBatchManager, "_cleanup_batch_data", return_value=True),
):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "mock_trace_batch_id",
"status": "success",
"message": "Batch created successfully",
}
mock_response.raise_for_status.return_value = None
mock_post.return_value = mock_response
mock_get.return_value = mock_response
mock_put.return_value = mock_response
mock_delete.return_value = mock_response
yield {
"post": mock_post,
"get": mock_get,
"put": mock_put,
"delete": mock_delete,
}
@pytest.mark.vcr(filter_headers=["authorization"])
def test_trace_listener_collects_crew_events(self):
"""Test that trace listener properly collects events from crew execution"""
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
llm="gpt-4o-mini",
)
task = Task(
description="Say hello to the world",
expected_output="hello world",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task], verbose=True)
trace_listener = TraceCollectionListener()
from crewai.events.event_bus import crewai_event_bus
trace_listener.setup_listeners(crewai_event_bus)
with patch.object(
trace_listener.batch_manager,
"initialize_batch",
return_value=None,
) as initialize_mock:
crew.kickoff()
assert initialize_mock.call_count >= 1
call_args = initialize_mock.call_args_list[0]
assert len(call_args[0]) == 2 # user_context, execution_metadata
_, execution_metadata = call_args[0]
assert isinstance(execution_metadata, dict)
assert "crew_name" in execution_metadata
@pytest.mark.vcr(filter_headers=["authorization"])
def test_batch_manager_finalizes_batch_clears_buffer(self):
"""Test that batch manager properly finalizes batch and clears buffer"""
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
llm="gpt-4o-mini",
)
task = Task(
description="Say hello to the world",
expected_output="hello world",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task], verbose=True)
from crewai.events.event_bus import crewai_event_bus
trace_listener = None
for handler_list in crewai_event_bus._handlers.values():
for handler in handler_list:
if hasattr(handler, "__self__") and isinstance(
handler.__self__, TraceCollectionListener
):
trace_listener = handler.__self__
break
if trace_listener:
break
if not trace_listener:
pytest.skip(
"No trace listener found - tracing may not be properly enabled"
)
with patch.object(
trace_listener.batch_manager,
"finalize_batch",
wraps=trace_listener.batch_manager.finalize_batch,
) as finalize_mock:
crew.kickoff()
assert finalize_mock.call_count >= 1
@pytest.mark.vcr(filter_headers=["authorization"])
def test_events_collection_batch_manager(self, mock_plus_api_calls):
"""Test that trace listener properly collects events from crew execution"""
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
llm="gpt-4o-mini",
)
task = Task(
description="Say hello to the world",
expected_output="hello world",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task], verbose=True)
from crewai.events.event_bus import crewai_event_bus
# Create and setup trace listener explicitly
trace_listener = TraceCollectionListener()
trace_listener.setup_listeners(crewai_event_bus)
with patch.object(
trace_listener.batch_manager,
"add_event",
wraps=trace_listener.batch_manager.add_event,
) as add_event_mock:
crew.kickoff()
assert add_event_mock.call_count >= 2
completion_events = [
call.args[0]
for call in add_event_mock.call_args_list
if call.args[0].type == "crew_kickoff_completed"
]
assert len(completion_events) >= 1
# Verify the first completion event has proper structure
completion_event = completion_events[0]
assert "crew_name" in completion_event.event_data
assert completion_event.event_data["crew_name"] == "crew"
# Verify all events have proper structure
for call in add_event_mock.call_args_list:
event = call.args[0]
assert isinstance(event, TraceEvent)
assert hasattr(event, "event_data")
assert hasattr(event, "type")
@pytest.mark.vcr(filter_headers=["authorization"])
def test_trace_listener_disabled_when_env_false(self):
"""Test that trace listener doesn't make HTTP calls when tracing is disabled"""
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "false"}):
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
llm="gpt-4o-mini",
)
task = Task(
description="Say hello to the world",
expected_output="hello world",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task], verbose=True)
result = crew.kickoff()
assert result is not None
from crewai.events.event_bus import crewai_event_bus
trace_handlers = []
for handlers in crewai_event_bus._handlers.values():
for handler in handlers:
if hasattr(handler, "__self__") and isinstance(
handler.__self__, TraceCollectionListener
):
trace_handlers.append(handler)
elif hasattr(handler, "__name__") and any(
trace_name in handler.__name__
for trace_name in [
"on_crew_started",
"on_crew_completed",
"on_flow_started",
]
):
trace_handlers.append(handler)
assert (
len(trace_handlers) == 0
), f"Found {len(trace_handlers)} trace handlers when tracing should be disabled"
def test_trace_listener_setup_correctly_for_crew(self):
"""Test that trace listener is set up correctly when enabled"""
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
llm="gpt-4o-mini",
)
task = Task(
description="Say hello to the world",
expected_output="hello world",
agent=agent,
)
with patch.object(
TraceCollectionListener, "setup_listeners"
) as mock_listener_setup:
Crew(agents=[agent], tasks=[task], verbose=True)
assert mock_listener_setup.call_count >= 1
def test_trace_listener_setup_correctly_for_flow(self):
"""Test that trace listener is set up correctly when enabled"""
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
class FlowExample(Flow):
@start()
def start(self):
pass
with patch.object(
TraceCollectionListener, "setup_listeners"
) as mock_listener_setup:
FlowExample()
assert mock_listener_setup.call_count >= 1
@pytest.mark.vcr(filter_headers=["authorization"])
def test_trace_listener_ephemeral_batch(self):
"""Test that trace listener properly handles ephemeral batches"""
with (
patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}),
patch(
"crewai.events.listeners.tracing.trace_listener.TraceCollectionListener._check_authenticated",
return_value=False,
),
):
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
llm="gpt-4o-mini",
)
task = Task(
description="Say hello to the world",
expected_output="hello world",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task], tracing=True)
with patch.object(TraceBatchManager, "initialize_batch") as mock_initialize:
crew.kickoff()
assert mock_initialize.call_count >= 1
assert mock_initialize.call_args_list[0][1]["use_ephemeral"] is True
@pytest.mark.vcr(filter_headers=["authorization"])
def test_trace_listener_with_authenticated_user(self):
"""Test that trace listener properly handles authenticated batches"""
with (
patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}),
patch(
"crewai.events.listeners.tracing.trace_batch_manager.PlusAPI"
) as mock_plus_api_class,
):
mock_plus_api_instance = MagicMock()
mock_plus_api_class.return_value = mock_plus_api_instance
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
llm="gpt-4o-mini",
)
task = Task(
description="Say hello to the world",
expected_output="hello world",
agent=agent,
)
with (
patch.object(TraceBatchManager, "initialize_batch") as mock_initialize,
patch.object(
TraceBatchManager, "finalize_batch"
) as mock_finalize_backend_batch,
):
crew = Crew(agents=[agent], tasks=[task], tracing=True)
crew.kickoff()
mock_plus_api_class.assert_called_with(api_key="mock_token_12345")
assert mock_initialize.call_count >= 1
mock_finalize_backend_batch.assert_called_with()
assert mock_finalize_backend_batch.call_count >= 1
# Helper method to ensure cleanup
def teardown_method(self):
"""Cleanup after each test method"""
from crewai.events.event_bus import crewai_event_bus
crewai_event_bus._handlers.clear()
@classmethod
def teardown_class(cls):
"""Final cleanup after all tests in this class"""
from crewai.events.event_bus import crewai_event_bus
crewai_event_bus._handlers.clear()