Files
temp/features/steps/stream_router_additional_coverage_steps.py

331 lines
12 KiB
Python

"""
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 == []