Files
cleveragents-core/features/steps/route_coverage_comprehensive_steps.py
T

558 lines
18 KiB
Python

"""Route module comprehensive coverage steps (ported)."""
from __future__ import annotations
from pathlib import Path
from behave import given, then, when
from cleveragents.core.exceptions import ConfigurationError
from cleveragents.langgraph.graph import GraphConfig
from cleveragents.langgraph.nodes import Edge, NodeConfig, NodeType
from cleveragents.reactive.route import (
BridgeConfig,
RouteComplexityAnalyzer,
RouteConfig,
RouteType,
)
from cleveragents.reactive.stream_router import StreamConfig, StreamType
@given("the route system is available")
def step_route_system_available(context):
context.route_configs = {}
context.stream_configs = {}
context.graph_configs = {}
context.analysis_results = {}
context.errors = {}
@given("I create a RouteConfig with graph type but no nodes")
def step_create_graph_config_no_nodes(context):
context.test_config_params = {
"name": "test_graph",
"type": RouteType.GRAPH,
"nodes": {},
}
@when("I validate the configuration")
def step_validate_configuration(context):
try:
context.test_config = RouteConfig(**context.test_config_params)
context.validation_error = None
except Exception as exc: # pylint: disable=broad-except
context.validation_error = exc
@then("I should get a ConfigurationError about missing nodes")
def step_verify_missing_nodes_error(context):
assert context.validation_error is not None
assert isinstance(context.validation_error, ConfigurationError)
assert "must have nodes defined" in str(context.validation_error)
@given("I create a RouteConfig with graph type")
def step_create_graph_route_config(context):
context.test_config = RouteConfig(
name="test_graph",
type=RouteType.GRAPH,
nodes={"test_node": {"type": "agent"}},
edges=[{"source": "start", "target": "test_node"}],
)
@when("I try to convert it to StreamConfig")
def step_convert_to_stream_config(context):
try:
context.conversion_result = context.test_config.to_stream_config()
context.conversion_error = None
except Exception as exc: # pylint: disable=broad-except
context.conversion_error = exc
@then("I should get a ValueError about invalid conversion")
def step_verify_invalid_conversion_error(context):
assert context.conversion_error is not None
assert isinstance(context.conversion_error, ValueError)
assert "Cannot convert" in str(context.conversion_error)
@given("I create a RouteConfig with stream type")
def step_create_stream_route_config(context):
context.test_config = RouteConfig(
name="test_stream",
type=RouteType.STREAM,
stream_type=StreamType.COLD,
operators=[{"type": "map"}],
)
@when("I try to convert it to GraphConfig")
def step_convert_to_graph_config(context):
try:
context.conversion_result = context.test_config.to_graph_config()
context.conversion_error = None
except Exception as exc: # pylint: disable=broad-except
context.conversion_error = exc
@given("I have a valid StreamConfig")
def step_create_valid_stream_config(context):
context.source_stream_config = StreamConfig(
name="test_stream",
type=StreamType.HOT,
operators=[
{"type": "map", "params": {"function": "transform"}},
{"type": "filter", "params": {"predicate": "is_valid"}},
],
subscriptions=["input_topic"],
publications=["output_topic"],
agents=["agent1", "agent2"],
initial_value="initial",
buffer_size=10,
template_config={"template": "test"},
)
@when("I create a RouteConfig from the StreamConfig")
def step_create_route_from_stream(context):
context.result_route_config = RouteConfig.from_stream_config(
context.source_stream_config
)
@then("the RouteConfig should have stream type")
def step_verify_stream_type(context):
assert context.result_route_config.type == RouteType.STREAM
@then("it should preserve all stream configuration details")
def step_verify_stream_details_preserved(context):
route = context.result_route_config
source = context.source_stream_config
assert route.name == source.name
assert route.stream_type == source.type
assert route.operators == source.operators
assert route.subscriptions == source.subscriptions
assert route.publications == source.publications
assert route.agents == source.agents
assert route.initial_value == source.initial_value
assert route.buffer_size == source.buffer_size
assert route.template_config == source.template_config
@given("I have a valid GraphConfig with nodes and edges")
def step_create_valid_graph_config(context):
node1 = NodeConfig(
name="node1",
type=NodeType.AGENT,
agent="test_agent",
tools=["tool1", "tool2"],
retry_policy={"max_retries": 3},
timeout=30,
parallel=True,
metadata={"key": "value"},
)
node2 = NodeConfig(
name="node2",
type=NodeType.FUNCTION,
function="test_function",
condition={"type": "test_condition"},
subgraph="test_subgraph",
)
edge1 = Edge(
source="start",
target="node1",
condition={"type": "edge_condition"},
metadata={"edge_key": "edge_value"},
)
edge2 = Edge(source="node1", target="node2")
context.source_graph_config = GraphConfig(
name="test_graph",
nodes={"node1": node1, "node2": node2},
edges=[edge1, edge2],
entry_point="start",
state_class=None,
checkpointing=True,
checkpoint_dir=Path("/tmp/checkpoints"),
enable_time_travel=True,
parallel_execution=True,
metadata={"graph_meta": "meta"},
)
@when("I create a RouteConfig from the GraphConfig")
def step_create_route_from_graph(context):
context.result_route_config = RouteConfig.from_graph_config(
context.source_graph_config
)
@then("the RouteConfig should have graph type")
def step_verify_graph_type(context):
assert context.result_route_config.type == RouteType.GRAPH
@then("it should preserve all graph configuration details")
def step_verify_graph_details_preserved(context):
route = context.result_route_config
source = context.source_graph_config
assert route.name == source.name
assert route.nodes
assert route.edges
assert route.entry_point == source.entry_point
assert route.checkpointing == source.checkpointing
assert (
(route.checkpoint_dir == str(source.checkpoint_dir))
if source.checkpoint_dir
else True
)
assert route.enable_time_travel == source.enable_time_travel
assert route.parallel_execution == source.parallel_execution
assert route.metadata == source.metadata
@then("node configurations should be converted to dictionaries")
def step_verify_node_conversion(context):
assert all(isinstance(v, dict) for v in context.result_route_config.nodes.values())
@then("edge configurations should be converted to dictionaries")
def step_verify_edge_conversion(context):
assert all(isinstance(e, dict) for e in context.result_route_config.edges)
@given("I have a RouteConfig with bridge type")
def step_bridge_type_config(context):
context.bridge_route = RouteConfig(
name="bridge_route", type=RouteType.BRIDGE, bridge=BridgeConfig()
)
@when("I analyze the route complexity")
def step_analyze_route_complexity(context):
context.analysis = RouteComplexityAnalyzer.analyze_route(context.bridge_route)
@then("it should return bridge complexity analysis")
def step_bridge_analysis(context):
assert context.analysis.get("complexity") == "bridge"
@then('the complexity should be "{expected}"')
def step_complexity_value(context, expected):
assert str(context.analysis.get("complexity")) == expected
@then("the score should be {score:d}")
def step_score_value(context, score):
assert int(context.analysis.get("score", -1)) == score
@given("I have stream routes with different features")
def step_stream_routes_features(context):
context.stream_routes = [
RouteConfig(name="simple", type=RouteType.STREAM, operators=[]),
RouteConfig(
name="ops",
type=RouteType.STREAM,
operators=[{"type": "map"}, {"type": "filter"}],
),
RouteConfig(
name="connected",
type=RouteType.STREAM,
operators=[{"type": "map"}],
subscriptions=["s1"],
publications=["s2"],
),
RouteConfig(
name="hot", type=RouteType.STREAM, stream_type=StreamType.HOT, operators=[]
),
]
@when("I analyze each route complexity")
def step_analyze_each_route(context):
context.analysis_results = [
RouteComplexityAnalyzer.analyze_route(rc) for rc in context.stream_routes
]
@then("routes with more operators should have higher complexity scores")
def step_routes_more_ops_score(context):
scores = [a["score"] for a in context.analysis_results]
assert max(scores) >= min(scores)
@then("routes with routing connections should get additional score")
def step_routes_connections_score(context):
scores = [a["score"] for a in context.analysis_results]
assert scores[2] >= scores[1]
@then("hot streams should get additional score")
def step_hot_stream_score(context):
scores = [a["score"] for a in context.analysis_results]
assert scores[3] >= scores[0]
@then("the complexity classification should match the score ranges")
def step_complexity_classification(context):
for analysis in context.analysis_results:
c = analysis["complexity"]
s = analysis["score"]
if s <= 2:
assert c == "simple"
elif s <= 5:
assert c in ("simple", "moderate")
else:
assert c in ("moderate", "complex")
@given("I have graph routes with conditional edges")
def step_graph_routes_conditionals(context):
context.graph_routes = [
RouteConfig(
name="cond_graph",
type=RouteType.GRAPH,
nodes={"n1": {"type": "agent"}, "n2": {"type": "function"}},
edges=[{"source": "n1", "target": "n2", "condition": {"field": "x"}}],
checkpointing=True,
enable_time_travel=True,
)
]
@when("I analyze the graph complexity")
def step_analyze_graph_complexity(context):
context.analysis_results = [
RouteComplexityAnalyzer.analyze_route(rc) for rc in context.graph_routes
]
@then("conditional edges should increase the complexity score")
def step_cond_edges_score(context):
assert context.analysis_results[0]["score"] >= 9
@then("the features should include conditional edge information")
def step_cond_edges_features(context):
assert any("conditional" in f for f in context.analysis_results[0]["features"])
@then("checkpointing should increase the score")
def step_checkpointing_score(context):
assert "checkpointing enabled" in context.analysis_results[0]["features"]
@then("time travel should increase the score")
def step_time_travel_score(context):
assert "time travel enabled" in context.analysis_results[0]["features"]
@given("I have streams with different complexity scores")
def step_streams_for_recommendations(context):
context.stream_scores = [
RouteComplexityAnalyzer._get_stream_recommendation(1),
RouteComplexityAnalyzer._get_stream_recommendation(3),
RouteComplexityAnalyzer._get_stream_recommendation(6),
]
@when("I get recommendations for each stream")
def step_get_stream_recs(context):
context.recs = context.stream_scores
@then("simple streams should get simple transformation recommendation")
def step_simple_stream_rec(context):
assert "simple" in context.recs[0].lower()
@then("moderate streams should get multi-step processing recommendation")
def step_moderate_stream_rec(context):
assert (
"multi-step" in context.recs[1].lower()
or "pipelines" in context.recs[1].lower()
)
@then("complex streams should get graph consideration recommendation")
def step_complex_stream_rec(context):
assert "graph" in context.recs[2].lower() or "consider" in context.recs[2].lower()
@given("I have graphs with different complexity scores")
def step_graphs_for_recommendations(context):
context.graph_scores = [
RouteComplexityAnalyzer._get_graph_recommendation(8),
RouteComplexityAnalyzer._get_graph_recommendation(12),
RouteComplexityAnalyzer._get_graph_recommendation(18),
]
@when("I get recommendations for each graph")
def step_get_graph_recs(context):
context.graph_recs = context.graph_scores
@then("moderate graphs should get conditional logic recommendation")
def step_moderate_graph_rec(context):
assert "conditional" in context.graph_recs[0].lower()
@then("complex graphs should get stateful workflow recommendation")
def step_complex_graph_rec(context):
assert "stateful" in context.graph_recs[1].lower()
@then("advanced graphs should get feature evaluation recommendation")
def step_advanced_graph_rec(context):
assert (
"advanced" in context.graph_recs[2].lower()
or "ensure" in context.graph_recs[2].lower()
)
@given("I have different requirement scenarios")
def step_requirement_scenarios(context):
context.requirements = [
{"needs_persistence": True},
{"needs_state": True, "needs_conditionals": True},
{"needs_conditionals": True, "is_continuous": False},
{"is_stateless": True, "is_continuous": True},
{},
]
@when("I ask for route type suggestions")
def step_suggest_route_types(context):
context.suggestions = [
RouteComplexityAnalyzer.suggest_route_type(req) for req in context.requirements
]
@then("persistence requirements should suggest graph")
def step_persistence_suggests_graph(context):
assert context.suggestions[0] == RouteType.GRAPH
@then("state with conditionals should suggest graph")
def step_state_conditionals_suggest_graph(context):
assert context.suggestions[1] == RouteType.GRAPH
@then("conditionals without continuous should suggest graph")
def step_conditionals_no_continuous_graph(context):
assert context.suggestions[2] == RouteType.GRAPH
@then("stateless continuous should suggest stream")
def step_stateless_continuous_stream(context):
assert context.suggestions[3] == RouteType.STREAM
@then("simple cases should default to stream")
def step_simple_cases_stream(context):
assert context.suggestions[4] == RouteType.STREAM
@given("I create a stream RouteConfig without specifying stream_type")
def step_stream_without_stream_type(context):
context.test_config = RouteConfig(name="default_stream", type=RouteType.STREAM)
@when("the post_init validation runs")
def step_post_init_validation(context):
context.post_init_type = context.test_config.stream_type
@then("the stream_type should default to COLD")
def step_stream_type_default(context):
assert context.post_init_type == StreamType.COLD
@given("I have a RouteConfig with various node types and configurations")
def step_complex_node_route(context):
context.complex_route = RouteConfig(
name="complex_graph",
type=RouteType.GRAPH,
nodes={
"agent_node": {
"type": "agent",
"agent": "agentA",
"retry_policy": {"max_retries": 2},
"timeout": 5,
},
"func_node": {
"type": "function",
"function": "fn1",
"parallel": True,
"metadata": {"x": 1},
},
"router": {
"type": "message_router",
"metadata": {"rules": [{"target": "end"}]},
},
},
edges=[
{"source": "start", "target": "agent_node"},
{
"source": "agent_node",
"target": "func_node",
"condition": {"equals": "ok"},
},
{"source": "func_node", "target": "router"},
{"source": "router", "target": "end"},
],
entry_point="start",
checkpointing=False,
enable_time_travel=False,
parallel_execution=True,
metadata={"meta": True},
)
@when("I convert it to GraphConfig")
def step_convert_complex_route(context):
context.graph_config_result = context.complex_route.to_graph_config()
@then("all node types should be properly converted")
def step_all_nodes_converted(context):
assert set(context.graph_config_result.nodes.keys()) == set(
context.complex_route.nodes.keys()
)
@then("all node properties should be preserved")
def step_node_properties_preserved(context):
for name, node_cfg in context.graph_config_result.nodes.items():
source = context.complex_route.nodes[name]
if "agent" in source:
assert node_cfg.agent == source["agent"]
if "function" in source:
assert node_cfg.function == source["function"]
@then("retry policies and timeouts should be included")
def step_retry_timeout_included(context):
node = context.graph_config_result.nodes["agent_node"]
assert node.retry_policy == context.complex_route.nodes["agent_node"].get(
"retry_policy"
)
assert node.timeout == context.complex_route.nodes["agent_node"].get("timeout")
@then("conditions and subgraphs should be handled")
def step_conditions_subgraphs(context):
edge = next(e for e in context.graph_config_result.edges if e.target == "func_node")
assert edge.condition is not None
@then("node metadata should be preserved in conversion")
def step_metadata_preserved(context):
router = context.graph_config_result.nodes["router"]
assert router.metadata.get("rules") is not None