Files
temp/tests/features/steps/message_context_persistence_steps.py

142 lines
4.9 KiB
Python

import asyncio
import unittest.mock
from typing import Any
from typing import Dict
from typing import Optional
from behave import given
from behave import then
from behave import when
from cleveragents.agents.base import Agent
from cleveragents.routing.router import Router
from cleveragents.templates.renderer import TemplateEngine
from cleveragents.templates.renderer import TemplateRenderer
class MockAgent(Agent):
"""A mock agent to control its output and track its inputs."""
def __init__(self, name, config, template_renderer, response="default response"):
super().__init__(name, config, template_renderer)
# Use create_autospec so the mock has the correct async signature,
# ensuring it handles keyword arguments like `context=...`.
self.process = unittest.mock.create_autospec(
self.process, return_value=response
)
async def process(
self,
message: str,
context: Optional[Dict[str, Any]] = None,
) -> str: # type: ignore[override]
"""
Concrete implementation to satisfy the abstract method in `Agent`.
It is immediately shadowed by the AsyncMock assigned in `__init__`,
so it should never be invoked during tests.
"""
raise NotImplementedError(
"This method is expected to be mocked."
) # pragma: no cover
def get_capabilities(self):
return ["mock"]
@given("a router configured for a multi-step workflow")
def step_impl(context):
"""
Sets up a router with two agents: a classifier and a responder.
The key part is the route from the classifier to the responder,
which uses a transform to pass the original message from the context.
"""
# Use the simple template engine which exhibits the bug
context.template_renderer = TemplateRenderer(engine_type=TemplateEngine.SIMPLE)
# Mock agents
context.classifier_agent = MockAgent(
name="classifier",
config={},
template_renderer=context.template_renderer,
response="NEEDS_INFO",
)
context.responder_agent = MockAgent(
name="responder",
config={},
template_renderer=context.template_renderer,
response="The capital of France is Paris.",
)
agents = {
"classifier": context.classifier_agent,
"responder": context.responder_agent,
}
# Setup router
context.router = Router(
name="test_router", agents=agents, template_renderer=context.template_renderer
)
# Define routes that mimic the scenario leading to the bug
routes = [
{"from": "input", "to": "classifier"},
{
"from": "classifier",
"to": "responder",
# This transform is crucial. It attempts to retrieve the original message.
# The bug is that this evaluates to an empty string.
"transform": "{{ context['initial_message'] }}",
},
{"from": "responder", "to": "output"},
]
context.router.add_routes(routes)
@when('I process the prompt "{prompt}"')
def step_impl(context, prompt):
"""
Processes the given prompt through the configured router.
"""
context.initial_prompt = prompt
context.final_result = asyncio.run(
context.router.process_message(context.initial_prompt)
)
@then('the final output should be "{expected_output}"')
def step_impl(context, expected_output):
"""
Verifies that the responder agent received the correct (original) message
and that the final output from the router is as expected.
"""
# 1. Verify the first agent was called with the initial prompt.
context.classifier_agent.process.assert_called_once_with(
context.initial_prompt, context=unittest.mock.ANY
)
# 2. This is the key assertion that should FAIL due to the bug.
# We expect the responder agent to be called with the initial prompt,
# passed via the transform. The bug causes it to be called with "".
try:
context.responder_agent.process.assert_called_once_with(
context.initial_prompt, context=unittest.mock.ANY
)
except AssertionError as e:
# We expect this to fail, but let's add a more informative message
# to the test output if it does.
call_args, _ = context.responder_agent.process.call_args
actual_message = call_args[0]
error_message = (
f"BUG REPRODUCED: Responder agent was called with incorrect message.\n"
f"Expected: '{context.initial_prompt}'\n"
f"Actual: '{actual_message}'\n"
f"Original AssertionError: {e}"
)
raise AssertionError(error_message) from e
# 3. Check the final output. Note: this assertion may pass if the mock
# is not dependent on its input, but the assertion above is the real test.
assert (
context.final_result == expected_output
), f"Expected final output '{expected_output}', but got '{context.final_result}'"