Files
cleveractors-core/features/steps/stream_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

748 lines
26 KiB
Python

"""
Step definitions for Stream Template Operator Processing BDD tests.
"""
import copy
from typing import Any, Dict
from behave import given, then, when
from cleveractors.templates.base import (
BaseTemplate,
ComponentReference,
InstantiationContext,
TemplateType,
)
from cleveractors.templates.stream_templates import StreamTemplate
# Mock implementations for testing
class MockTemplateRegistry:
"""Mock template registry for testing."""
def __init__(self):
self.templates = {}
self.calls = []
def get_template(self, template_type: TemplateType, name: str) -> BaseTemplate:
self.calls.append(("get_template", template_type, name))
return self.templates.get(f"{template_type.value}:{name}")
def instantiate(
self,
template_type: TemplateType,
name: str,
params: Dict[str, Any],
context: InstantiationContext = None,
) -> Any:
self.calls.append(("instantiate", template_type, name, params, context))
return {"instantiated": f"{template_type.value}:{name}"}
class MockInstantiationContext(InstantiationContext):
"""Mock instantiation context for testing."""
def __init__(self, resolves_successfully=True, raise_exception=False):
super().__init__()
self.resolves_successfully = resolves_successfully
self.raise_exception = raise_exception
self.resolution_calls = []
def resolve_reference(self, ref: ComponentReference) -> Any:
self.resolution_calls.append(ref)
if self.raise_exception:
raise Exception("Mock resolution exception")
if self.resolves_successfully:
return {"resolved": f"{ref.ref_type}:{ref.ref_name}"}
else:
return None
@given("I have a clean test environment for stream templates")
def step_clean_environment_stream_templates(context):
"""Set up clean test environment for stream templates."""
context.stream_template = None
context.template_registry = None
context.instantiation_context = None
context.template_params = {}
context.operators = []
context.result = None
context.error = None
context.processed_operators = None
@given("I have a stream template with basic definition")
def step_stream_template_basic_definition(context):
"""Create stream template with basic definition."""
definition = {
"type": "stream",
"stream_type": "cold",
"operators": [
{"type": "map", "params": {"function": "transform"}},
{"type": "filter", "params": {"condition": "valid"}},
],
"publications": ["output_stream"],
}
context.stream_template = StreamTemplate(
name="test_stream", template_type=TemplateType.STREAM, definition=definition
)
@given("I have a stream template with parameters in definition")
def step_stream_template_with_parameters(context):
"""Create stream template with parameters section."""
definition = {
"parameters": {
"agent_name": {"type": "string", "required": True},
"filter_condition": {"type": "string", "default": "default_condition"},
},
"type": "stream",
"stream_type": "cold",
"operators": [{"type": "map", "params": {"agent": "{{ agent_name }}"}}],
}
context.stream_template = StreamTemplate(
name="parameterized_stream",
template_type=TemplateType.STREAM,
definition=definition,
)
@given("I have a stream template with operators in definition")
def step_stream_template_with_operators(context):
"""Create stream template with operators."""
definition = {
"type": "stream",
"operators": [
{"type": "map", "params": {"agent": "test_agent"}},
{"type": "filter", "params": {"condition": "test"}},
],
}
context.stream_template = StreamTemplate(
name="operators_stream",
template_type=TemplateType.STREAM,
definition=definition,
)
@given("I have a stream template without name in definition")
def step_stream_template_without_name(context):
"""Create stream template without name in definition."""
definition = {
"type": "stream",
"stream_type": "hot",
"operators": [{"type": "buffer", "params": {"count": 5}}],
}
context.stream_template = StreamTemplate(
name="unnamed_stream", template_type=TemplateType.STREAM, definition=definition
)
@given("I have a stream template with name in definition")
def step_stream_template_with_name(context):
"""Create stream template with name in definition."""
definition = {
"name": "predefined_name",
"type": "stream",
"stream_type": "cold",
"operators": [{"type": "debounce", "params": {"duration": 1.0}}],
}
context.stream_template = StreamTemplate(
name="named_stream", template_type=TemplateType.STREAM, definition=definition
)
@given("I have valid template parameters")
def step_valid_template_parameters(context):
"""Create valid template parameters."""
context.template_params = {
"agent_name": "test_agent",
"filter_condition": "active",
"timeout": 30,
"buffer_size": 10,
}
@given("I have template parameters requiring validation")
def step_template_parameters_requiring_validation(context):
"""Create parameters that require validation."""
context.template_params = {
"agent_name": "validated_agent",
"required_param": "required_value",
"optional_param": None, # Should use default
}
@given("I have a template registry for stream templates")
def step_template_registry_stream_templates(context):
"""Create mock template registry for stream templates."""
context.template_registry = MockTemplateRegistry()
@given("I have an instantiation context")
def step_instantiation_context(context):
"""Create mock instantiation context."""
context.instantiation_context = MockInstantiationContext()
@given("I have an instantiation context with resolvable agent reference")
def step_instantiation_context_resolvable_agent(context):
"""Create context that can resolve agent references."""
context.instantiation_context = MockInstantiationContext(resolves_successfully=True)
@given("I have an instantiation context with unresolvable agent reference")
def step_instantiation_context_unresolvable_agent(context):
"""Create context that cannot resolve agent references."""
context.instantiation_context = MockInstantiationContext(
resolves_successfully=False
)
@given("I have an instantiation context with resolvable graph reference")
def step_instantiation_context_resolvable_graph(context):
"""Create context that can resolve graph references."""
context.instantiation_context = MockInstantiationContext(resolves_successfully=True)
@given("I have an instantiation context with unresolvable graph reference")
def step_instantiation_context_unresolvable_graph(context):
"""Create context that cannot resolve graph references."""
context.instantiation_context = MockInstantiationContext(
resolves_successfully=False
)
@given("I have an instantiation context with mixed references")
def step_instantiation_context_mixed_references(context):
"""Create context with mixed reference resolution."""
context.instantiation_context = MockInstantiationContext(resolves_successfully=True)
@given("I have an instantiation context with resolvable references")
def step_instantiation_context_resolvable_references(context):
"""Create context with resolvable references."""
context.instantiation_context = MockInstantiationContext(resolves_successfully=True)
@given("I have an instantiation context that throws exception during resolution")
def step_instantiation_context_exception(context):
"""Create context that throws exception during resolution."""
context.instantiation_context = MockInstantiationContext(
resolves_successfully=False, raise_exception=True
)
@given("I have a stream template instance")
def step_stream_template_instance(context):
"""Create basic stream template instance."""
definition = {"type": "stream", "operators": []}
context.stream_template = StreamTemplate(
name="test_stream", template_type=TemplateType.STREAM, definition=definition
)
@given("I have operators list with None operator")
def step_operators_with_none(context):
"""Create operators list containing None."""
context.operators = [
{"type": "map", "params": {"agent": "test_agent"}},
None, # Conditionally excluded operator
{"type": "filter", "params": {"condition": "active"}},
]
@given("I have operators list with parameterized type")
def step_operators_parameterized_type(context):
"""Create operators with parameterized type."""
context.operators = [
{"type": "operator_type_param", "params": {"value": "test"}},
{"type": "map", "params": {"agent": "test_agent"}},
]
@given("I have template parameters with operator type")
def step_template_params_operator_type(context):
"""Create parameters with operator type."""
context.template_params = {
"operator_type_param": "debounce",
"agent_name": "test_agent",
}
@given("I have operators list with processor graph_execute reference")
def step_operators_processor_graph_execute(context):
"""Create operators with processor graph_execute reference."""
context.operators = [
{
"type": "process",
"params": {
"processor": {
"type": "graph_execute",
"params": {"graph": "test_graph", "input_key": "data"},
}
},
}
]
@given("I have operators list with processor agent reference")
def step_operators_processor_agent(context):
"""Create operators with processor agent reference."""
context.operators = [
{
"type": "process",
"params": {
"processor": {
"type": "agent",
"params": {"name": "test_agent"},
}
},
}
]
@given("I have operators list with map operator having agent parameter reference")
def step_operators_map_agent_param_ref(context):
"""Create map operator with agent parameter reference."""
context.operators = [
{"type": "map", "params": {"agent": "agent_param"}},
]
@given("I have template parameters with agent name")
def step_template_params_agent_name(context):
"""Create parameters with agent name."""
context.template_params = {
"agent_param": "resolved_agent",
"other_param": "other_value",
}
@given("I have operators list with map operator having agent component reference")
def step_operators_map_agent_component_ref(context):
"""Create map operator with agent component reference."""
context.operators = [
{"type": "map", "params": {"agent": "component_agent_ref"}},
]
@given(
"I have operators list with graph_execute operator having graph parameter reference"
)
def step_operators_graph_execute_graph_param_ref(context):
"""Create graph_execute operator with graph parameter reference."""
context.operators = [
{"type": "graph_execute", "params": {"graph": "graph_param"}},
]
@given("I have template parameters with graph name")
def step_template_params_graph_name(context):
"""Create parameters with graph name."""
context.template_params = {
"graph_param": "resolved_graph",
"other_param": "other_value",
}
@given(
"I have operators list with graph_execute operator having graph component reference"
)
def step_operators_graph_execute_graph_component_ref(context):
"""Create graph_execute operator with graph component reference."""
context.operators = [
{"type": "graph_execute", "params": {"graph": "component_graph_ref"}},
]
@given("I have operators list with agent component reference")
def step_operators_with_agent_component_reference(context):
"""Create operators list with agent component reference."""
context.operators = [
{"type": "map", "params": {"agent": "component_agent_ref"}},
]
@given("I have operators list with mixed operator types")
def step_operators_mixed_types(context):
"""Create operators with mixed types."""
context.operators = [
{"type": "map", "params": {"agent": "agent_param"}}, # Parameter reference
{
"type": "graph_execute",
"params": {"graph": "component_graph_ref"},
}, # Component ref
{
"type": "process",
"params": {
"processor": {"type": "agent", "params": {"name": "proc_agent"}}
},
}, # Processor agent
None, # Conditionally excluded
{"type": "filter", "params": {"condition": "active"}}, # Regular operator
]
@given("I have comprehensive template parameters")
def step_comprehensive_template_params(context):
"""Create comprehensive template parameters."""
context.template_params = {
"agent_param": "resolved_agent_from_param",
"other_param": "other_value",
"timeout": 30,
}
@given("I have empty operators list")
def step_empty_operators_list(context):
"""Create empty operators list."""
context.operators = []
@given("I have template parameters")
def step_basic_template_parameters(context):
"""Create basic template parameters."""
context.template_params = {
"param1": "value1",
"param2": "value2",
}
@given("I have operators list with resolvable references")
def step_operators_resolvable_references(context):
"""Create operators with resolvable references."""
context.operators = [
{"type": "map", "params": {"agent": "resolvable_agent"}},
{"type": "graph_execute", "params": {"graph": "resolvable_graph"}},
]
@given("I have a stream template with parameter definitions")
def step_stream_template_with_param_definitions(context):
"""Create stream template with parameter definitions."""
definition = {
"parameters": {
"required_param": {"type": "string", "required": True},
"optional_param": {"type": "string", "default": "default_value"},
},
"type": "stream",
"operators": [{"type": "map", "params": {"agent": "{{ required_param }}"}}],
}
context.stream_template = StreamTemplate(
name="param_def_stream",
template_type=TemplateType.STREAM,
definition=definition,
)
@given("I have a stream template with mutable definition")
def step_stream_template_mutable_definition(context):
"""Create stream template with mutable definition for deep copy testing."""
definition = {
"type": "stream",
"operators": [{"type": "map", "params": {"mutable_list": [1, 2, 3]}}],
"nested_dict": {"key": {"subkey": "value"}},
}
context.stream_template = StreamTemplate(
name="mutable_stream", template_type=TemplateType.STREAM, definition=definition
)
# Store original for comparison
context.original_definition = copy.deepcopy(definition)
@when("I instantiate the stream template")
def step_instantiate_stream_template(context):
"""Instantiate the stream template."""
try:
context.result = context.stream_template.instantiate(
context.template_params,
context.template_registry,
context.instantiation_context,
)
except Exception as e:
context.error = e
@when("I process the operators")
def step_process_operators(context):
"""Process the operators using _process_operators method."""
try:
context.processed_operators = context.stream_template._process_operators(
context.operators, context.template_params, context.instantiation_context
)
except Exception as e:
context.error = e
@then("the stream definition should be created correctly")
def step_verify_stream_definition_created(context):
"""Verify stream definition was created correctly."""
assert context.error is None, f"Unexpected error: {context.error}"
assert context.result is not None
assert isinstance(context.result, dict)
@then("the parameters section should be removed")
def step_verify_parameters_section_removed(context):
"""Verify parameters section was removed."""
assert "parameters" not in context.result
@then("the parameters section should be removed from definition")
def step_verify_parameters_section_removed_from_definition(context):
"""Verify parameters section was removed from definition."""
assert "parameters" not in context.result
@then("template variables should be applied")
def step_verify_template_variables_applied(context):
"""Verify template variables were applied."""
# This verifies that _apply_template_vars was called
# The actual template variable processing is tested elsewhere
assert context.result is not None
@then("operators should be processed for references")
def step_verify_operators_processed(context):
"""Verify operators were processed for references."""
if "operators" in context.result:
assert isinstance(context.result["operators"], list)
@then("the template name should be added to definition")
def step_verify_template_name_added(context):
"""Verify template name was added."""
assert context.result["name"] == context.stream_template.name
@then("the existing name should be preserved")
def step_verify_existing_name_preserved(context):
"""Verify existing name was preserved."""
assert context.result["name"] == "predefined_name"
@then("the None operator should be skipped")
def step_verify_none_operator_skipped(context):
"""Verify None operator was skipped."""
assert context.error is None
assert context.processed_operators is not None
assert None not in context.processed_operators
@then("processed operators should not contain None")
def step_verify_processed_operators_no_none(context):
"""Verify processed operators don't contain None."""
assert None not in context.processed_operators
@then("the operator type should be replaced with parameter value")
def step_verify_operator_type_replaced(context):
"""Verify operator type was replaced with parameter value."""
assert context.processed_operators is not None
# Find the operator that had parameterized type
found_replaced = False
for op in context.processed_operators:
if op.get("type") == "debounce": # This should be the replaced value
found_replaced = True
break
assert found_replaced, "Operator type was not replaced with parameter value"
@then("the operator should be converted to graph_execute type")
def step_verify_operator_converted_graph_execute(context):
"""Verify operator was converted to graph_execute type."""
assert context.processed_operators is not None
assert len(context.processed_operators) > 0
op = context.processed_operators[0]
assert op["type"] == "graph_execute"
@then("the processor params should be applied")
def step_verify_processor_params_applied(context):
"""Verify processor params were applied."""
op = context.processed_operators[0]
assert "params" in op
assert "graph" in op["params"]
assert op["params"]["graph"] == "test_graph"
@then("the operator should be converted to map type")
def step_verify_operator_converted_map(context):
"""Verify operator was converted to map type."""
assert context.processed_operators is not None
assert len(context.processed_operators) > 0
op = context.processed_operators[0]
assert op["type"] == "map"
@then("the agent name should be applied")
def step_verify_agent_name_applied(context):
"""Verify agent name was applied."""
op = context.processed_operators[0]
assert "params" in op
assert "agent" in op["params"]
assert op["params"]["agent"] == "test_agent"
@then("the agent reference should be replaced with parameter value")
def step_verify_agent_reference_replaced(context):
"""Verify agent reference was replaced with parameter value."""
assert context.processed_operators is not None
op = context.processed_operators[0]
assert op["params"]["agent"] == "resolved_agent"
@then("the agent reference should be resolved successfully")
def step_verify_agent_reference_resolved(context):
"""Verify agent reference was resolved successfully."""
assert context.processed_operators is not None
op = context.processed_operators[0]
assert "agent_config" in op["params"]
assert op["params"]["agent_config"]["resolved"] == "agent:component_agent_ref"
@then("agent_config should be added to operator params")
def step_verify_agent_config_added(context):
"""Verify agent_config was added."""
op = context.processed_operators[0]
assert "agent_config" in op["params"]
@then("the agent reference resolution should fail silently")
def step_verify_agent_reference_resolution_failed(context):
"""Verify agent reference resolution failed silently."""
assert context.error is None # Should not raise exception
assert context.processed_operators is not None
@then("the original agent reference should be preserved")
def step_verify_original_agent_reference_preserved(context):
"""Verify original agent reference was preserved."""
op = context.processed_operators[0]
assert op["params"]["agent"] == "component_agent_ref" # Original value
assert "agent_config" not in op["params"] # No resolved config
@then("the graph reference should be replaced with parameter value")
def step_verify_graph_reference_replaced(context):
"""Verify graph reference was replaced with parameter value."""
assert context.processed_operators is not None
op = context.processed_operators[0]
assert op["params"]["graph"] == "resolved_graph"
@then("the graph reference should be resolved successfully")
def step_verify_graph_reference_resolved(context):
"""Verify graph reference was resolved successfully."""
assert context.processed_operators is not None
op = context.processed_operators[0]
assert "graph_config" in op["params"]
assert op["params"]["graph_config"]["resolved"] == "graph:component_graph_ref"
@then("graph_config should be added to operator params")
def step_verify_graph_config_added(context):
"""Verify graph_config was added."""
op = context.processed_operators[0]
assert "graph_config" in op["params"]
@then("the graph reference resolution should fail silently")
def step_verify_graph_reference_resolution_failed(context):
"""Verify graph reference resolution failed silently."""
assert context.error is None # Should not raise exception
assert context.processed_operators is not None
@then("the original graph reference should be preserved")
def step_verify_original_graph_reference_preserved(context):
"""Verify original graph reference was preserved."""
op = context.processed_operators[0]
assert op["params"]["graph"] == "component_graph_ref" # Original value
assert "graph_config" not in op["params"] # No resolved config
@then("all operators should be processed correctly")
def step_verify_all_operators_processed(context):
"""Verify all operators were processed correctly."""
assert context.processed_operators is not None
# Should process all non-None operators
expected_count = 4 # map, graph_execute, process, filter (None is skipped)
assert len(context.processed_operators) == expected_count
@then("references should be resolved appropriately")
def step_verify_references_resolved_appropriately(context):
"""Verify references were resolved appropriately."""
# Check that agent parameter reference was resolved
map_op = next(op for op in context.processed_operators if op.get("type") == "map")
assert map_op["params"]["agent"] == "resolved_agent_from_param"
@then("processed operators should be returned")
def step_verify_processed_operators_returned(context):
"""Verify processed operators were returned."""
assert context.processed_operators is not None
assert isinstance(context.processed_operators, list)
@then("an empty processed operators list should be returned")
def step_verify_empty_processed_operators_returned(context):
"""Verify empty processed operators list was returned."""
assert context.processed_operators is not None
assert context.processed_operators == []
@then("debug logging should be triggered for successful resolutions")
def step_verify_debug_logging_triggered(context):
"""Verify debug logging was triggered."""
# This tests that logger.debug is called, which happens when references are resolved
assert len(context.instantiation_context.resolution_calls) > 0
@then("parameters should be validated correctly")
def step_verify_parameters_validated(context):
"""Verify parameters were validated correctly."""
# validate_params was called, which is part of the instantiate method
assert context.result is not None
@then("the stream definition should use validated parameters")
def step_verify_stream_uses_validated_params(context):
"""Verify stream definition uses validated parameters."""
assert context.result is not None
@then("the original definition should not be modified")
def step_verify_original_definition_not_modified(context):
"""Verify original definition was not modified."""
# Check that the template's definition is still the same as original
assert context.stream_template.definition == context.original_definition
@then("the returned definition should be a deep copy")
def step_verify_returned_definition_deep_copy(context):
"""Verify returned definition is a deep copy."""
# Modify the returned result and ensure original is unchanged
if "operators" in context.result:
original_ops = context.stream_template.definition["operators"]
context.result["operators"][0]["params"]["mutable_list"].append(999)
# Original should be unchanged
assert 999 not in original_ops[0]["params"]["mutable_list"]
@then("the exception should be caught and handled silently")
def step_verify_exception_caught_silently(context):
"""Verify exception was caught and handled silently."""
assert context.error is None # Should not propagate the exception
assert context.processed_operators is not None
@then("processing should continue for remaining operators")
def step_verify_processing_continues(context):
"""Verify processing continued for remaining operators."""
# Should still process other operators even if one fails
assert len(context.processed_operators) > 0