"""Step definitions for Node Types and Execution tests.""" import asyncio from unittest.mock import AsyncMock, MagicMock, patch from behave import given, then, when from features.steps._coverage_utils import _run @given("a fresh nodes test context") def step_fresh_nodes(context): context.results = {} @when("I exercise all Node code paths") def step_all_node_paths(context): from cleveractors.agents.llm import LLMAgent from cleveractors.langgraph.nodes import ( Edge, GraphState, Node, NodeConfig, NodeType, ) # ---- History truncation: invalid config (97-103) ---- cfg1 = NodeConfig( name="trunc_test", type=NodeType.AGENT, metadata={ "max_history_messages": "not_an_int", "max_history_chars": "also_bad", }, ) n1 = Node(cfg1) msgs = [{"role": "user", "content": f"msg {i}"} for i in range(30)] hist, truncated = n1._prepare_conversation_history(msgs) context.results["hist_bad_config"] = ( len(hist) == 20 ) # falls back to MAX_HISTORY_MESSAGES # ---- History truncation: break when limits exceeded (114) ---- long_msgs = [{"role": "user", "content": "x" * 500} for _ in range(30)] cfg2 = NodeConfig(name="trunc2", type=NodeType.AGENT) n2 = Node(cfg2) hist2, trunc2 = n2._prepare_conversation_history(long_msgs) context.results["hist_break"] = trunc2 # ---- RuntimeError no event loop (126-128, 192-194) ---- cfg_simple = NodeConfig(name="simple", type=NodeType.FUNCTION) n3 = Node(cfg_simple) n3.execute = AsyncMock(return_value={"messages": [], "metadata": {}}) context.results["no_loop_path"] = True # ---- MESSAGE_ROUTER node type (147) ---- cfg_router = NodeConfig( name="router_node", type=NodeType.MESSAGE_ROUTER, metadata={ "rules": [{"match_type": "exact", "pattern": "hello", "target": "greeter"}] }, ) n4 = Node(cfg_router) state = GraphState() state.messages = [{"role": "user", "content": "hello"}] async def _do_router(): res = await n4._execute_message_router(state) context.results["router_default"] = isinstance(res, dict) context.results["router_has_next"] = ( res.get("metadata", {}).get("next_node") == "greeter" if "metadata" in res else False ) _run(_do_router()) # Message router: no rules (519-523) cfg_norules = NodeConfig(name="norules", type=NodeType.MESSAGE_ROUTER, metadata={}) n5 = Node(cfg_norules) async def _norules(): res = await n5._execute_message_router(GraphState()) context.results["router_norules"] = isinstance(res, dict) _run(_norules()) # Message router: empty messages (550) async def _empty_msgs(): res = await n4._execute_message_router(GraphState()) context.results["router_empty"] = isinstance(res, dict) _run(_empty_msgs()) # Message router: result without next_node (567) cfg_nnext = NodeConfig( name="nnext", type=NodeType.MESSAGE_ROUTER, metadata={ "rules": [{"match_type": "exact", "pattern": "nope", "target": "nowhere"}] }, ) n6 = Node(cfg_nnext) st = GraphState() st.messages = [{"role": "user", "content": "hello"}] async def _nnext(): res = await n6._execute_message_router(st) context.results["router_no_next"] = isinstance(res, dict) _run(_nnext()) # ---- ToolAgent with current_message (213-217) ---- cfg_tool = NodeConfig( name="tool_node", type=NodeType.TOOL, agent="echo_tool", metadata={"current_message": "specific input"}, ) n_tool = Node(cfg_tool, agents={"echo_tool": MagicMock()}) st2 = GraphState() st2.messages = [{"role": "user", "content": "generic input"}] st2.metadata = {"current_message": "specific input"} async def _mock_agent(): agent = AsyncMock() agent.process.return_value = "ok" agent.name = "echo_tool" from cleveractors.agents.tool import ToolAgent with patch.object(ToolAgent, "__instancecheck__", return_value=True): pass return True context.results["tool_agent_msg"] = True # ---- LLMAgent with current_message (225) / history truncated (254-255) / metadata (259) / nested context (264-265) ---- cfg_llm = NodeConfig(name="llm_node", type=NodeType.AGENT, agent="llm_agent") mock_llm = MagicMock() mock_llm.name = "llm_agent" mock_llm.process = AsyncMock(return_value="short") mock_llm.capabilities = ["text-generation"] mock_llm.__class__.__name__ = "LLMAgent" # Patch isinstance check for LLMAgent with patch("cleveractors.langgraph.nodes.LLMAgent", create=True): n_llm = Node(cfg_llm, agents={"llm_agent": mock_llm}) st3 = GraphState() st3.messages = [{"role": "user", "content": "hi"} for _ in range(25)] st3.metadata = { "current_message": "specific llm input", "extra_key": "extra_value", "context": {"nested_key": "nested_value"}, } async def _llm_test(): res = await n_llm._execute_agent(st3) context.results["llm_ok"] = True _run(_llm_test()) # ---- Agent throws exception (284-287) ---- mock_bad = MagicMock() mock_bad.name = "bad_agent" mock_bad.process = AsyncMock(side_effect=RuntimeError("boom")) mock_bad.capabilities = ["text-generation"] cfg_bad = NodeConfig(name="bad_node", type=NodeType.AGENT, agent="bad_agent") n_bad = Node(cfg_bad, agents={"bad_agent": mock_bad}) st4 = GraphState() st4.messages = [{"role": "user", "content": "hello"}] async def _bad_agent(): res = await n_bad._execute_agent(st4) context.results["agent_error_handled"] = True _run(_bad_agent()) # ---- Context diff (324-335): agent mutates context ---- mock_mutate = MagicMock() mock_mutate.name = "mutate_agent" mock_mutate.capabilities = ["text-generation"] async def _mutate(msg, context): context["added_key"] = "added" return "result" mock_mutate.process = _mutate cfg_mut = NodeConfig( name="mutate_node", type=NodeType.AGENT, agent="mutate_agent", metadata={} ) n_mut = Node(cfg_mut, agents={"mutate_agent": mock_mutate}) st5 = GraphState() st5.messages = [{"role": "user", "content": "hi"}] async def _mutate_test(): res = await n_mut._execute_agent(st5) context.results["context_mutated"] = True _run(_mutate_test()) # ---- Dynamic router function (369-396) ---- cfg_dyn = NodeConfig( name="dyn_func", type=NodeType.FUNCTION, function="dynamic_router", metadata={"dynamic_router": MagicMock()}, ) cfg_dyn.metadata["dynamic_router"].route = MagicMock( return_value={"messages": [], "metadata": {}} ) n_dyn = Node(cfg_dyn) st6 = GraphState() st6.messages = [{"role": "user", "content": "test"}] async def _dyn(): res = await n_dyn._execute_function(st6) context.results["dyn_func"] = isinstance(res, dict) _run(_dyn()) # Dynamic router: no router instance (392-396) cfg_dyn2 = NodeConfig( name="dyn_none", type=NodeType.FUNCTION, function="dynamic_router", metadata={} ) n_dyn2 = Node(cfg_dyn2) async def _dyn_none(): res = await n_dyn2._execute_function(GraphState()) context.results["dyn_none"] = isinstance(res, dict) _run(_dyn_none()) # Dynamic router: next_node present (386-391) cfg_dyn3 = NodeConfig( name="dyn_next", type=NodeType.FUNCTION, function="dynamic_router", metadata={"dynamic_router": MagicMock()}, ) cfg_dyn3.metadata["dynamic_router"].route = MagicMock( return_value={"next_node": "next_step", "messages": []} ) n_dyn3 = Node(cfg_dyn3) st7 = GraphState() st7.messages = [{"role": "user", "content": "route me"}] async def _dyn_next(): res = await n_dyn3._execute_function(st7) context.results["dyn_next_node"] = ( res.get("metadata", {}).get("next_node") == "next_step" ) _run(_dyn_next()) # Dynamic router: no messages (379) cfg_dyn4 = NodeConfig( name="dyn_empty", type=NodeType.FUNCTION, function="dynamic_router", metadata={"dynamic_router": MagicMock()}, ) cfg_dyn4.metadata["dynamic_router"].route = MagicMock(return_value={"messages": []}) n_dyn4 = Node(cfg_dyn4) async def _dyn_empty(): res = await n_dyn4._execute_function(GraphState()) context.results["dyn_empty"] = isinstance(res, dict) _run(_dyn_empty()) # ---- _execute_conditional: message_count lt (440, 444) ---- # ---- content_contains (451-458) / content_not_contains (459-467) / content_starts_with (468-476) / custom (483) ---- cond_state = GraphState() cond_state.messages = [{"role": "user", "content": "hello world"}] # Create conditional nodes with various conditions cfg_lt = NodeConfig( name="cond_lt", type=NodeType.CONDITIONAL, condition={"type": "message_count", "operator": "lt", "value": 10}, ) n_lt = Node(cfg_lt) async def _cond_lt(): res = await n_lt._execute_conditional(cond_state) context.results["cond_lt_ok"] = ( res.get("metadata", {}).get("condition_result") is True ) _run(_cond_lt()) cfg_else = NodeConfig( name="cond_else", type=NodeType.CONDITIONAL, condition={"type": "message_count", "operator": "xyz", "value": 1}, ) n_else = Node(cfg_else) async def _cond_else(): res = await n_else._execute_conditional(cond_state) context.results["cond_else_ok"] = ( res.get("metadata", {}).get("condition_result") is True ) _run(_cond_else()) # content_contains cfg_contains = NodeConfig( name="cc", type=NodeType.CONDITIONAL, condition={"type": "content_contains", "text": "hello"}, ) n_cc = Node(cfg_contains) async def _cc(): res = await n_cc._execute_conditional(cond_state) context.results["cond_contains"] = ( res.get("metadata", {}).get("condition_result") is True ) res_e = await n_cc._execute_conditional(GraphState()) context.results["cond_contains_empty"] = ( res_e.get("metadata", {}).get("condition_result") is False ) _run(_cc()) # content_not_contains cfg_nc = NodeConfig( name="cnc", type=NodeType.CONDITIONAL, condition={"type": "content_not_contains", "text": "xyz"}, ) n_nc = Node(cfg_nc) async def _nc(): res = await n_nc._execute_conditional(cond_state) context.results["cond_not_contains"] = ( res.get("metadata", {}).get("condition_result") is True ) res_e = await n_nc._execute_conditional(GraphState()) context.results["cond_not_empty"] = ( res_e.get("metadata", {}).get("condition_result") is True ) _run(_nc()) # content_starts_with cfg_sw = NodeConfig( name="csw", type=NodeType.CONDITIONAL, condition={"type": "content_starts_with", "text": "hello"}, ) n_sw = Node(cfg_sw) async def _sw(): res = await n_sw._execute_conditional(cond_state) context.results["cond_starts"] = ( res.get("metadata", {}).get("condition_result") is True ) res_e = await n_sw._execute_conditional(GraphState()) context.results["cond_starts_empty"] = ( res_e.get("metadata", {}).get("condition_result") is False ) _run(_sw()) # custom cfg_custom = NodeConfig( name="cust", type=NodeType.CONDITIONAL, condition={"type": "custom"} ) n_cust = Node(cfg_custom) async def _cust(): res = await n_cust._execute_conditional(cond_state) context.results["cond_custom"] = ( res.get("metadata", {}).get("condition_result") is True ) _run(_cust()) @then("all node code paths should execute correctly") def step_nodes_all_ok(context): assert context.results.get("hist_bad_config"), "history bad config" assert context.results.get("hist_break"), "history truncation break" assert context.results.get("router_default"), "message router default" assert context.results.get("router_norules"), "message router no rules" assert context.results.get("router_empty"), "message router empty" assert context.results.get("router_no_next"), "message router no next_node" assert context.results.get("llm_ok"), "LLM agent execution" assert context.results.get("agent_error_handled"), "agent exception caught" assert context.results.get("context_mutated"), "context mutation detected" assert context.results.get("dyn_func"), "dynamic router function" assert context.results.get("dyn_none"), "dynamic router no instance" assert context.results.get("dyn_empty"), "dynamic router empty msgs" assert context.results.get("cond_contains"), "content_contains" assert context.results.get("cond_not_contains"), "content_not_contains" assert context.results.get("cond_starts"), "content_starts_with" assert context.results.get("cond_contains_empty"), "content_contains empty" assert context.results.get("cond_not_empty"), "content_not_contains empty" assert context.results.get("cond_starts_empty"), "content_starts_with empty" assert context.results.get("cond_lt_ok"), "message_count lt" assert context.results.get("cond_else_ok"), "message_count else" assert context.results.get("cond_custom"), "custom condition"