test: add detailed router scenarios and steps for coverage
This commit is contained in:
@@ -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})
|
||||
Reference in New Issue
Block a user