""" Additional step definitions to cover remaining ReactiveStreamRouter branches. """ from __future__ import annotations import rx from behave import given, then, when from behave.runner import Context from rx.subject import ReplaySubject, Subject from cleveragents.reactive.stream_router import ( StreamConfig, StreamMessage, StreamRoutingError, StreamType, ) @when("I copy the message with explicit metadata override") def step_copy_with_override(context: Context) -> None: # assumes context.original_message provided by other steps override = {"override": True} context.copied_message = context.original_message.copy_with(metadata=override) @then("the copy should use the provided metadata") def step_verify_copy_override(context: Context) -> None: assert context.copied_message.metadata == {"override": True} @when('I create a hot stream named "hot_stream" with initial value 5') def step_create_hot_stream(context: Context) -> None: config = StreamConfig(name="hot_stream", type=StreamType.HOT, initial_value=5) stream = context.stream_router.create_stream(config) context.created_stream = stream @then("the hot stream should store the initial value") def step_verify_hot_stream(context: Context) -> None: assert "hot_stream" in context.stream_router.streams hot_stream = context.created_stream # BehaviorSubject holds the last value; get value via .value assert hasattr(hot_stream, "value") assert hot_stream.value == 5 @when('I create a replay stream named "replay_stream" with buffer size 3') def step_create_replay_stream(context: Context) -> None: config = StreamConfig( name="replay_stream", type=StreamType.REPLAY, buffer_size=3, initial_value=1 ) stream = context.stream_router.create_stream(config) context.created_stream = stream @then("the replay stream should be created with buffer size 3") def step_verify_replay_stream(context: Context) -> None: assert "replay_stream" in context.stream_router.streams replay_stream = context.created_stream assert isinstance(replay_stream, ReplaySubject) @when('I build a langgraph operator "graph_execute" without bridge') def step_langgraph_without_bridge(context: Context) -> None: context.error = None try: context.stream_router._create_operator({"type": "graph_execute", "params": {}}) except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about missing langgraph bridge") def step_verify_langgraph_error(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "LangGraph operator" in str(context.error) @when('I build a map operator targeting unknown agent "ghost"') def step_map_unknown_agent(context: Context) -> None: context.error = None try: context.stream_router._create_operator( {"type": "map", "params": {"agent": "ghost"}} ) except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about missing agent") def step_verify_missing_agent(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "not found" in str(context.error) @when("I build debounce, throttle, and delay operators") def step_build_temporal_ops(context: Context) -> None: context.debounce_op = context.stream_router._create_operator( {"type": "debounce", "params": {"duration": 0.03}} ) context.throttle_op = context.stream_router._create_operator( {"type": "throttle", "params": {"duration": 0.1}} ) context.delay_op = context.stream_router._create_operator( {"type": "delay", "params": {"duration": 0.01}} ) @then("the temporal operators should be created successfully") def step_verify_temporal_ops(context: Context) -> None: assert context.debounce_op is not None assert context.throttle_op is not None assert context.delay_op is not None @when("I build buffer and window operators with count 2") def step_build_buffer_window(context: Context) -> None: context.buffer_op = context.stream_router._create_operator( {"type": "buffer", "params": {"count": 2, "timeout": 0.1}} ) context.window_op = context.stream_router._create_operator( {"type": "window", "params": {"count": 2}} ) @then("the buffer and window operators should be created successfully") def step_verify_buffer_window(context: Context) -> None: assert context.buffer_op is not None assert context.window_op is not None @when("I build take and skip operators") def step_build_take_skip(context: Context) -> None: context.take_op = context.stream_router._create_operator( {"type": "take", "params": {"count": 1}} ) context.skip_op = context.stream_router._create_operator( {"type": "skip", "params": {"count": 1}} ) @then("the take and skip operators should be created successfully") def step_verify_take_skip(context: Context) -> None: assert context.take_op is not None assert context.skip_op is not None @given('I have two source streams named "merge_x" and "merge_y"') def step_create_merge_sources_additional(context: Context) -> None: context.stream_router.create_stream("merge_x") context.stream_router.create_stream("merge_y") @when("I create a merge operator for those streams") def step_build_merge_operator(context: Context) -> None: context.merge_op = context.stream_router._create_operator( {"type": "merge", "params": {"streams": ["merge_x", "merge_y"]}} ) @then("the merge operator should be usable") def step_verify_merge_operator(context: Context) -> None: captured: list[str] = [] rx.merge(rx.just("a"), rx.just("b")).pipe(context.merge_op).subscribe( captured.append ) assert captured == ["a", "b"] @when("I build an accumulate operator with sum") def step_build_accumulate_sum(context: Context) -> None: context.accumulate_op = context.stream_router._create_operator( {"type": "accumulate", "params": {"accumulator": {"op": "sum"}}} ) @then("the accumulate operator should sum incoming numbers") def step_verify_accumulate_sum(context: Context) -> None: captured: list[int] = [] rx.from_([1, 2]).pipe(context.accumulate_op).subscribe(captured.append) assert captured[-1] == 3 @when("I apply an extract_field transform to a dictionary message") def step_apply_extract_transform(context: Context) -> None: context.extract_result = context.stream_router._apply_transform( {"foo": 42}, {"type": "extract_field", "field": "foo"} ) @then("the extracted field should be returned") def step_verify_extract_transform(context: Context) -> None: assert context.extract_result == 42 @when("I evaluate empty and equals conditions against messages") def step_evaluate_conditions(context: Context) -> None: context.condition_empty = context.stream_router._evaluate_condition({}, {}) context.condition_equals = context.stream_router._evaluate_condition( StreamMessage(content="value"), {"equals": "value"} ) @then("the empty condition should be true and equals should match") def step_verify_conditions(context: Context) -> None: assert context.condition_empty is True assert context.condition_equals is True class _AgentSuccess: def process_message_sync(self, content, metadata): # type: ignore[override] return f"ok:{content}" class _AgentFailure: def process_message_sync(self, content, metadata): # type: ignore[override] raise RuntimeError("fail") @when("I map messages through agent mapper success and failure paths") def step_agent_mapper_paths(context: Context) -> None: success_mapper = context.stream_router._create_agent_mapper(_AgentSuccess()) failure_mapper = context.stream_router._create_agent_mapper(_AgentFailure()) context.success_result = success_mapper("hi").content context.failure_result = failure_mapper("hi").content @then("the mapper should emit processed content and blank on error") def step_verify_agent_mapper_paths(context: Context) -> None: assert context.success_result == "ok:hi" assert context.failure_result == "" @when("I apply append accumulator to non-list accumulator") def step_append_non_list(context: Context) -> None: context.append_result = context.stream_router._apply_accumulator( 0, "x", {"op": "append"} ) @then("the accumulator should remain unchanged") def step_verify_append_non_list(context: Context) -> None: assert context.append_result == 0 @when("I setup subscriptions including a missing source") def step_setup_subscriptions_missing(context: Context) -> None: config = StreamConfig(name="target_sub", subscriptions=["missing_source"]) context.stream_router.create_stream(config) context.subscription_count = len(context.stream_router.subscriptions) @then("no subscription should be added for the missing source") def step_verify_missing_subscription(context: Context) -> None: assert context.subscription_count == 0 @given('I have a stream "source_split" and target "target_split"') def step_create_split_streams(context: Context) -> None: context.stream_router.create_stream("source_split") context.stream_router.create_stream("target_split") @when("I split the source stream with a matching condition") def step_split_with_condition(context: Context) -> None: condition = {"target": "target_split", "condition": {"equals": "route"}} context.stream_router.split_stream("source_split", [condition]) captured: list[str] = [] context.stream_router.streams["target_split"].subscribe( lambda v: captured.append(v.content) ) context.stream_router.send_message("source_split", "route") context.split_captured = captured @then("the target stream should receive the routed message") def step_verify_split_routing(context: Context) -> None: assert context.split_captured == ["route"] @when("I subscribe to output and error streams and emit messages") def step_subscribe_output_error(context: Context) -> None: output_captured: list[str] = [] error_captured: list[str] = [] context.stream_router.subscribe_to_output( lambda v: output_captured.append(v.content) ) context.stream_router.subscribe_to_error(lambda v: error_captured.append(v.content)) context.stream_router.send_message("__output__", "out_msg") context.stream_router.send_message("__error__", "err_msg") context.output_captured = output_captured context.error_captured = error_captured @then("the observers should receive the respective messages") def step_verify_output_error(context: Context) -> None: assert context.output_captured == ["out_msg"] assert context.error_captured == ["err_msg"] @given("I have a stream router with disposable streams and subscriptions that may fail") def step_router_with_failing_disposables(context: Context) -> None: failing_stream = Subject() def _dispose_fail(): raise RuntimeError("dispose failure") failing_stream.dispose = _dispose_fail # type: ignore[attr-defined] context.stream_router.streams["fail_stream"] = failing_stream context.stream_router.observables["fail_stream"] = failing_stream context.stream_router.stream_configs["fail_stream"] = StreamConfig( name="fail_stream" ) class _Sub: def dispose(self): raise RuntimeError("subscription dispose failure") context.stream_router.subscriptions.append(_Sub()) @when("I dispose the stream router with failing disposables") def step_dispose_with_failures(context: Context) -> None: context.stream_router.dispose() @then("the stream router internals should be cleared even on dispose errors") def step_verify_dispose_with_failures(context: Context) -> None: assert context.stream_router.streams == {} assert context.stream_router.subscriptions == []