forked from HAL9000/cleveragents-core
331 lines
12 KiB
Python
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 == []
|