""" Step definitions for uncovered stream_router paths. """ from typing import Any import rx from behave import given, then, when from behave.runner import Context from cleveragents.reactive.stream_router import ( ReactiveStreamRouter, StreamConfig, StreamMessage, StreamRoutingError, StreamType, ) @given("the CleverAgents reactive system is available") def step_system_available(context: Context) -> None: context.stream_router = ReactiveStreamRouter() @given("I have a base stream message with context metadata") def step_base_stream_message(context: Context) -> None: context.original_message = StreamMessage( content={"value": 1}, metadata={"context": {"trace": 1}, "other": {"a": 1}}, source_stream="base", timestamp=1.0, ) @when("I copy the message without metadata override") def step_copy_message(context: Context) -> None: context.copied_message = context.original_message.copy_with() @then("the metadata should be deep-copied except context") def step_verify_copy(context: Context) -> None: assert context.copied_message.metadata["other"] == {"a": 1} assert ( context.copied_message.metadata["other"] is not context.original_message.metadata["other"] ) assert ( context.copied_message.metadata["context"] is context.original_message.metadata["context"] ) @when("I build a map operator without required params") def step_map_without_params(context: Context) -> None: context.error = None try: context.stream_router._create_operator({"type": "map", "params": {}}) except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about missing map params") def step_verify_map_error(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "Map operator requires" in str(context.error) @when("I build a buffer operator without count") def step_buffer_without_count(context: Context) -> None: context.error = None try: context.stream_router._create_operator({"type": "buffer", "params": {}}) except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about buffer count") def step_verify_buffer_error(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "Buffer operator requires" in str(context.error) @when("I build a window operator without count") def step_window_without_count(context: Context) -> None: context.error = None try: context.stream_router._create_operator({"type": "window", "params": {}}) except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about window count") def step_verify_window_error(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "Window operator requires" in str(context.error) @when("I evaluate a field condition against a message without that field") def step_eval_missing_field(context: Context) -> None: context.condition_result = context.stream_router._evaluate_condition( {}, {"field": "missing"} ) @then("the condition result should be false") def step_verify_condition_false(context: Context) -> None: assert context.condition_result is False @when("I apply the append accumulator with list initialization") def step_apply_append_accumulator(context: Context) -> None: context.acc_result = context.stream_router._apply_accumulator( None, "item", {"op": "append", "init": "list"} ) @then("the accumulator should return a list with the item") def step_verify_append_accumulator(context: Context) -> None: assert context.acc_result == ["item"] @when("I send a message to an unknown stream") def step_send_unknown_stream(context: Context) -> None: context.error = None try: context.stream_router.send_message("unknown_stream", "data") except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about missing stream") def step_verify_missing_stream(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "not found" in str(context.error) @when("I split a stream from an unknown source") def step_split_unknown_source(context: Context) -> None: context.error = None try: context.stream_router.split_stream("unknown_source", []) except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about missing source") def step_verify_missing_source(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "not found" in str(context.error) @when('I create a stream from the string name "string_stream"') def step_create_stream_from_string(context: Context) -> None: context.created_stream = context.stream_router.create_stream("string_stream") @then("the stream should exist in the router") def step_stream_exists(context: Context) -> None: assert "string_stream" in context.stream_router.streams assert context.stream_router.stream_configs["string_stream"].name == "string_stream" @given('I have already created a stream named "dup_stream"') def step_existing_stream(context: Context) -> None: context.stream_router.create_stream("dup_stream") @when('I try to create the stream "dup_stream" again') def step_create_duplicate_stream(context: Context) -> None: context.error = None try: context.stream_router.create_stream("dup_stream") except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about duplicate stream") def step_verify_duplicate_stream_error(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "already exists" in str(context.error) @when('I map a message using function param "noop_function"') def step_map_with_function_param(context: Context) -> None: context.stream_router._builtin_noop_function = lambda x: x operator = context.stream_router._create_operator( {"type": "map", "params": {"function": "noop_function"}} ) captured: list[Any] = [] rx.just(5).pipe(operator).subscribe(lambda v: captured.append(v)) context.map_result = captured[0] if captured else None @then("the mapped result should equal the original message") def step_verify_map_function_result(context: Context) -> None: assert context.map_result == 5 @when("I build a transform operator with an invalid function string") def step_transform_invalid_fn(context: Context) -> None: context.error = None try: context.stream_router._create_operator( {"type": "transform", "params": {"fn": "lambda x: x +"}} ) except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about unregistered transform fn") def step_verify_transform_invalid_fn(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "Unknown transform" in str(context.error) @when("I build a filter operator without condition") def step_filter_without_condition(context: Context) -> None: context.error = None try: context.stream_router._create_operator({"type": "filter", "params": {}}) except Exception as exc: # pylint: disable=broad-except context.error = exc @then("I should get a stream routing error about missing filter condition") def step_verify_filter_condition_error(context: Context) -> None: assert isinstance(context.error, StreamRoutingError) assert "Filter operator requires" in str(context.error) @when("I run a switch operator with default operators against a message") def step_switch_default_operators(context: Context) -> None: default_ops = [ { "type": "map", "params": {"transform": {"type": "replace", "target": "x", "value": 10}}, } ] switch_op = context.stream_router._create_operator( {"type": "switch", "params": {"cases": [], "default_operators": default_ops}} ) message = {"x": 1} captured = [] switch_op(rx.just(message)).subscribe(lambda v: captured.append(v)) if captured and isinstance(captured[0], dict): context.switch_result = captured[0].get("x") else: context.switch_result = None @then("the switch operator should output the transformed value") def step_verify_switch_default_ops(context: Context) -> None: assert context.switch_result == 10 @when("I run a switch operator that targets a default stream subject") def step_switch_default_stream(context: Context) -> None: context.stream_router.create_stream( StreamConfig(name="default_target", type=StreamType.COLD) ) switch_op = context.stream_router._create_operator( {"type": "switch", "params": {"cases": [], "default": "default_target"}} ) message = StreamMessage(content="payload") emitted = [] context.stream_router.streams["default_target"].subscribe( lambda v: emitted.append(v.content if hasattr(v, "content") else v) ) switch_op(rx.just(message)).subscribe(lambda _v: None) # drive operator # manually emit to default stream to exercise branch context.stream_router.streams["default_target"].on_next(message) context.default_stream_value = emitted[0] if emitted else None @then("the default stream should emit its initial value") def step_verify_switch_default_stream(context: Context) -> None: assert context.default_stream_value == "payload" @when("I apply a replace transform to a dictionary message") def step_apply_replace_transform(context: Context) -> None: context.replace_result = context.stream_router._apply_transform( {"foo": 1}, {"type": "replace", "target": "foo", "value": 2} ) @then("the dictionary field should be updated") def step_verify_replace_transform(context: Context) -> None: assert context.replace_result["foo"] == 2 @when("I apply the sum accumulator to numeric messages") def step_apply_sum_accumulator(context: Context) -> None: acc = context.stream_router._apply_accumulator(None, 1, {"op": "sum"}) context.sum_result = context.stream_router._apply_accumulator(acc, 2, {"op": "sum"}) @then("the accumulator should produce the summed value") def step_verify_sum_accumulator(context: Context) -> None: assert context.sum_result == 3 @given('I have two source streams named "merge_a" and "merge_b"') def step_create_merge_sources(context: Context) -> None: context.stream_router.create_stream("merge_a") context.stream_router.create_stream("merge_b") @when('I merge the streams into a new target "merge_output"') def step_merge_streams(context: Context) -> None: context.stream_router.merge_streams(["merge_a", "merge_b"], "merge_output") @then("the merged target stream should exist") def step_verify_merge_target(context: Context) -> None: assert "merge_output" in context.stream_router.streams @given("I have a stream router with disposable streams and subscriptions") def step_router_with_disposables(context: Context) -> None: from rx.subject import Subject disposable_stream = Subject() disposable_stream.dispose = lambda: setattr(context, "disposed_called", True) # type: ignore[attr-defined] context.stream_router.streams["disposable"] = disposable_stream context.stream_router.observables["disposable"] = disposable_stream context.stream_router.stream_configs["disposable"] = StreamConfig(name="disposable") context.stream_router.subscriptions.append( lambda: setattr(context, "subscription_disposed", True) ) @when("I dispose the stream router") def step_dispose_router(context: Context) -> None: context.stream_router.dispose() @then("the stream router internals should be cleared") def step_verify_dispose(context: Context) -> None: assert context.stream_router.streams == {} assert context.stream_router.subscriptions == []