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
158 lines
7.5 KiB
Gherkin
158 lines
7.5 KiB
Gherkin
Feature: Reactive Stream Router Edge Cases
|
|
As an actor orchestration system
|
|
I want the stream router to handle invalid inputs, missing components, and edge cases safely
|
|
So that stream pipelines degrade gracefully and report clear errors
|
|
|
|
# Tool agent operation safety
|
|
|
|
Scenario: Registering an operation with non-alphanumeric characters is rejected
|
|
Given a fresh SimpleToolAgent registry
|
|
When I register an operation named "bad-name!" with a lambda
|
|
Then a ValueError should be raised about operation name format
|
|
|
|
Scenario: A tool agent with a malformed tool entry passes content through unchanged
|
|
Given a SimpleToolAgent whose tools list contains a non-dict entry
|
|
When I process content "hello" through that SimpleToolAgent
|
|
Then the SimpleToolAgent should return "hello" unchanged
|
|
|
|
Scenario: A tool agent with an unrecognised operation name passes content through unchanged
|
|
Given a SimpleToolAgent with an unknown operation "nonexistent_op"
|
|
When I process content "data" through that SimpleToolAgent
|
|
Then the SimpleToolAgent should return "data" unchanged
|
|
|
|
Scenario: A tool agent safely returns empty output when an operation throws
|
|
Given a SimpleToolAgent with an operation that raises an exception
|
|
When I process content "boom" through that SimpleToolAgent
|
|
Then the SimpleToolAgent should return an empty string
|
|
|
|
# Transform registration safety
|
|
|
|
Scenario: Registering a transform with non-alphanumeric characters is rejected
|
|
Given a fresh ReactiveStreamRouter registry
|
|
When I register a transform named "bad@name" with a lambda
|
|
Then a ValueError should be raised about transform name format
|
|
|
|
# Operator construction validation
|
|
|
|
Scenario: A transform operator created without a function or type is rejected
|
|
Given a reactive stream router for coverage
|
|
And a stream named "transform_test" exists on the router
|
|
When I create a transform operator with neither fn nor type
|
|
Then a StreamRoutingError should be raised about missing transform params
|
|
|
|
# LLM agent provider resolution
|
|
|
|
Scenario: An LLM agent resolves its provider without forwarding absent optional parameters
|
|
Given a SimpleLLMAgent with no temperature or max_tokens or max_retries
|
|
And a stub provider registry is active
|
|
When I resolve the LLM on that agent
|
|
Then the LLM should be resolved without optional kwargs
|
|
|
|
Scenario: An LLM agent with no system prompt sends only the user message to the model
|
|
Given a SimpleLLMAgent with an empty system prompt
|
|
And a stub provider registry is active
|
|
When I process content "test input" through that SimpleLLMAgent
|
|
Then the LLM should receive only a HumanMessage
|
|
|
|
# LangGraph bridge delegation
|
|
|
|
Scenario: A graph operator delegates to the bridge factory when the method exists
|
|
Given a reactive stream router for coverage
|
|
And a LangGraph bridge stub with a graph_execute factory is registered
|
|
When I create a graph_execute operator via the bridge
|
|
Then the bridge factory method should produce a valid operator
|
|
|
|
Scenario: A graph operator raises a clear error when the bridge lacks the factory method
|
|
Given a reactive stream router for coverage
|
|
And a LangGraph bridge stub without the requested factory method is registered
|
|
When I attempt to create a graph_execute operator via the bridge
|
|
Then a StreamRoutingError should be raised about LangGraph bridge
|
|
|
|
# Switch operator resilience
|
|
|
|
Scenario: A switch case pipeline tolerates an operator that resolves to nothing
|
|
Given a reactive stream router for coverage
|
|
And a stream named "switch_null_op" exists on the router
|
|
When I run a switch operator whose case pipeline includes a None operator
|
|
Then the switch should still emit the message through the pipeline
|
|
|
|
Scenario: A switch case with a nonexistent target stream falls through to the default
|
|
Given a reactive stream router for coverage
|
|
And a stream named "switch_missing_target" exists on the router
|
|
When I run a switch with a matching case whose target stream does not exist
|
|
Then the switch should fall through to the default
|
|
|
|
Scenario: A switch default pipeline tolerates an operator that resolves to nothing
|
|
Given a reactive stream router for coverage
|
|
And a stream named "switch_default_null" exists on the router
|
|
When I run a switch operator whose default pipeline includes a None operator
|
|
Then the switch should still emit the message through the default pipeline
|
|
|
|
# Message transformation edge cases
|
|
|
|
Scenario: A replace transform on a non-dictionary message passes it through unchanged
|
|
Given a reactive stream router for coverage
|
|
When I apply a replace transform to a non-dict message
|
|
Then the message should be returned unchanged
|
|
|
|
Scenario: An extract-field transform retrieves the named value from a dictionary message
|
|
Given a reactive stream router for coverage
|
|
When I apply an extract_field transform to a dict message with the field
|
|
Then the extracted field value should be returned
|
|
|
|
Scenario: An extract-field transform on a non-dictionary message passes it through
|
|
Given a reactive stream router for coverage
|
|
When I apply an extract_field transform to a non-dict message
|
|
Then the non-dict message should be returned from extract_field
|
|
|
|
Scenario: An unrecognised transform type passes the message through unchanged
|
|
Given a reactive stream router for coverage
|
|
When I apply a transform with unknown type to a message
|
|
Then the original message should be returned
|
|
|
|
# Condition evaluation edge cases
|
|
|
|
Scenario: A field condition matches when the dictionary message contains the field
|
|
Given a reactive stream router for coverage
|
|
When I evaluate a field condition against a dict message containing the field
|
|
Then the condition should return true
|
|
|
|
Scenario: A field condition does not match when the dictionary message lacks the field
|
|
Given a reactive stream router for coverage
|
|
When I evaluate a field condition against a dict message missing the field
|
|
Then the condition should return false
|
|
|
|
Scenario: A field condition does not match against a non-dictionary message
|
|
Given a reactive stream router for coverage
|
|
When I evaluate a field condition against a non-dict message
|
|
Then the condition should return false
|
|
|
|
# Stream splitting and merging edge cases
|
|
|
|
Scenario: Splitting a stream ignores condition targets that do not exist
|
|
Given a reactive stream router for coverage
|
|
And a stream named "split_source" exists on the router
|
|
When I split the stream with a condition targeting a nonexistent stream
|
|
Then no error should occur and the message should not route
|
|
|
|
Scenario: Merging streams into a nonexistent target auto-creates it
|
|
Given a reactive stream router for coverage
|
|
And a stream named "merge_src_a" exists on the router
|
|
And a stream named "merge_src_b" exists on the router
|
|
When I merge streams into a target that does not exist yet
|
|
Then the target stream should be created and receive merged messages
|
|
|
|
Scenario: Merging streams silently skips source streams that do not exist
|
|
Given a reactive stream router for coverage
|
|
And a stream named "merge_src_c" exists on the router
|
|
When I merge streams including a nonexistent source into an existing target
|
|
Then only the existing source should be subscribed
|
|
|
|
# Router disposal
|
|
|
|
Scenario: Disposing the router tolerates streams that lack a dispose method
|
|
Given a reactive stream router for coverage
|
|
And a stream that lacks a dispose method is injected into the router
|
|
When I dispose the router
|
|
Then the router should be fully cleared without errors
|