mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-08 23:58:34 +00:00
Add thread-safe, async-compatible event bus with read–write locking and handler dependency ordering. Remove blinker dependency and implement direct dispatch. Improve type safety, error handling, and deterministic event synchronization. Refactor tests to auto-wait for async handlers, ensure clean teardown, and add comprehensive concurrency coverage. Replace thread-local state in AgentEvaluator with instance-based locking for correct cross-thread access. Enhance tracing reliability and event finalization.
287 lines
9.1 KiB
Python
287 lines
9.1 KiB
Python
"""Tests for FastAPI-style dependency injection in event handlers."""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from crewai.events import Depends, crewai_event_bus
|
|
from crewai.events.base_events import BaseEvent
|
|
|
|
|
|
class DependsTestEvent(BaseEvent):
|
|
"""Test event for dependency tests."""
|
|
|
|
value: int = 0
|
|
type: str = "test_event"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_basic_dependency():
|
|
"""Test that handler with dependency runs after its dependency."""
|
|
execution_order = []
|
|
|
|
with crewai_event_bus.scoped_handlers():
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
def setup(source, event: DependsTestEvent):
|
|
execution_order.append("setup")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent, Depends(setup))
|
|
def process(source, event: DependsTestEvent):
|
|
execution_order.append("process")
|
|
|
|
event = DependsTestEvent(value=1)
|
|
future = crewai_event_bus.emit("test_source", event)
|
|
|
|
if future:
|
|
await asyncio.wrap_future(future)
|
|
|
|
assert execution_order == ["setup", "process"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_dependencies():
|
|
"""Test handler with multiple dependencies."""
|
|
execution_order = []
|
|
|
|
with crewai_event_bus.scoped_handlers():
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
def setup_a(source, event: DependsTestEvent):
|
|
execution_order.append("setup_a")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
def setup_b(source, event: DependsTestEvent):
|
|
execution_order.append("setup_b")
|
|
|
|
@crewai_event_bus.on(
|
|
DependsTestEvent, depends_on=[Depends(setup_a), Depends(setup_b)]
|
|
)
|
|
def process(source, event: DependsTestEvent):
|
|
execution_order.append("process")
|
|
|
|
event = DependsTestEvent(value=1)
|
|
future = crewai_event_bus.emit("test_source", event)
|
|
|
|
if future:
|
|
await asyncio.wrap_future(future)
|
|
|
|
# setup_a and setup_b can run in any order (same level)
|
|
assert "process" in execution_order
|
|
assert execution_order.index("process") > execution_order.index("setup_a")
|
|
assert execution_order.index("process") > execution_order.index("setup_b")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chain_of_dependencies():
|
|
"""Test chain of dependencies (A -> B -> C)."""
|
|
execution_order = []
|
|
|
|
with crewai_event_bus.scoped_handlers():
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
def handler_a(source, event: DependsTestEvent):
|
|
execution_order.append("handler_a")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(handler_a))
|
|
def handler_b(source, event: DependsTestEvent):
|
|
execution_order.append("handler_b")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(handler_b))
|
|
def handler_c(source, event: DependsTestEvent):
|
|
execution_order.append("handler_c")
|
|
|
|
event = DependsTestEvent(value=1)
|
|
future = crewai_event_bus.emit("test_source", event)
|
|
|
|
if future:
|
|
await asyncio.wrap_future(future)
|
|
|
|
assert execution_order == ["handler_a", "handler_b", "handler_c"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_handler_with_dependency():
|
|
"""Test async handler with dependency on sync handler."""
|
|
execution_order = []
|
|
|
|
with crewai_event_bus.scoped_handlers():
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
def sync_setup(source, event: DependsTestEvent):
|
|
execution_order.append("sync_setup")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(sync_setup))
|
|
async def async_process(source, event: DependsTestEvent):
|
|
await asyncio.sleep(0.01)
|
|
execution_order.append("async_process")
|
|
|
|
event = DependsTestEvent(value=1)
|
|
future = crewai_event_bus.emit("test_source", event)
|
|
|
|
if future:
|
|
await asyncio.wrap_future(future)
|
|
|
|
assert execution_order == ["sync_setup", "async_process"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mixed_handlers_with_dependencies():
|
|
"""Test mix of sync and async handlers with dependencies."""
|
|
execution_order = []
|
|
|
|
with crewai_event_bus.scoped_handlers():
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
def setup(source, event: DependsTestEvent):
|
|
execution_order.append("setup")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(setup))
|
|
def sync_process(source, event: DependsTestEvent):
|
|
execution_order.append("sync_process")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent, depends_on=Depends(setup))
|
|
async def async_process(source, event: DependsTestEvent):
|
|
await asyncio.sleep(0.01)
|
|
execution_order.append("async_process")
|
|
|
|
@crewai_event_bus.on(
|
|
DependsTestEvent, depends_on=[Depends(sync_process), Depends(async_process)]
|
|
)
|
|
def finalize(source, event: DependsTestEvent):
|
|
execution_order.append("finalize")
|
|
|
|
event = DependsTestEvent(value=1)
|
|
future = crewai_event_bus.emit("test_source", event)
|
|
|
|
if future:
|
|
await asyncio.wrap_future(future)
|
|
|
|
# Verify execution order
|
|
assert execution_order[0] == "setup"
|
|
assert "finalize" in execution_order
|
|
assert execution_order.index("finalize") > execution_order.index("sync_process")
|
|
assert execution_order.index("finalize") > execution_order.index("async_process")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_independent_handlers_run_concurrently():
|
|
"""Test that handlers without dependencies can run concurrently."""
|
|
execution_order = []
|
|
|
|
with crewai_event_bus.scoped_handlers():
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
async def handler_a(source, event: DependsTestEvent):
|
|
await asyncio.sleep(0.01)
|
|
execution_order.append("handler_a")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
async def handler_b(source, event: DependsTestEvent):
|
|
await asyncio.sleep(0.01)
|
|
execution_order.append("handler_b")
|
|
|
|
event = DependsTestEvent(value=1)
|
|
future = crewai_event_bus.emit("test_source", event)
|
|
|
|
if future:
|
|
await asyncio.wrap_future(future)
|
|
|
|
# Both handlers should have executed
|
|
assert len(execution_order) == 2
|
|
assert "handler_a" in execution_order
|
|
assert "handler_b" in execution_order
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_circular_dependency_detection():
|
|
"""Test that circular dependencies are detected and raise an error."""
|
|
from crewai.events.handler_graph import CircularDependencyError, build_execution_plan
|
|
|
|
# Create circular dependency: handler_a -> handler_b -> handler_c -> handler_a
|
|
def handler_a(source, event: DependsTestEvent):
|
|
pass
|
|
|
|
def handler_b(source, event: DependsTestEvent):
|
|
pass
|
|
|
|
def handler_c(source, event: DependsTestEvent):
|
|
pass
|
|
|
|
# Build a dependency graph with a cycle
|
|
handlers = [handler_a, handler_b, handler_c]
|
|
dependencies = {
|
|
handler_a: [Depends(handler_b)],
|
|
handler_b: [Depends(handler_c)],
|
|
handler_c: [Depends(handler_a)], # Creates the cycle
|
|
}
|
|
|
|
# Should raise CircularDependencyError about circular dependency
|
|
with pytest.raises(CircularDependencyError, match="Circular dependency"):
|
|
build_execution_plan(handlers, dependencies)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handler_without_dependency_runs_normally():
|
|
"""Test that handlers without dependencies still work as before."""
|
|
execution_order = []
|
|
|
|
with crewai_event_bus.scoped_handlers():
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
def simple_handler(source, event: DependsTestEvent):
|
|
execution_order.append("simple_handler")
|
|
|
|
event = DependsTestEvent(value=1)
|
|
future = crewai_event_bus.emit("test_source", event)
|
|
|
|
if future:
|
|
await asyncio.wrap_future(future)
|
|
|
|
assert execution_order == ["simple_handler"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_depends_equality():
|
|
"""Test Depends equality and hashing."""
|
|
|
|
def handler_a(source, event):
|
|
pass
|
|
|
|
def handler_b(source, event):
|
|
pass
|
|
|
|
dep_a1 = Depends(handler_a)
|
|
dep_a2 = Depends(handler_a)
|
|
dep_b = Depends(handler_b)
|
|
|
|
# Same handler should be equal
|
|
assert dep_a1 == dep_a2
|
|
assert hash(dep_a1) == hash(dep_a2)
|
|
|
|
# Different handlers should not be equal
|
|
assert dep_a1 != dep_b
|
|
assert hash(dep_a1) != hash(dep_b)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aemit_ignores_dependencies():
|
|
"""Test that aemit only processes async handlers (no dependency support yet)."""
|
|
execution_order = []
|
|
|
|
with crewai_event_bus.scoped_handlers():
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
def sync_handler(source, event: DependsTestEvent):
|
|
execution_order.append("sync_handler")
|
|
|
|
@crewai_event_bus.on(DependsTestEvent)
|
|
async def async_handler(source, event: DependsTestEvent):
|
|
execution_order.append("async_handler")
|
|
|
|
event = DependsTestEvent(value=1)
|
|
await crewai_event_bus.aemit("test_source", event)
|
|
|
|
# Only async handler should execute
|
|
assert execution_order == ["async_handler"]
|