forked from HAL9000/cleveragents-core
f2f7aa5dc9
Implement multiple Stage A/B/E/SEC milestones for the v3 lifecycle system: - Stage A5.3+A5.4: Add LifecycleActionModel and LifecyclePlanModel SQLAlchemy models with to_domain()/from_domain() conversion methods - Stage A5.6: Implement ActionRepository with full CRUD, namespace/state queries, referential integrity checks, and retry decorator - Stage E1: Add subplan domain models (ExecutionMode, SubplanMergeStrategy, SubplanConfig, SubplanStatus, SubplanAttempt, SubplanFailureHandler) with computed properties on Plan (is_subplan, is_root_plan, depth, has_subplans) - Stage A6: Add AutomationLevel enum (MANUAL, REVIEW_BEFORE_APPLY, FULL_AUTOMATION), settings integration, PlanLifecycleService auto-progression, pause/resume, and CLI commands (--automation-level, set-automation-level) - Stage SEC1: Remove eval()/exec() from stream_router.py, replace with named operation and transform registries; code blocks and unregistered transforms now raise StreamRoutingError - Add langchain-anthropic dependency - Update BDD tests for security changes and relax ADR directory requirement
266 lines
8.8 KiB
Python
266 lines
8.8 KiB
Python
"""
|
|
Step definitions for additional stream_router uncovered branches.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import rx
|
|
from behave import given, then, when
|
|
from rx import operators as ops
|
|
|
|
from cleveragents.reactive.stream_router import (
|
|
StreamConfig,
|
|
StreamMessage,
|
|
StreamRoutingError,
|
|
StreamType,
|
|
)
|
|
|
|
|
|
@given("a LangGraph bridge stub is registered on the stream router")
|
|
def step_langgraph_bridge_stub(context) -> None:
|
|
class BridgeStub:
|
|
def __init__(self) -> None:
|
|
self.called = False
|
|
|
|
def _operator_graph_execute(self, params):
|
|
self.called = True
|
|
suffix = params.get("value", "mapped")
|
|
return ops.map(lambda msg: f"{getattr(msg, 'content', msg)}-{suffix}")
|
|
|
|
context.bridge = BridgeStub()
|
|
context.stream_router._langgraph_bridge = context.bridge
|
|
|
|
|
|
@when("I build a graph_execute operator through the bridge")
|
|
def step_build_graph_execute(context) -> None:
|
|
operator = context.stream_router._create_operator(
|
|
{"type": "graph_execute", "params": {"value": "ok"}}
|
|
)
|
|
captured: list[Any] = []
|
|
rx.just(StreamMessage(content="ping")).pipe(operator).subscribe(captured.append)
|
|
context.graph_result = captured[0] if captured else None
|
|
|
|
|
|
@then("the bridge factory should be invoked and map data")
|
|
def step_verify_bridge_called(context) -> None:
|
|
assert context.bridge.called is True
|
|
assert getattr(context.graph_result, "content", context.graph_result).endswith(
|
|
"-ok"
|
|
)
|
|
|
|
|
|
@given("a dummy agent is registered on the stream router")
|
|
def step_register_dummy_agent(context) -> None:
|
|
class DummyAgent:
|
|
def process(self, content, metadata):
|
|
return f"processed-{content}"
|
|
|
|
context.dummy_agent = DummyAgent()
|
|
context.stream_router.register_agent("dummy", context.dummy_agent)
|
|
|
|
|
|
@when("I map a message using that agent")
|
|
def step_map_with_agent(context) -> None:
|
|
operator = context.stream_router._create_operator(
|
|
{"type": "map", "params": {"agent": "dummy"}}
|
|
)
|
|
captured: list[StreamMessage] = []
|
|
rx.just(StreamMessage(content="payload", metadata={"foo": "bar"})).pipe(
|
|
operator
|
|
).subscribe(captured.append)
|
|
context.agent_result = captured[0] if captured else None
|
|
|
|
|
|
@then("the agent-mapped content should be returned with metadata")
|
|
def step_verify_agent_map(context) -> None:
|
|
assert isinstance(context.agent_result, StreamMessage)
|
|
assert context.agent_result.content == "processed-payload"
|
|
assert (
|
|
context.agent_result.metadata.get("processed_by")
|
|
== context.dummy_agent.__class__.__name__
|
|
)
|
|
assert context.agent_result.metadata.get("foo") == "bar"
|
|
|
|
|
|
@when("I build a filter operator with equals condition")
|
|
def step_build_filter_equals(context) -> None:
|
|
operator = context.stream_router._create_operator(
|
|
{"type": "filter", "params": {"condition": {"equals": 2}}}
|
|
)
|
|
captured: list[int] = []
|
|
rx.from_([1, 2, 3]).pipe(operator).subscribe(captured.append)
|
|
context.filter_result = captured
|
|
|
|
|
|
@then("the filter should emit only matching values")
|
|
def step_verify_filter(context) -> None:
|
|
assert context.filter_result == [2]
|
|
|
|
|
|
@when("I build a transform operator with an unregistered function string")
|
|
def step_build_transform_fn_rejected(context) -> None:
|
|
context.error = None
|
|
try:
|
|
context.stream_router._create_operator(
|
|
{"type": "transform", "params": {"fn": "lambda x: x * 3"}}
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
context.error = exc
|
|
|
|
|
|
@then("I should get a stream routing error about unregistered transform")
|
|
def step_verify_transform_rejected(context) -> None:
|
|
from cleveragents.reactive.stream_router import StreamRoutingError
|
|
|
|
assert isinstance(context.error, StreamRoutingError)
|
|
assert "Unknown transform" in str(context.error)
|
|
assert "register_transform" in str(context.error).lower()
|
|
|
|
|
|
@when("I run a switch operator with a case operator pipeline")
|
|
def step_switch_case_operators(context) -> None:
|
|
case_ops = [
|
|
{
|
|
"type": "map",
|
|
"params": {"transform": {"type": "replace", "target": "a", "value": 2}},
|
|
}
|
|
]
|
|
switch_op = context.stream_router._create_operator(
|
|
{
|
|
"type": "switch",
|
|
"params": {
|
|
"cases": [
|
|
{"condition": {"equals": {"a": 1}}, "operators": case_ops},
|
|
]
|
|
},
|
|
}
|
|
)
|
|
message = {"a": 1}
|
|
captured: list[Any] = []
|
|
switch_op(rx.just(message)).subscribe(captured.append)
|
|
context.switch_case_result = captured[0] if captured else None
|
|
|
|
|
|
@then("the switch case should apply its operators")
|
|
def step_verify_switch_case(context) -> None:
|
|
assert isinstance(context.switch_case_result, dict)
|
|
assert context.switch_case_result.get("a") == 2
|
|
|
|
|
|
@given('I have an existing target stream named "case_target"')
|
|
def step_setup_case_target(context) -> None:
|
|
context.stream_router.create_stream(
|
|
StreamConfig(name="case_target", type=StreamType.COLD)
|
|
)
|
|
|
|
|
|
@when("I run a switch operator that routes to that case target")
|
|
def step_switch_routes_case_target(context) -> None:
|
|
switch_op = context.stream_router._create_operator(
|
|
{
|
|
"type": "switch",
|
|
"params": {
|
|
"cases": [
|
|
{"condition": {"equals": "route"}, "target": "case_target"},
|
|
]
|
|
},
|
|
}
|
|
)
|
|
message = StreamMessage(content="route")
|
|
emitted: list[Any] = []
|
|
context.stream_router.streams["case_target"].subscribe(
|
|
lambda v: emitted.append(getattr(v, "content", v))
|
|
)
|
|
switch_op(rx.just(message)).subscribe(lambda _v: None)
|
|
context.stream_router.streams["case_target"].on_next(message)
|
|
context.case_target_emitted = emitted[0] if emitted else None
|
|
|
|
|
|
@then("the case target stream should receive the message")
|
|
def step_verify_case_target(context) -> None:
|
|
assert context.case_target_emitted == "route"
|
|
|
|
|
|
@when("I build an operator with an unknown type")
|
|
def step_build_unknown_operator(context) -> None:
|
|
context.error = None
|
|
try:
|
|
context.stream_router._create_operator({"type": "unknown", "params": {}})
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
context.error = exc
|
|
|
|
|
|
@then("I should get a stream routing error about unknown operator")
|
|
def step_verify_unknown_operator_error(context) -> None:
|
|
assert isinstance(context.error, StreamRoutingError)
|
|
assert "Unknown operator type" in str(context.error)
|
|
|
|
|
|
@when("I apply an extract_field transform to different message types")
|
|
def step_apply_extract_transform(context) -> None:
|
|
context.extract_value = context.stream_router._apply_transform(
|
|
{"foo": "bar"}, {"type": "extract_field", "field": "foo"}
|
|
)
|
|
context.extract_fallback = context.stream_router._apply_transform(
|
|
"raw", {"type": "extract_field", "field": "foo"}
|
|
)
|
|
|
|
|
|
@then("the transform should return the field value or original message")
|
|
def step_verify_extract_transform(context) -> None:
|
|
assert context.extract_value == "bar"
|
|
assert context.extract_fallback == "raw"
|
|
|
|
|
|
@when("I evaluate equals and field conditions")
|
|
def step_evaluate_conditions(context) -> None:
|
|
context.equals_result = context.stream_router._evaluate_condition(
|
|
StreamMessage(content="yes"), {"equals": "yes"}
|
|
)
|
|
context.field_result = context.stream_router._evaluate_condition(
|
|
{"present": 1}, {"field": "present"}
|
|
)
|
|
|
|
|
|
@then("both condition checks should return true")
|
|
def step_verify_conditions(context) -> None:
|
|
assert context.equals_result is True
|
|
assert context.field_result is True
|
|
|
|
|
|
@when("I apply the append accumulator to a non-list accumulator")
|
|
def step_append_non_list(context) -> None:
|
|
context.append_nonlist_result = context.stream_router._apply_accumulator(
|
|
0, "item", {"op": "append"}
|
|
)
|
|
|
|
|
|
@then("the accumulator should be returned unchanged")
|
|
def step_verify_append_non_list(context) -> None:
|
|
assert context.append_nonlist_result == 0
|
|
|
|
|
|
@given("I prepare stream config with existing and missing subscriptions")
|
|
def step_prepare_subscriptions(context) -> None:
|
|
from rx.subject import Subject
|
|
|
|
context.stream_router.streams["existing"] = Subject()
|
|
context.stream_router.streams["subscriber"] = Subject()
|
|
context.stream_router.stream_configs["existing"] = StreamConfig(name="existing")
|
|
context.stream_router.stream_configs["subscriber"] = StreamConfig(name="subscriber")
|
|
context.subscription_config = StreamConfig(
|
|
name="subscriber", subscriptions=["existing", "missing"]
|
|
)
|
|
|
|
|
|
@when("I setup subscriptions for that config")
|
|
def step_setup_subscriptions(context) -> None:
|
|
context.stream_router._setup_subscriptions(context.subscription_config)
|
|
|
|
|
|
@then("only existing sources should be subscribed")
|
|
def step_verify_subscriptions(context) -> None:
|
|
assert len(context.stream_router.subscriptions) == 1
|