Files
temp/features/steps/reactive_route_coverage_boost_steps.py
freemo a808c395f9 test(coverage): add Behave BDD tests to improve unit test coverage across 53 source modules
Add 53 new .feature files and corresponding step definition files targeting
uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts
in 7 pre-existing step files by disambiguating step text.

New tests cover: ACP clients/facade, actor CLI/config, application container,
ACMS service/strategies, async worker, automation profile CLI, autonomy
guardrail, bridge, change model, config CLI/service, context service,
cross-plan correction, database models, decision service, decomposition
clustering/service, discovery handler, langchain chat provider, langgraph
nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/
preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI,
provider registry, reactive application/route, repositories, resolver handler,
resource registry service, resume model, retry patterns, sandbox protocol,
server CLI, skill CLI/service, skills registry, subplan execution/service,
system CLI, UKO loader, UoW, and YAML template engine.

Closes #645
2026-03-09 13:01:58 -04:00

222 lines
7.7 KiB
Python

"""Step definitions targeting uncovered lines in reactive/route.py.
Covers:
- lines 90-99: to_stream_config() success path
- line 113: MESSAGE_ROUTER rules propagation in to_graph_config()
- line 177: from_graph_config() fallback when node type has no .value
"""
from __future__ import annotations
from behave import given, then, when
from cleveragents.langgraph.graph import GraphConfig
from cleveragents.langgraph.nodes import NodeConfig, NodeType
from cleveragents.reactive.route import RouteConfig, RouteType
from cleveragents.reactive.stream_router import StreamType
@given("the reactive route coverage module is loaded")
def step_module_loaded(context):
"""Ensure the route module is importable and set up context."""
context.route_config = None
context.stream_config_result = None
context.graph_config_result = None
context.result_route_config = None
# ── Scenario: Convert a STREAM RouteConfig to StreamConfig successfully ──
@given("I have a stream RouteConfig with full configuration")
def step_full_stream_route_config(context):
context.route_config = RouteConfig(
name="full_stream",
type=RouteType.STREAM,
stream_type=StreamType.HOT,
operators=[
{"type": "map", "params": {"function": "transform"}},
{"type": "filter", "params": {"predicate": "is_valid"}},
],
subscriptions=["input_topic"],
publications=["output_topic"],
agents=["agent_a", "agent_b"],
initial_value="init_val",
buffer_size=16,
template_config={"template": "my_template"},
)
@when("I convert the stream RouteConfig to a StreamConfig")
def step_convert_route_to_stream_config(context):
context.stream_config_result = context.route_config.to_stream_config()
@then("the StreamConfig should have the correct name")
def step_stream_config_name(context):
assert context.stream_config_result.name == context.route_config.name
@then("the StreamConfig should have the correct stream type")
def step_stream_config_type(context):
assert context.stream_config_result.type == context.route_config.stream_type
@then("the StreamConfig should preserve all operator definitions")
def step_stream_config_operators(context):
assert context.stream_config_result.operators == context.route_config.operators
@then("the StreamConfig should preserve subscriptions and publications")
def step_stream_config_subs_pubs(context):
assert (
context.stream_config_result.subscriptions == context.route_config.subscriptions
)
assert (
context.stream_config_result.publications == context.route_config.publications
)
@then("the StreamConfig should preserve agents and buffer size")
def step_stream_config_agents_buffer(context):
assert context.stream_config_result.agents == context.route_config.agents
assert context.stream_config_result.buffer_size == context.route_config.buffer_size
@then("the StreamConfig should preserve initial value and template config")
def step_stream_config_initial_template(context):
assert (
context.stream_config_result.initial_value == context.route_config.initial_value
)
assert (
context.stream_config_result.template_config
== context.route_config.template_config
)
# ── Scenario: Convert a STREAM RouteConfig with defaults to StreamConfig ──
@given("I have a minimal stream RouteConfig with no explicit stream type")
def step_minimal_stream_route_config(context):
context.route_config = RouteConfig(
name="minimal_stream",
type=RouteType.STREAM,
)
@then("the StreamConfig stream type should be COLD")
def step_stream_config_default_cold(context):
assert context.stream_config_result.type == StreamType.COLD
# ── Scenario: MESSAGE_ROUTER rules propagation ──
@given(
"I have a graph RouteConfig with a MESSAGE_ROUTER node containing top-level rules"
)
def step_graph_route_with_message_router_rules(context):
context.route_config = RouteConfig(
name="router_graph",
type=RouteType.GRAPH,
nodes={
"start_node": {"type": "agent", "agent": "agentA"},
"router_node": {
"type": "message_router",
"rules": [
{"target": "end", "condition": "done"},
{"target": "start_node", "condition": "retry"},
],
"metadata": {"existing_key": "existing_value"},
},
},
edges=[
{"source": "start_node", "target": "router_node"},
{"source": "router_node", "target": "start_node"},
],
)
@when("I convert the graph RouteConfig to a GraphConfig")
def step_convert_route_to_graph_config(context):
context.graph_config_result = context.route_config.to_graph_config()
@then("the resulting MESSAGE_ROUTER node metadata should contain the rules")
def step_router_node_metadata_has_rules(context):
router_node = context.graph_config_result.nodes["router_node"]
assert "rules" in router_node.metadata
assert len(router_node.metadata["rules"]) == 2
assert router_node.metadata["rules"][0]["target"] == "end"
# Verify existing metadata is also preserved
assert router_node.metadata.get("existing_key") == "existing_value"
# ── Scenario: from_graph_config fallback when node type is None ──
@given("I have a GraphConfig with a node whose type attribute is None")
def step_graph_config_node_type_none(context):
# Build a stub node config object where .type is None
class _StubNodeConfig:
def __init__(self):
self.type = None
self.agent = None
self.function = "my_func"
self.tools = []
self.retry_policy = None
self.timeout = None
self.parallel = False
self.metadata = {}
context.source_graph_config = GraphConfig(
name="none_type_graph",
nodes={"stub_node": NodeConfig(name="stub_node", type=NodeType.FUNCTION)},
edges=[],
entry_point="start",
)
# Replace the node value with our stub that has type=None
# GraphConfig.nodes is dict[str, NodeConfig] but from_graph_config uses
# getattr() so it works with any object that has the right attributes.
context.source_graph_config.nodes["stub_node"] = _StubNodeConfig() # type: ignore[assignment]
@when("I create a RouteConfig from that GraphConfig")
def step_route_from_graph_config(context):
context.result_route_config = RouteConfig.from_graph_config(
context.source_graph_config
)
@then('the node type in the resulting RouteConfig should be "{expected_type}"')
def step_node_type_in_route(context, expected_type):
node_dict = context.result_route_config.nodes.get("stub_node")
assert node_dict is not None, "stub_node not found in resulting RouteConfig"
assert node_dict["type"] == expected_type
# ── Scenario: from_graph_config fallback when node type has no .value attribute ──
@given("I have a GraphConfig with a node whose type is a plain string")
def step_graph_config_node_type_plain_string(context):
class _StubNodeConfig:
def __init__(self):
self.type = "custom_type" # plain string, no .value attribute
self.agent = None
self.function = None
self.tools = None
self.retry_policy = None
self.timeout = None
self.parallel = False
self.metadata = None
context.source_graph_config = GraphConfig(
name="string_type_graph",
nodes={"stub_node": NodeConfig(name="stub_node", type=NodeType.FUNCTION)},
edges=[],
entry_point="start",
)
context.source_graph_config.nodes["stub_node"] = _StubNodeConfig() # type: ignore[assignment]