Files
temp/features/steps/langgraph_nodes_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

227 lines
7.8 KiB
Python

"""Step definitions targeting uncovered lines in cleveragents.langgraph.nodes."""
import asyncio
from behave import given, then, when
from cleveragents.langgraph.nodes import Edge, Node, NodeConfig, NodeType, ToolAgent
from cleveragents.langgraph.state import GraphState
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_state(messages=None, metadata=None):
return GraphState(messages=messages or [], metadata=metadata or {})
def _run(coro):
"""Run a coroutine in a fresh event loop."""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
class _RecordingToolAgent(ToolAgent):
"""ToolAgent subclass that records inputs."""
def __init__(self, name: str, response: str):
super().__init__(name)
self.response = response
self.received_inputs: list = []
async def process_message(self, message, context=None):
self.received_inputs.append(message)
return self.response
def get_capabilities(self):
return []
# ---------------------------------------------------------------------------
# Line 130 - Unknown node type -> result = {}
# ---------------------------------------------------------------------------
@given("a node whose type is patched to an unrecognised value")
def step_patch_node_type(context):
config = NodeConfig(name="unknown", type=NodeType.END)
node = Node(config)
# Patch the type to something that won't match any branch
node.type = "UNRECOGNISED"
context.node = node
context.state = _make_state()
@when("I execute the unknown-type node")
def step_execute_unknown_type(context):
context.result = _run(context.node.execute(context.state))
@then("the result should contain current_node and nothing else")
def step_assert_unknown_type_result(context):
assert context.result == {"current_node": "unknown"}
# ---------------------------------------------------------------------------
# Line 176 - ToolAgent with empty current_message -> fallback to last message
# ---------------------------------------------------------------------------
@given("a ToolAgent node with an empty current_message and messages in state")
def step_toolagent_empty_current_msg(context):
tool_agent = _RecordingToolAgent("tool_fb", response="tooled")
config = NodeConfig(name="tool_fb_node", type=NodeType.AGENT, agent="tool_fb")
context.node = Node(config, agents={"tool_fb": tool_agent})
context.state = _make_state(
messages=[
{"role": "user", "content": "hello from user"},
{"role": "assistant", "content": "last_content"},
],
metadata={"current_message": ""}, # falsy string
)
@when("I execute the ToolAgent fallback node")
def step_execute_toolagent_fallback(context):
context.result = _run(context.node.execute(context.state))
@then("the ToolAgent should receive the last message content as input")
def step_assert_toolagent_fallback_input(context):
agent = context.node.agents["tool_fb"]
# current_message is "" (falsy), so it should fall back to last message content
assert agent.received_inputs[-1] == "last_content"
# ---------------------------------------------------------------------------
# Line 270 - _execute_function with non-callable object
# ---------------------------------------------------------------------------
@given("a function node whose registered object is not callable")
def step_non_callable_function(context):
config = NodeConfig(name="fn_nc", type=NodeType.FUNCTION, function="not_a_fn")
# Register a non-callable object under the function name
context.node = Node(config, agents={"not_a_fn": 42})
context.state = _make_state()
@when("I execute the non-callable function node")
def step_execute_non_callable_fn(context):
context.result = _run(context.node.execute(context.state))
@then("the result should report a not-callable error")
def step_assert_non_callable_error(context):
assert context.result.get("failed_node") == "fn_nc"
assert "not callable" in context.result.get("error", "")
# ---------------------------------------------------------------------------
# Line 319 - message router rule with target=None -> continue
# ---------------------------------------------------------------------------
@given("a message router with a rule that has no target")
def step_router_null_target(context):
rules = [
{"condition": {"equals": "anything"}, "target": None},
]
config = NodeConfig(
name="router_null", type=NodeType.MESSAGE_ROUTER, metadata={"rules": rules}
)
context.node = Node(config)
context.state = _make_state(metadata={"current_message": "anything"})
@when("I execute the null-target router")
def step_execute_null_target_router(context):
context.result = _run(context.node.execute(context.state))
@then("the router should return routed_to as None")
def step_assert_null_target_router(context):
metadata = context.result.get("metadata", {})
assert metadata.get("routed_to") is None
# ---------------------------------------------------------------------------
# Lines 321-322 - message router rule with condition=None -> unconditional match
# ---------------------------------------------------------------------------
@given('a message router with an unconditional rule targeting "{target}"')
def step_router_unconditional(context, target):
rules = [
{"condition": None, "target": target},
]
config = NodeConfig(
name="router_uncond", type=NodeType.MESSAGE_ROUTER, metadata={"rules": rules}
)
context.node = Node(config)
context.state = _make_state(metadata={"current_message": "irrelevant"})
@when("I execute the unconditional router")
def step_execute_unconditional_router(context):
context.result = _run(context.node.execute(context.state))
@then('the router should route to "{target}"')
def step_assert_router_target(context, target):
metadata = context.result.get("metadata", {})
assert metadata.get("routed_to") == target
# ---------------------------------------------------------------------------
# Lines 319 + 321-322 combined - null-target skipped, then unconditional hit
# ---------------------------------------------------------------------------
@given(
'a message router with a null-target rule followed by an unconditional rule to "{target}"'
)
def step_router_mixed_rules(context, target):
rules = [
{"condition": {"equals": "x"}, "target": None}, # line 319: continue
{"condition": None, "target": target}, # lines 321-322: match
]
config = NodeConfig(
name="router_mixed", type=NodeType.MESSAGE_ROUTER, metadata={"rules": rules}
)
context.node = Node(config)
context.state = _make_state(metadata={"current_message": "x"})
@when("I execute the mixed-rules router")
def step_execute_mixed_router(context):
context.result = _run(context.node.execute(context.state))
# ---------------------------------------------------------------------------
# Line 337 - evaluate_edge_condition returns True when no condition
# ---------------------------------------------------------------------------
@given("an edge with no condition")
def step_edge_no_condition(context):
context.edge = Edge(source="a", target="b", condition=None)
context.node = Node(NodeConfig(name="edge_node", type=NodeType.END))
context.state = _make_state(messages=[{"role": "user", "content": "test"}])
@when("I evaluate the unconditioned edge")
def step_evaluate_unconditioned_edge(context):
context.edge_result = context.node.evaluate_edge_condition(
context.edge, context.state
)
@then("the edge evaluation should return true")
def step_assert_unconditioned_edge(context):
assert context.edge_result is True