test: add detailed router scenarios and steps for coverage

This commit is contained in:
2025-07-06 19:23:48 +00:00
parent 47b32b2800
commit 245a8774e8
2 changed files with 490 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
Feature: Router Detailed Scenarios for coverage
Background:
Given I import the agent module
Scenario: Add an invalid route (not a dictionary)
Given a router with no agents
When I try to add a route which is not a dictionary
Then a RoutingError should be raised with message "Route must be a dictionary"
Scenario Outline: Add a route with missing required keys
Given a router with agent "agent1"
When I try to add a route with key "<key_to_remove>" removed from a valid route
Then a RoutingError should be raised with message "Route missing required key: <key_to_remove>"
Examples:
| key_to_remove |
| from |
| to |
Scenario Outline: Add a route with non-existent agents
Given a router with one agent named "agent1"
When I try to add a route from "<source>" to "<destination>"
Then a RoutingError should be raised with message "<error_message>"
Examples:
| source | destination | error_message |
| agent2 | agent1 | Source agent 'agent2' not found |
| agent1 | agent2 | Destination agent 'agent2' not found |
Scenario Outline: Add a route with alias keys
Given a router with agent "agent1"
When I add a route with from key "<from_key>" and to key "<to_key>"
And I route the message "hello" from "input"
Then the destination should be "agent1"
Examples:
| from_key | to_key |
| source | destination |
| from | destination |
| source | to |
Scenario: Route message with no matching route for input and a single agent
Given a router with agent "agent1"
And no routes
When I route the message "hello" from "input"
Then the destination should be "agent1"
And the message must be "hello"
Scenario: Route message with no matching route for input and multiple agents
Given a router with agents "agent1" and "agent2"
And no routes
When I try to route the message "hello" from "input"
Then a RoutingError should be raised with message "No route found for input message"
Scenario: Route message with no matching route for a non-input source
Given a router with agent "agent1"
And a route coming from "input" to "agent1"
When I route the message "hello" from "agent1"
Then the destination should be "output"
And the message must be "hello"
Scenario: Route message with a condition that raises an exception
Given a router with agent "agent1"
And a route from "input" to "agent1" with a condition that will raise an exception
When I try to route the message "hello" from "input"
Then a RoutingError should be raised with a message containing "Error evaluating condition"
Scenario: Route message with context modification that renders invalid JSON
Given a router with agent "agent1"
And a route from "input" to "agent1" with a context modification template "{'key': 'value'}" which is invalid JSON
When I try to route the message "hello" from "input"
Then a RoutingError should be raised with a message containing "Failed to decode JSON from rendered context template"
Scenario: Route message with context modification that renders non-dict JSON
Given a router with agent "agent1"
And a route from "input" to "agent1" with a context modification template that renders to a list
When I try to route the message "hello" from "input"
Then a RoutingError should be raised with message "Context template must render to a JSON object (dict)."
Scenario: Route message with context modification that fails to render
Given a router with agent "agent1"
And a route from "input" to "agent1" with a context modification template that will fail to render
When I try to route the message "hello" from "input"
Then a RoutingError should be raised with a message containing "Error processing context template"
Scenario: Route message with context modification attempting to modify a protected key
Given a router with agent "agent1"
And a route must go from "input" to "agent1" with a context modification template that modifies "history"
When I try to route the message "hello" from "input"
Then a RoutingError should be raised with message "Cannot modify or delete protected context key: 'history'"
Scenario: Route message with context modification that deletes a key
Given a router with agent "agent1"
And a route from "input" to "agent1" with a context modification template that deletes key "to_delete"
And an initial context with "to_delete" set to "some_value"
When I route the message "hello" from "input" with the initial context
Then the context should not contain the key "to_delete"
Scenario: Route message with an unsupported transform format
Given a router with agent "agent1"
And a route from "input" to "agent1" with an unsupported transform format
When I try to route the message "hello" from "input"
Then a RoutingError should be raised with a message containing "Unsupported transform format"
Scenario: Route message with a transform that fails to render
Given a router with agent "agent1"
And a route from "input" to "agent1" with a transform template that will fail to render
When I try to route the message "hello" from "input"
Then a RoutingError should be raised with a message containing "Error applying template transformation"
Scenario: Route message with transform as a dictionary
Given a router with agent "agent1"
And a route from "input" to "agent1" with a transform as a dictionary
When I route the message "hello" from "input"
Then the message must be "transformed: hello"
Scenario: Process message with a destination agent not in router
Given a router with agent "agent1"
And a special route from "input" to "non_existent_agent"
When I try to process the message "hello" with the router that fails
Then a RoutingError should be raised with message "Destination agent 'non_existent_agent' not found in router."
Scenario: Process message where agent process raises an exception
Given a router with an agent "failing_agent" that always fails
And a route coming from "input" to "failing_agent"
When I try to process the message "hello" with the router that fails
Then a RoutingError should be raised with a message containing "Error processing message with agent 'failing_agent'"
Scenario: Process message that exceeds maximum routing steps
Given a router with agents "agent1" and "agent2"
And a route coming from "input" to "agent1"
And a route coming from "agent1" to "agent2"
And a route coming from "agent2" to "agent1"
When I try to process the message "hello" with the router that fails
Then a RoutingError should be raised with message "Maximum routing steps exceeded"
Scenario: Process message correctly populates initial context
Given a router with agent "agent1"
And a route coming from "input" to "agent1"
And a route coming from "agent1" to "output"
When I process the message "hello" with the router
Then agent "agent1" should have received a context containing "initial_message" with value "hello"
And agent "agent1" should have received a context containing "history" as an empty list
@@ -0,0 +1,349 @@
import asyncio
import copy
import json
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from unittest.mock import MagicMock
from behave import given
from behave import then
from behave import when
from cleveragents.agents.base import Agent
from cleveragents.core.exceptions import RoutingError
from cleveragents.core.exceptions import TemplateError
from cleveragents.routing.router import Router
from cleveragents.templates.renderer import TemplateRenderer
# Mock Agent class for testing
class MockAgent(Agent):
def __init__(
self,
name: str,
config: Dict[str, Any] = None,
template_renderer=None,
should_fail=False,
):
super().__init__(name, config or {}, template_renderer)
self.should_fail = should_fail
self.received_messages = []
async def process(
self, message: str, context: Optional[Dict[str, Any]] = None
) -> str:
if self.should_fail:
raise Exception("Agent processing failed")
self.received_messages.append(
{"message": message, "context": copy.deepcopy(context)}
)
# Return a different message to test message propagation
return f"processed by {self.name}"
def get_capabilities(self) -> List[str]:
return ["mock"]
# Helper to set up a router
def setup_router(context, agent_names: List[str]):
context.template_renderer = MagicMock(spec=TemplateRenderer)
context.agents = {
name: MockAgent(name, template_renderer=context.template_renderer)
for name in agent_names
}
context.router = Router("test_router", context.agents, context.template_renderer)
def _route_message_step(context, message, source, use_initial_context=False):
context.error = None
context.result = None
initial_context = (
context.initial_context
if use_initial_context and hasattr(context, "initial_context")
else {}
)
async def run():
try:
context.result = await context.router.route_message(
message, source=source, context=initial_context
)
# The context might have been modified, so we store it for assertions
context.final_context = initial_context
except RoutingError as e:
context.error = e
asyncio.run(run())
@given("a router with no agents")
def step_impl(context):
setup_router(context, [])
@when("I try to add a route which is not a dictionary")
def step_impl(context):
context.error = None
try:
context.router.add_route("not a dict")
except RoutingError as e:
context.error = e
@then('a RoutingError should be raised with message "{message}"')
def step_impl(context, message):
assert context.error is not None, "RoutingError was not raised"
assert isinstance(context.error, RoutingError)
assert str(context.error) == message
@given('a router with agent "{agent_name}"')
def step_impl(context, agent_name):
setup_router(context, [agent_name])
@when('I try to add a route with key "{key_to_remove}" removed from a valid route')
def step_impl(context, key_to_remove):
route = {"from": "input", "to": "agent1"}
del route[key_to_remove]
context.error = None
try:
context.router.add_route(route)
except RoutingError as e:
context.error = e
@given('a router with one agent named "{agent_name}"')
def step_impl(context, agent_name):
setup_router(context, [agent_name])
@when('I try to add a route from "{source}" to "{destination}"')
def step_impl(context, source, destination):
route = {"from": source, "to": destination}
context.error = None
try:
context.router.add_route(route)
except RoutingError as e:
context.error = e
@when('I add a route with from key "{from_key}" and to key "{to_key}"')
def step_impl(context, from_key, to_key):
route = {from_key: "input", to_key: "agent1"}
context.router.add_route(route)
@given("no routes")
def step_impl(context):
context.router.routes = []
@when('I route the message "{message}" from "{source}"')
def step_impl_route(context, message, source):
_route_message_step(context, message, source)
@when('I try to route the message "{message}" from "{source}"')
def step_impl_try_route(context, message, source):
_route_message_step(context, message, source)
@then('the destination should be "{destination}"')
def step_impl(context, destination):
assert context.result["destination"] == destination
@then('the message must be "{message}"')
def step_impl(context, message):
assert context.result["message"] == message
@given('a router with agents "{agent1}" and "{agent2}"')
def step_impl(context, agent1, agent2):
setup_router(context, [agent1, agent2])
@given('a route coming from "{source}" to "{destination}"')
def step_impl(context, source, destination):
context.router.add_route({"from": source, "to": destination})
@given(
'a route from "{source}" to "{destination}" with a condition that will raise an exception'
)
def step_impl(context, source, destination):
mock_evaluator = MagicMock()
mock_evaluator.evaluate.side_effect = Exception("mock condition error")
context.router.condition_evaluator = mock_evaluator
context.router.add_route(
{"from": source, "to": destination, "condition": "anything"}
)
@then('a RoutingError should be raised with a message containing "{text}"')
def step_impl(context, text):
assert context.error is not None, "RoutingError was not raised"
assert isinstance(context.error, RoutingError)
assert text in str(context.error)
@given(
'a route from "{source}" to "{destination}" with a context modification template "{template}" which is invalid JSON'
)
def step_impl(context, source, destination, template):
context.template_renderer.render_string.return_value = template
route = {"from": source, "to": destination, "context": "some template string"}
context.router.add_route(route)
@given(
'a route from "{source}" to "{destination}" with a context modification template that renders to a list'
)
def step_impl(context, source, destination):
context.template_renderer.render_string.return_value = '["item1", "item2"]'
route = {"from": source, "to": destination, "context": "some template string"}
context.router.add_route(route)
@given(
'a route from "{source}" to "{destination}" with a context modification template that will fail to render'
)
def step_impl(context, source, destination):
context.template_renderer.render_string.side_effect = TemplateError(
"mock render error"
)
route = {"from": source, "to": destination, "context": "failing template"}
context.router.add_route(route)
@given(
'a route must go from "{source}" to "{destination}" with a context modification template that modifies "{protected_key}"'
)
def step_impl(context, source, destination, protected_key):
context.template_renderer.render_string.return_value = json.dumps(
{protected_key: "new value"}
)
route = {"from": source, "to": destination, "context": "modifying template"}
context.router.add_route(route)
@given(
'a route from "{source}" to "{destination}" with a context modification template that deletes key "{key_to_delete}"'
)
def step_impl(context, source, destination, key_to_delete):
context.template_renderer.render_string.return_value = json.dumps(
{key_to_delete: None}
)
route = {"from": source, "to": destination, "context": "deleting template"}
context.router.add_route(route)
@given('an initial context with "{key}" set to "{value}"')
def step_impl(context, key, value):
context.initial_context = {key: value}
@when('I route the message "{message}" from "{source}" with the initial context')
def step_impl(context, message, source):
_route_message_step(context, message, source, use_initial_context=True)
@when('I try to process the message "{message}" with the router that fails')
def step_impl_try_process_fail(context, message):
context.error = None
context.response = None
try:
async def run():
context.response = await context.router.process_message(message)
asyncio.run(run())
except RoutingError as e:
context.error = e
@then('the context should not contain the key "{key}"')
def step_impl(context, key):
assert key not in context.final_context
@given(
'a route from "{source}" to "{destination}" with an unsupported transform format'
)
def step_impl(context, source, destination):
route = {"from": source, "to": destination, "transform": 123}
context.router.add_route(route)
@given(
'a route from "{source}" to "{destination}" with a transform template that will fail to render'
)
def step_impl(context, source, destination):
context.template_renderer.render_string.side_effect = TemplateError(
"mock render error"
)
route = {"from": source, "to": destination, "transform": "failing template"}
context.router.add_route(route)
@given('a route from "{source}" to "{destination}" with a transform as a dictionary')
def step_impl(context, source, destination):
context.template_renderer.render_string.return_value = "transformed: hello"
route = {
"from": source,
"to": destination,
"transform": {"template": "transformed: {message}"},
}
context.router.add_route(route)
@when('I process the message "{message}" with the router')
def step_impl(context, message):
context.error = None
context.response = None
async def run():
try:
context.response = await context.router.process_message(message)
except (RoutingError, Exception) as e:
context.error = e
asyncio.run(run())
@given('a router with an agent "{agent_name}" that always fails')
def step_impl(context, agent_name):
context.template_renderer = MagicMock(spec=TemplateRenderer)
context.agents = {agent_name: MockAgent(agent_name, should_fail=True)}
context.router = Router("test_router", context.agents, context.template_renderer)
@then(
'agent "{agent_name}" should have received a context containing "{key}" with value "{value}"'
)
def step_impl(context, agent_name, key, value):
agent = context.agents[agent_name]
assert len(agent.received_messages) > 0
received_context = agent.received_messages[0]["context"]
assert key in received_context
assert received_context[key] == value
@then(
'agent "{agent_name}" should have received a context containing "{key}" as an empty list'
)
def step_impl(context, agent_name, key):
agent = context.agents[agent_name]
assert len(agent.received_messages) > 0
received_context = agent.received_messages[0]["context"]
assert key in received_context
assert received_context[key] == []
@given('a special route from "{source}" to "{destination}"')
def step_impl(context, source, destination):
context.router.routes.append({"from": source, "to": destination})