Files
cleveractors-core/features/steps/graph_templates_steps.py
CoreRasurae 9753b31c7e
CI / quality (push) Successful in 36s
CI / lint (push) Successful in 38s
CI / typecheck (push) Successful in 49s
CI / security (push) Successful in 51s
CI / integration_tests (push) Successful in 55s
CI / unit_tests (push) Successful in 3m35s
CI / build (push) Successful in 33s
CI / coverage (push) Successful in 3m36s
CI / status-check (push) Successful in 3s
test: add coverage gap tests improving coverage from 81.4% to 96.90%
Add 13 BDD scenarios covering previously uncovered code paths:
- SAFE_BUILTINS validation (sandbox.py: 0% → 100%)
- ConfigurationError re-raise path (config.py: 99.3% → 100%)
- CLI hello/main functions (cli.py: 72.7% → 90.9%)
- GraphState message truncation (state.py: 98.7% → 100%)
- ProgressBarManager update/context rendering (progress.py: 0% → 87.7%)
- MessageRouter regex/exact/contains routing (message_router.py: 0% → 59.4%)
- RoutingAdapter parse_routing_command (routing_adapter.py)
- DynamicRouterNode pattern-based routing (dynamic_router.py)
- EnhancedTemplateRegistry unknown template type (enhanced_registry.py: 99.2%)
- CompositeAgent null-graph error path (composite.py: 97.8% → 98.6%)

Cover routing_adapter.py (17% → 100%): all GOTO/ROUTE patterns,
create_routing_node, create_conditional_router, dynamic config conversion.

Cover dynamic_router.py (21% → 87%): execute with empty/dict/string
messages, extract_message with colon parsing, config creation, graph
extension with edge generation.

Cover message_router.py (59% → 78%): regex, exact, contains, prefix,
suffix match types, invalid regex handling, non-string message, set_state.

Exercise Node._prepare_conversation_history with invalid configs,
_runtime error paths, _execute_message_router with rules,
_execute_agent with current_message and metadata propagation,
_execute_function with dynamic_router, and _execute_conditional
with content_contains/content_not_contains/content_starts_with
and custom condition types. Improves nodes.py from 71.3% to 72.5%.

Exercise PureGraphConfig, PureLangGraph init with dict/config,
RxPyLangGraphBridge registration/connection/lookup,
ReactiveStreamRouter operator creation and condition functions,
ReactiveConfigParser config/route/graph parsing,
ToolAgent tool execution with JSON, space-separated, single,
file_read, progress_bar invocations, and
ReactiveCleverAgentsApp init/dispose/visualization.
2026-06-02 20:02:01 +01:00

609 lines
24 KiB
Python

"""
Step definitions for Graph Template Node and Edge Processing BDD tests.
"""
import copy
from unittest.mock import Mock, patch
from behave import given, then, when
from cleveractors.templates.base import (
InstantiationContext,
TemplateType,
)
from cleveractors.templates.graph_templates import GraphTemplate
@given("I have a clean test environment for graph templates")
def step_clean_environment_graph_templates(context):
"""Clean test environment setup for graph templates."""
context.graph_template = None
context.registry = None
context.params = {}
context.instantiation_context = None
context.result = None
context.original_definition = None
@given("I have a graph template registry")
def step_graph_template_registry(context):
"""Setup graph template registry."""
context.registry = Mock()
@given("I have a basic graph template")
def step_basic_graph_template(context):
"""Setup basic graph template."""
definition = {
"name": "test_graph",
"parameters": {"test_param": {"type": "string", "default": "test_value"}},
"nodes": {"node1": {"type": "task", "config": {"task": "test_task"}}},
"edges": [{"from": "node1", "to": "node2"}],
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
context.original_definition = copy.deepcopy(definition)
@given("I have a graph template with required parameters")
def step_graph_template_with_required_parameters(context):
"""Setup graph template with required parameters."""
definition = {
"name": "test_graph",
"parameters": {
"required_param": {"type": "string", "required": True},
"optional_param": {"type": "string", "default": "default_value"},
},
"nodes": {"node1": {"type": "task", "config": {"task": "{{required_param}}"}}},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with template variables")
def step_graph_template_with_template_variables(context):
"""Setup graph template with template variables."""
definition = {
"name": "{{graph_name}}",
"nodes": {
"{{node_name}}": {"type": "task", "config": {"task": "{{task_name}}"}}
},
"edges": [{"from": "{{source_node}}", "to": "{{target_node}}"}],
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template for deep copy test")
def step_graph_template_for_deep_copy_test(context):
"""Setup graph template for deep copy test."""
definition = {
"name": "test_graph",
"parameters": {"test_param": {"type": "string", "default": "test_value"}},
"nodes": {"node1": {"type": "task", "config": {"task": "test_task"}}},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
context.original_definition = copy.deepcopy(definition)
@given("I have a graph template without name")
def step_graph_template_without_name(context):
"""Setup graph template without name."""
definition = {
"parameters": {"test_param": {"type": "string", "default": "test_value"}},
"nodes": {"node1": {"type": "task", "config": {"task": "test_task"}}},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with nodes")
def step_graph_template_with_nodes(context):
"""Setup graph template with various node types."""
definition = {
"name": "test_graph",
"nodes": {
"task_node": {"type": "task", "config": {"task": "test_task"}},
"agent_node": {"type": "agent", "agent": "test_agent"},
"regular_node": {"type": "regular", "config": {"value": "test"}},
},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with None nodes")
def step_graph_template_with_none_nodes(context):
"""Setup graph template with None nodes for conditional exclusion."""
definition = {
"name": "test_graph",
"nodes": {
"valid_node": {"type": "task", "config": {"task": "test_task"}},
"conditional_node": None,
"another_valid_node": {"type": "task", "config": {"task": "test_task2"}},
},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with agent nodes using parameter references")
def step_graph_template_with_agent_nodes_parameter_references(context):
"""Setup graph template with agent nodes using parameter references."""
definition = {
"name": "test_graph",
"parameters": {"agent_ref": {"type": "string", "default": "resolved_agent"}},
"nodes": {"agent_node": {"type": "agent", "agent": "agent_ref"}},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with agent nodes using component references")
def step_graph_template_with_agent_nodes_component_references(context):
"""Setup graph template with agent nodes using component references."""
definition = {
"name": "test_graph",
"nodes": {
"agent_node": {"type": "agent", "agent": "local_agent"},
"other_node": {"type": "task", "config": {"task": "test"}},
},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with agent nodes using invalid references")
def step_graph_template_with_agent_nodes_invalid_references(context):
"""Setup graph template with agent nodes using invalid references."""
definition = {
"name": "test_graph",
"nodes": {"agent_node": {"type": "agent", "agent": "invalid_agent_ref"}},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with agent nodes using template variables")
def step_graph_template_with_agent_nodes_template_variables(context):
"""Setup graph template with agent nodes using template variables."""
definition = {
"name": "test_graph",
"nodes": {"agent_node": {"type": "agent", "agent": "{{agent_name}}"}},
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with edges")
def step_graph_template_with_edges(context):
"""Setup graph template with various edge types."""
definition = {
"name": "test_graph",
"nodes": {
"node1": {"type": "task", "config": {"task": "test_task1"}},
"node2": {"type": "task", "config": {"task": "test_task2"}},
},
"edges": [
{"from": "node1", "to": "node2"},
{"from": "node2", "to": "node1", "condition": {"type": "test"}},
],
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with None edges")
def step_graph_template_with_none_edges(context):
"""Setup graph template with None edges for conditional exclusion."""
definition = {
"name": "test_graph",
"nodes": {
"node1": {"type": "task", "config": {"task": "test_task1"}},
"node2": {"type": "task", "config": {"task": "test_task2"}},
},
"edges": [
{"from": "node1", "to": "node2"},
None,
{"from": "node2", "to": "node1"},
],
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with edges containing conditions")
def step_graph_template_with_edges_containing_conditions(context):
"""Setup graph template with edges containing conditions with template variables."""
definition = {
"name": "test_graph",
"nodes": {
"node1": {"type": "task", "config": {"task": "test_task1"}},
"node2": {"type": "task", "config": {"task": "test_task2"}},
},
"edges": [
{
"from": "node1",
"to": "node2",
"condition": {
"type": "{{condition_type}}",
"value": "{{condition_value}}",
},
}
],
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a graph template with edges without conditions")
def step_graph_template_with_edges_without_conditions(context):
"""Setup graph template with edges without conditions."""
definition = {
"name": "test_graph",
"nodes": {
"node1": {"type": "task", "config": {"task": "test_task1"}},
"node2": {"type": "task", "config": {"task": "test_task2"}},
},
"edges": [{"from": "node1", "to": "node2"}, {"from": "node2", "to": "node1"}],
}
context.graph_template = GraphTemplate("test_graph", TemplateType.GRAPH, definition)
@given("I have a complete graph template with nodes and edges")
def step_complete_graph_template_with_nodes_and_edges(context):
"""Setup complete graph template with nodes and edges."""
definition = {
"name": "complete_graph",
"parameters": {
"param1": {"type": "string", "default": "value1"},
"param2": {"type": "string", "default": "value2"},
},
"nodes": {
"start_node": {"type": "task", "config": {"task": "{{param1}}"}},
"agent_node": {"type": "agent", "agent": "test_agent"},
"end_node": {"type": "task", "config": {"task": "{{param2}}"}},
},
"edges": [
{"from": "start_node", "to": "agent_node"},
{"from": "agent_node", "to": "end_node", "condition": {"type": "success"}},
],
}
context.graph_template = GraphTemplate(
"complete_graph", TemplateType.GRAPH, definition
)
@given("I have an empty graph template")
def step_empty_graph_template(context):
"""Setup empty graph template."""
definition = {"name": "empty_graph"}
context.graph_template = GraphTemplate(
"empty_graph", TemplateType.GRAPH, definition
)
@given("I have template parameters for graph templates")
def step_template_parameters_for_graph_templates(context):
"""Setup template parameters for graph templates."""
context.params = {"test_param": "test_value", "optional_param": "optional_value"}
@given("I have valid template parameters for graph templates")
def step_valid_template_parameters_for_graph_templates(context):
"""Setup valid template parameters for graph templates."""
context.params = {
"required_param": "required_value",
"optional_param": "optional_value",
}
@given("I have template parameters with variable values for graph templates")
def step_template_parameters_with_variable_values_for_graph_templates(context):
"""Setup template parameters with variable values for graph templates."""
context.params = {
"graph_name": "dynamic_graph",
"node_name": "dynamic_node",
"task_name": "dynamic_task",
"source_node": "start",
"target_node": "end",
"condition_type": "equals",
"condition_value": "success",
}
@given("I have template parameters with agent references")
def step_template_parameters_with_agent_references(context):
"""Setup template parameters with agent references."""
context.params = {"agent_ref": "resolved_agent_name"}
@given("I have an instantiation context for graph templates")
def step_instantiation_context_for_graph_templates(context):
"""Setup instantiation context for graph templates."""
context.instantiation_context = Mock(spec=InstantiationContext)
context.instantiation_context.resolve_reference.return_value = None
@given("I have an instantiation context with registered agents for graph templates")
def step_instantiation_context_with_registered_agents_for_graph_templates(context):
"""Setup instantiation context with registered agents for graph templates."""
context.instantiation_context = Mock(spec=InstantiationContext)
agent_config = {"type": "llm", "config": {"model": "test-model"}}
context.instantiation_context.resolve_reference.return_value = agent_config
@when("I instantiate the graph template")
def step_instantiate_graph_template(context):
"""Instantiate the graph template."""
with patch("cleveractors.templates.graph_templates.logger") as mock_logger:
context.mock_logger = mock_logger
# Mock the validate_params method to return the input params
with patch.object(
context.graph_template, "validate_params", return_value=context.params
):
# Mock the _apply_template_vars method to return input unchanged
with patch.object(
context.graph_template,
"_apply_template_vars",
side_effect=lambda x, y: x,
):
context.result = context.graph_template.instantiate(
context.params, context.registry, context.instantiation_context
)
@then("the graph configuration should be returned")
def step_graph_configuration_should_be_returned(context):
"""Verify graph configuration is returned."""
assert context.result is not None
assert isinstance(context.result, dict)
@then("the parameters section should be removed from graph config")
def step_parameters_section_should_be_removed_from_graph_config(context):
"""Verify parameters section is removed from graph config."""
assert "parameters" not in context.result
@then("template variables should be applied to the graph config")
def step_template_variables_should_be_applied_to_graph_config(context):
"""Verify template variables are applied to the graph config."""
# This would be tested by checking if template variables are replaced
# The actual implementation depends on the _apply_template_vars method
assert context.result is not None
@then("the result should have correct graph structure")
def step_result_should_have_correct_graph_structure(context):
"""Verify the result has correct graph structure."""
assert "name" in context.result
assert context.result["name"] == "test_graph"
@then("parameter validation should be called for graph templates")
def step_parameter_validation_should_be_called_for_graph_templates(context):
"""Verify parameter validation is called for graph templates."""
# This is tested implicitly as validate_params is called in instantiate
assert context.result is not None
@then("the filled parameters should be used for graph templates")
def step_filled_parameters_should_be_used_for_graph_templates(context):
"""Verify filled parameters are used for graph templates."""
# This is tested by checking that the instantiation succeeded
assert context.result is not None
@then("template variables should be replaced with parameter values in graph config")
def step_template_variables_should_be_replaced_with_parameter_values_in_graph_config(
context,
):
"""Verify template variables are replaced with parameter values in graph config."""
# Check that template variables have been replaced
assert context.result is not None
# The actual verification would depend on the specific template variables used
@then("the original definition should not be modified for graph templates")
def step_original_definition_should_not_be_modified_for_graph_templates(context):
"""Verify the original definition is not modified for graph templates."""
# Compare with the stored original definition
assert context.graph_template.definition == context.original_definition
@then("the returned config should be a separate copy for graph templates")
def step_returned_config_should_be_separate_copy_for_graph_templates(context):
"""Verify the returned config is a separate copy for graph templates."""
assert context.result is not context.graph_template.definition
# Modify the result and ensure original is unchanged
if "test_modification" not in context.result:
context.result["test_modification"] = "test"
assert "test_modification" not in context.graph_template.definition
@then("the graph name should be assigned from template name")
def step_graph_name_should_be_assigned_from_template_name(context):
"""Verify the graph name is assigned from template name."""
assert context.result["name"] == "test_graph"
@then("nodes should be processed correctly")
def step_nodes_should_be_processed_correctly(context):
"""Verify nodes are processed correctly."""
assert "nodes" in context.result
assert len(context.result["nodes"]) > 0
@then("non-agent nodes should pass through unchanged")
def step_non_agent_nodes_should_pass_through_unchanged(context):
"""Verify non-agent nodes pass through unchanged."""
nodes = context.result["nodes"]
assert "task_node" in nodes
assert nodes["task_node"]["type"] == "task"
assert "regular_node" in nodes
assert nodes["regular_node"]["type"] == "regular"
@then("None nodes should be filtered out")
def step_none_nodes_should_be_filtered_out(context):
"""Verify None nodes are filtered out."""
nodes = context.result["nodes"]
assert "conditional_node" not in nodes
assert "valid_node" in nodes
assert "another_valid_node" in nodes
@then("agent parameter references should be resolved")
def step_agent_parameter_references_should_be_resolved(context):
"""Verify agent parameter references are resolved."""
nodes = context.result["nodes"]
agent_node = nodes["agent_node"]
assert agent_node["agent"] == "resolved_agent_name"
@then("agent nodes should use resolved parameter values")
def step_agent_nodes_should_use_resolved_parameter_values(context):
"""Verify agent nodes use resolved parameter values."""
nodes = context.result["nodes"]
agent_node = nodes["agent_node"]
assert agent_node["agent"] == "resolved_agent_name"
@then("agent component references should be resolved")
def step_agent_component_references_should_be_resolved(context):
"""Verify agent component references are resolved."""
# Verify that resolve_reference was called
context.instantiation_context.resolve_reference.assert_called()
@then("agent_config should be stored for resolved agents")
def step_agent_config_should_be_stored_for_resolved_agents(context):
"""Verify agent_config is stored for resolved agents."""
nodes = context.result["nodes"]
agent_node = nodes["agent_node"]
assert "agent_config" in agent_node
@then("debug logging should be called for resolved references")
def step_debug_logging_should_be_called_for_resolved_references(context):
"""Verify debug logging is called for resolved references."""
context.mock_logger.debug.assert_called()
@then("invalid agent references should be handled gracefully")
def step_invalid_agent_references_should_be_handled_gracefully(context):
"""Verify invalid agent references are handled gracefully."""
# The test should not fail and should return a valid result
assert context.result is not None
assert "nodes" in context.result
@then("agent nodes should retain original reference")
def step_agent_nodes_should_retain_original_reference(context):
"""Verify agent nodes retain original reference when resolution fails."""
nodes = context.result["nodes"]
agent_node = nodes["agent_node"]
assert agent_node["agent"] == "invalid_agent_ref"
@then("template variable agent references should be preserved")
def step_template_variable_agent_references_should_be_preserved(context):
"""Verify template variable agent references are preserved."""
# Template variables starting with {{ should not be resolved as component references
assert context.result is not None
@then("template variables should not be resolved as components")
def step_template_variables_should_not_be_resolved_as_components(context):
"""Verify template variables are not resolved as components."""
# The resolve_reference should not be called for template variables
if context.instantiation_context.resolve_reference.called:
# Check that it wasn't called with a template variable
call_args = context.instantiation_context.resolve_reference.call_args
if call_args:
component_ref = call_args[0][0]
assert not component_ref.ref_name.startswith("{{")
@then("edges should be processed correctly")
def step_edges_should_be_processed_correctly(context):
"""Verify edges are processed correctly."""
assert "edges" in context.result
assert len(context.result["edges"]) > 0
@then("all valid edges should be included")
def step_all_valid_edges_should_be_included(context):
"""Verify all valid edges are included."""
edges = context.result["edges"]
assert len(edges) == 2 # Based on the test setup
@then("None edges should be filtered out")
def step_none_edges_should_be_filtered_out(context):
"""Verify None edges are filtered out."""
edges = context.result["edges"]
# Should have 2 valid edges, None edge should be filtered out
assert len(edges) == 2
assert all(edge is not None for edge in edges)
@then("edge conditions should be processed with template variables")
def step_edge_conditions_should_be_processed_with_template_variables(context):
"""Verify edge conditions are processed with template variables."""
edges = context.result["edges"]
# Find the edge with condition
edge_with_condition = next((edge for edge in edges if "condition" in edge), None)
assert edge_with_condition is not None
assert "condition" in edge_with_condition
@then("template variables in conditions should be replaced")
def step_template_variables_in_conditions_should_be_replaced(context):
"""Verify template variables in conditions are replaced."""
edges = context.result["edges"]
edge_with_condition = next((edge for edge in edges if "condition" in edge), None)
assert edge_with_condition is not None
# The specific replacement would depend on the _apply_template_vars implementation
@then("edges without conditions should pass through unchanged")
def step_edges_without_conditions_should_pass_through_unchanged(context):
"""Verify edges without conditions pass through unchanged."""
edges = context.result["edges"]
assert len(edges) == 2
for edge in edges:
assert "from" in edge
assert "to" in edge
@then("the complete graph should be properly instantiated")
def step_complete_graph_should_be_properly_instantiated(context):
"""Verify the complete graph is properly instantiated."""
assert context.result is not None
assert "name" in context.result
assert "nodes" in context.result
assert "edges" in context.result
@then("all nodes should be processed")
def step_all_nodes_should_be_processed(context):
"""Verify all nodes are processed."""
nodes = context.result["nodes"]
assert len(nodes) == 3 # start_node, agent_node, end_node
@then("all edges should be processed")
def step_all_edges_should_be_processed(context):
"""Verify all edges are processed."""
edges = context.result["edges"]
assert len(edges) == 2
@then("the graph should have proper structure")
def step_graph_should_have_proper_structure(context):
"""Verify the graph has proper structure."""
assert "name" in context.result
assert context.result["name"] == "complete_graph"
@then("the empty graph should be handled correctly")
def step_empty_graph_should_be_handled_correctly(context):
"""Verify the empty graph is handled correctly."""
assert context.result is not None
assert "name" in context.result
@then("basic graph structure should be maintained")
def step_basic_graph_structure_should_be_maintained(context):
"""Verify basic graph structure is maintained."""
assert context.result["name"] == "empty_graph"