558 lines
18 KiB
Python
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
|