"""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