from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock, patch import rx from behave import given, then, when from cleveragents.langgraph.bridge import RxPyLangGraphBridge from cleveragents.langgraph.state import GraphState from cleveragents.reactive.stream_router import ReactiveStreamRouter, StreamMessage def _make_graph_state( messages: list | None = None, data: dict | None = None ) -> GraphState: state = MagicMock(spec=GraphState) state.messages = messages or [] state.to_dict = MagicMock(return_value=data or {"messages": state.messages}) return state @given("I set up a langgraph bridge test harness") def step_impl(context): scheduler = MagicMock() router = ReactiveStreamRouter(scheduler=scheduler) router.agents = {"agent": MagicMock()} context.router = router context.bridge = RxPyLangGraphBridge(router) context.results = [] @when("I create a bridge with mocked graph configuration") def step_impl(context): config = { "name": "test_graph", "entry_point": "start", "nodes": { "worker": { "type": "function", "function": "do_work", "metadata": {"priority": "high"}, } }, "edges": [{"source": "start", "target": "worker"}], } with patch("cleveragents.langgraph.bridge.LangGraph") as mock_langgraph: mock_graph = MagicMock() mock_graph.name = "test_graph" mock_graph.nodes = {} mock_graph.state_manager = MagicMock() mock_graph.state_manager.get_state = MagicMock() mock_graph.state_manager.update_state = MagicMock() mock_graph.state_manager._save_checkpoint = MagicMock() mock_graph.get_execution_history = MagicMock(return_value=["step"]) mock_langgraph.return_value = mock_graph graph = context.bridge.create_graph_from_config(config) context.graph = graph context.graph_mock = mock_graph context.results.append("create_graph_from_config") # operator registration & helper passthrough context.results.append( "operators_registered" if hasattr(context.bridge, "_operator_graph_execute") else "operators_missing" ) context.results.append( "run_async_safely" if context.bridge._run_async_safely("x") == "x" else "run_async_failed" ) # cleanup_tasks path with pending task loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) async def pending(): await asyncio.sleep(0.01) task = loop.create_task(pending()) context.bridge._active_tasks.add(task) context.bridge.cleanup_tasks() loop.run_until_complete(asyncio.sleep(0)) context.results.append( "cleanup_tasks_cancelled" if task.cancelled() else "cleanup_tasks_failed" ) loop.close() asyncio.set_event_loop(asyncio.new_event_loop()) @when("I exercise bridge graph and stream creation") def step_impl(context): stream_config = context.bridge.create_graph_stream("test_graph") assert stream_config.name == "graph_test_graph" context.results.append("create_graph_stream") try: context.bridge.create_graph_stream("missing") except ValueError: context.results.append("create_graph_stream_error") @when("I exercise graph executor branches") def step_impl(context): bridge = context.bridge graph = context.graph_mock async def exec_with_messages(data): return _make_graph_state(messages=[{"content": "ok"}]) async def exec_without_messages(data): return _make_graph_state(messages=[], data={"data": "empty"}) graph.execute = exec_with_messages executor = bridge._create_graph_executor({"graph": "test_graph"}) loop = asyncio.get_event_loop() def _run_operator(message): results = [] rx.just(message).pipe(executor).subscribe(lambda x: results.append(x)) loop.run_until_complete(asyncio.sleep(0.01)) return results _run_operator(StreamMessage(content="hello", metadata={})) context.results.append("graph_executor_string") _run_operator( StreamMessage( content={"messages": [{"role": "user", "content": "hi"}]}, metadata={} ) ) context.results.append("graph_executor_dict") _run_operator(StreamMessage(content=123, metadata={"meta": True})) context.results.append("graph_executor_other") graph.execute = exec_without_messages _run_operator(StreamMessage(content="empty", metadata={})) context.results.append("graph_executor_empty") try: bridge._create_graph_executor({"graph": "bad"}) except ValueError: context.results.append("graph_executor_error") @when("I exercise state utilities and node operator") def step_impl(context): bridge = context.bridge graph = context.graph_mock updater = bridge._create_state_updater({"graph": "test_graph"}) rx.just(StreamMessage(content={"value": 1}, metadata={"a": 1})).pipe( updater ).subscribe(lambda _: None) rx.just(StreamMessage(content="text", metadata={})).pipe(updater).subscribe( lambda _: None ) context.results.append("state_updater") try: bridge._create_state_updater({"graph": "bad"}) except ValueError: context.results.append("state_updater_error") graph.state_manager.checkpoint_dir = True checkpointer = bridge._create_state_checkpointer({"graph": "test_graph"}) rx.just(StreamMessage(content="x", metadata={"cp": True})).pipe( checkpointer ).subscribe(lambda _: None) graph.state_manager._save_checkpoint.assert_called() context.results.append("state_checkpointer") try: bridge._create_state_checkpointer({"graph": "bad"}) except ValueError: context.results.append("state_checkpointer_error") node = MagicMock() node.execute = AsyncMock(return_value={"messages": [{"content": "node"}]}) graph.nodes["worker"] = node node_op = bridge._create_node_operator({"graph": "test_graph", "node": "worker"}) rx.just(StreamMessage(content="n", metadata={})).pipe(node_op).subscribe( lambda _: None ) context.results.append("node_operator") node.execute = AsyncMock(return_value={"data": "nomsg"}) rx.just(StreamMessage(content="n", metadata={})).pipe(node_op).subscribe( lambda _: None ) context.results.append("node_operator_nomsg") try: bridge._create_node_operator({"graph": "bad", "node": "worker"}) except ValueError: context.results.append("node_operator_bad_graph") try: bridge._create_node_operator({"graph": "test_graph", "node": "missing"}) except ValueError: context.results.append("node_operator_bad_node") @when("I exercise conditional routing helpers") def step_impl(context): bridge = context.bridge rules = [ {"condition": {"equals": "match"}, "target": "t1"}, {"condition": {"field": "kind", "value": "x"}, "target": "t2"}, ] router = bridge._create_conditional_router({"rules": rules}) msg = StreamMessage(content="match", metadata={}) result_list = [] rx.just(msg).pipe(router).subscribe(lambda x: result_list.append(x)) context.results.append("conditional_equals") msg2 = StreamMessage(content={"kind": "x"}, metadata={}) result_list2 = [] rx.just(msg2).pipe(router).subscribe(lambda x: result_list2.append(x)) context.results.append("conditional_field") msg3 = StreamMessage(content="none", metadata={}) result_list3 = [] rx.just(msg3).pipe(router).subscribe(lambda x: result_list3.append(x)) context.results.append("conditional_default") assert bridge._evaluate_condition( StreamMessage(content="value", metadata={}), {"equals": "value"} ) assert not bridge._evaluate_condition( StreamMessage(content={"k": "v"}, metadata={}), {"field": "k", "value": "x"} ) context.results.append("evaluate_condition") @when("I exercise router node helpers and cleanup") def step_impl(context): bridge = context.bridge rules = [{"condition": {"equals": "yes"}, "target": "node_a"}] node_cfg = bridge.create_message_router_node(rules) assert node_cfg.metadata["rules"] == rules context.results.append("message_router_node") edges = bridge.create_conditional_edges(rules) assert len(edges) == 2 # start->target and start->end context.results.append("conditional_edges") bridge.cleanup_tasks() context.results.append("cleanup_tasks_empty") @then("bridge coverage steps should complete successfully") def step_impl(context): expected = { "create_graph_from_config", "operators_registered", "run_async_safely", "cleanup_tasks_cancelled", "create_graph_stream", "create_graph_stream_error", "graph_executor_string", "graph_executor_dict", "graph_executor_other", "graph_executor_empty", "graph_executor_error", "state_updater", "state_updater_error", "state_checkpointer", "state_checkpointer_error", "node_operator", "node_operator_nomsg", "node_operator_bad_graph", "node_operator_bad_node", "conditional_equals", "conditional_field", "conditional_default", "evaluate_condition", "message_router_node", "conditional_edges", "cleanup_tasks_empty", } missing = expected - set(context.results) assert not missing, f"Missing coverage steps: {missing}"