import asyncio from typing import Any from unittest.mock import MagicMock, patch from behave import given, then, when from cleveragents.agents.base import Agent, AgentWithMemory from cleveragents.core.exceptions import ExecutionError class DefaultAgent(Agent): def __init__( self, name: str, config: dict[str, Any] | None = None, raise_error: bool = False ): self.raise_error = raise_error super().__init__(name, config) self.last_message = None self.last_context = None async def process_message( self, message: Any, context: dict[str, Any] | None = None ) -> str: self.last_message = message self.last_context = context if self.raise_error: raise RuntimeError("boom") return f"processed:{message}" def get_capabilities(self) -> list[str]: return ["default"] class MemoryAgent(AgentWithMemory): async def process_message( self, message: Any, context: dict[str, Any] | None = None ) -> Any: await self.remember("last", message) return f"mem:{message}" def get_capabilities(self) -> list[str]: return ["memory"] @given("a basic test agent setup") def step_basic_agent(context): context.loop = asyncio.new_event_loop() asyncio.set_event_loop(context.loop) context.agent = DefaultAgent("base-test") context.captured_output = [] @when("I patch asyncio create_task and send a message") def step_patch_create_task(context): with patch("asyncio.create_task") as mock_task: context.agent.send_message("hello", {"foo": "bar"}) context.create_task_called_with = None if mock_task.call_args: context.create_task_called_with = mock_task.call_args[0][0] @then("the processing pipeline should schedule the process wrapper task") def step_verify_create_task_called(context): assert context.create_task_called_with is not None assert context.create_task_called_with.__qualname__.endswith("_process_wrapper") @when("I process a tuple message through the wrapper") def step_process_tuple(context): context.captured_output = [] context.agent.output_stream.subscribe( lambda value: context.captured_output.append(value) ) asyncio.run(context.agent._process_wrapper(("tuple-msg", {"ctx": 1}))) @then("the tuple message and context should be passed and emitted") def step_verify_tuple_processing(context): assert context.agent.last_message == "tuple-msg" assert context.agent.last_context == {"ctx": 1} assert context.captured_output == ["processed:tuple-msg"] @when("I process a non tuple message through the wrapper") def step_process_non_tuple(context): context.captured_output = [] context.agent.output_stream.subscribe( lambda value: context.captured_output.append(value) ) asyncio.run(context.agent._process_wrapper("plain-msg")) @then("the non tuple message should be processed with empty context") def step_verify_non_tuple_processing(context): assert context.agent.last_message == "plain-msg" assert context.agent.last_context == {} assert context.captured_output == ["processed:plain-msg"] @when("process_message raises an error inside the wrapper") def step_wrapper_raises(context): error_agent = DefaultAgent("err-agent", raise_error=True) context.wrapper_error = None try: asyncio.run(error_agent._process_wrapper("fail")) except Exception as exc: context.wrapper_error = exc @then("an ExecutionError should be raised with the agent name") def step_verify_execution_error(context): assert isinstance(context.wrapper_error, ExecutionError) assert "err-agent" in str(context.wrapper_error) @when("I call process_message_sync on the agent") def step_call_process_message_sync(context): loop = context.loop asyncio.set_event_loop(loop) context.sync_result = context.agent.process_message_sync("sync-msg", {"sync": True}) @then("the synchronous result should match the async process output") def step_verify_sync_result(context): assert context.sync_result == "processed:sync-msg" assert context.agent.last_context == {"sync": True} @when("I call dispose on an agent with disposable streams") def step_call_dispose(context): context.agent.input_stream = MagicMock(dispose=MagicMock()) context.agent.output_stream = MagicMock(dispose=MagicMock()) context.agent.dispose() @then("both input and output streams should be disposed if available") def step_verify_dispose(context): context.agent.input_stream.dispose.assert_called_once() context.agent.output_stream.dispose.assert_called_once() @when("I remember a value and recall it later") def step_memory_remember_recall(context): context.memory_agent = MemoryAgent("mem-agent") asyncio.run(context.memory_agent.remember("answer", 42)) context.recalled = asyncio.run(context.memory_agent.recall("answer")) @then("the remembered value should be returned from memory") def step_verify_memory_recall(context): assert context.recalled == 42