0d267934a7
CI / push-validation (pull_request) Successful in 38s
CI / helm (pull_request) Successful in 39s
CI / lint (pull_request) Successful in 3m55s
CI / build (pull_request) Successful in 3m50s
CI / typecheck (pull_request) Successful in 4m33s
CI / quality (pull_request) Successful in 4m58s
CI / security (pull_request) Successful in 5m18s
CI / e2e_tests (pull_request) Successful in 8m59s
CI / integration_tests (pull_request) Successful in 10m48s
CI / unit_tests (pull_request) Successful in 11m51s
CI / docker (pull_request) Successful in 1m55s
CI / coverage (pull_request) Successful in 16m37s
CI / status-check (pull_request) Successful in 3s
Replace the no-op on_next handlers in _setup_node_stream_subscriptions with factory-created callbacks that look up and invoke the per-node sync_executor registered by _register_node_executor. The previous code silently discarded every message delivered to node streams. Key changes: - Wire on_next to executor via _make_on_next_handler factory method - Store executors in _node_executors dict (not setattr on stream_router) - Shared ThreadPoolExecutor with proper lifecycle (start/stop/restart) - Prefer run_coroutine_threadsafe when scheduler loop is running; fall back to thread pool with deadlock prevention - Thread-safe StateManager: Lock on all mutation methods, deep-copy emission inside lock for immutable subscriber snapshots - replace_state() and reset() deep-copy input to prevent external mutation bypassing the lock (review cycle 5 M1 fix) - Best-effort cancellation documented on timed-out futures (cycle 5 M2) - CancelledError catch in thread pool path with "graph stopping" message - is_running guard on both sync_executor and execute() entry point - Checkpoint filenames include update_count to prevent collisions - _save_checkpoint docstring warns against calling while _lock is held - on_next handler distinguishes TimeoutError (warning) from other exceptions (exception log) for clearer diagnostics - TODO comment for deferred downstream propagation to successor nodes - __del__ safety net for executor pool cleanup - File I/O outside lock for checkpoint save/load - Bounded execution_history via collections.deque(maxlen=1000) See PR !10795 for the full change log across 5 review cycles. ISSUES CLOSED: #6511
598 lines
24 KiB
Gherkin
598 lines
24 KiB
Gherkin
Feature: Consolidated Langgraph
|
|
Combined scenarios from: langgraph_bridge_coverage, langgraph_dynamic_router_coverage, langgraph_graph_coverage, langgraph_nodes_additional_coverage, langgraph_nodes_coverage, langgraph_nodes_uncovered_lines, langgraph_state_additional_coverage, langgraph_state_branch_coverage, pure_graph_coverage, agent_langgraph_port
|
|
|
|
# ============================================================
|
|
# Originally from: langgraph_bridge_coverage.feature
|
|
# Feature: LangGraph bridge coverage
|
|
# ============================================================
|
|
|
|
Scenario: Cover bridge lifecycle and operators
|
|
Given I set up a langgraph bridge test harness
|
|
When I create a bridge with mocked graph configuration
|
|
And I exercise bridge graph and stream creation
|
|
And I exercise graph executor branches
|
|
And I exercise state utilities and node operator
|
|
And I exercise conditional routing helpers
|
|
And I exercise router node helpers and cleanup
|
|
Then bridge coverage steps should complete successfully
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: langgraph_dynamic_router_coverage.feature
|
|
# Feature: Dynamic Router Coverage
|
|
# ============================================================
|
|
|
|
Scenario: Default route when no rules are provided
|
|
Given a dynamic router with no rules
|
|
When I route the message
|
|
"""
|
|
{"text": "hello"}
|
|
"""
|
|
Then the router should return the default end edge
|
|
|
|
|
|
Scenario: Route to target when no condition is required
|
|
Given a dynamic router with a rule targeting "next" and no condition
|
|
When I route the message
|
|
"""
|
|
{"text": "any"}
|
|
"""
|
|
Then the router should return a single edge to "next"
|
|
|
|
|
|
Scenario: Route to target when condition matches
|
|
Given a dynamic router with a rule targeting "match_target" when message type is "match"
|
|
When I route the message
|
|
"""
|
|
{"type": "match", "text": "payload"}
|
|
"""
|
|
Then the router should return a single edge to "match_target"
|
|
|
|
|
|
Scenario: Fall back to end when condition fails
|
|
Given a dynamic router with a rule targeting "conditional" when message type is "expected"
|
|
When I route the message
|
|
"""
|
|
{"type": "other", "text": "payload"}
|
|
"""
|
|
Then the router should return the default end edge
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: langgraph_graph_coverage.feature
|
|
# Feature: LangGraph uncovered branches
|
|
# ============================================================
|
|
|
|
Scenario: Scheduler falls back when no running loop exists
|
|
Given a langgraph config with only start and end nodes
|
|
And asyncio has no running loop
|
|
When I construct a LangGraph without providing a scheduler
|
|
Then it should create an AsyncIOScheduler and initialize streams
|
|
|
|
|
|
Scenario: Execute dispatches to start stream and returns state
|
|
Given a langgraph instance with a fresh state manager
|
|
When I execute the graph with input data
|
|
Then the start stream should receive the state and execution returns a GraphState
|
|
|
|
|
|
Scenario: Start raises when start stream is missing
|
|
Given a langgraph instance with a removed start stream
|
|
When I call start on the graph
|
|
Then a start stream error should be raised
|
|
|
|
|
|
Scenario: Parallel groups disabled and cycles detected
|
|
Given a langgraph config with parallel execution disabled and a cycle
|
|
When I construct the LangGraph
|
|
Then the graph should report cycles and empty parallel groups
|
|
|
|
|
|
Scenario: Node executor updates state and history
|
|
Given a langgraph instance with a patched state manager
|
|
When I invoke the builtin node executor
|
|
Then it should update state and record execution history
|
|
|
|
|
|
Scenario: Topological levels enumerate layered nodes
|
|
Given a langgraph config with layered nodes
|
|
When I compute the topological levels
|
|
Then the levels should list each layer
|
|
|
|
|
|
Scenario: Execution history getter returns copy
|
|
Given a langgraph instance with recorded history
|
|
When I request execution history
|
|
Then it should return a copy of the recorded history
|
|
|
|
|
|
Scenario: Initialization respects explicit guard nodes
|
|
Given a langgraph config with explicit start and end nodes
|
|
When I construct the LangGraph with that config
|
|
Then initialization should reuse the provided start and end nodes
|
|
|
|
|
|
Scenario: Node subscriptions invoke handlers on stream events
|
|
Given a langgraph instance prepared to observe node streams
|
|
When the node stream emits next error and completion events
|
|
Then the subscription handlers should be exercised
|
|
|
|
|
|
Scenario: Node stream messages are processed by the node executor
|
|
Given a langgraph instance with a mocked node executor
|
|
When a message is emitted on the worker node stream
|
|
Then the registered executor should be called with the message
|
|
|
|
|
|
Scenario: Raw stream values are wrapped before node processing
|
|
Given a langgraph instance with a mocked node executor
|
|
When a raw value is emitted on the worker node stream
|
|
Then the executor should receive a StreamMessage wrapping the raw value
|
|
|
|
|
|
Scenario: Missing executor triggers a diagnostic warning
|
|
Given a langgraph instance with a removed node executor
|
|
When a message is emitted on the executorless worker stream
|
|
Then a warning about missing executor should be logged
|
|
|
|
|
|
Scenario: Each node stream dispatches to its own executor
|
|
Given a langgraph instance with multiple mocked node executors
|
|
When messages are emitted on both alpha and beta node streams
|
|
Then each executor should receive its own message independently
|
|
|
|
|
|
Scenario: Node stream on_next handles executor exceptions gracefully
|
|
Given a langgraph instance with a failing node executor
|
|
When a message is emitted on the failing worker stream
|
|
Then the exception should be caught and logged without propagating
|
|
|
|
|
|
Scenario: Subscription is skipped when observable is missing
|
|
Given a langgraph instance with a removed observable
|
|
When node stream subscriptions are set up
|
|
Then no executor should be called and no error should occur
|
|
|
|
|
|
Scenario: Execute raises when start stream is missing
|
|
Given a langgraph instance with a removed start stream for execute
|
|
When I execute the graph expecting a start stream error
|
|
Then a start stream missing error should be raised from execute
|
|
|
|
|
|
Scenario: Parallel groups computed for converging branches
|
|
Given a langgraph config with converging branches
|
|
When I construct the LangGraph for parallel groups
|
|
Then parallel groups should reflect staged execution order
|
|
|
|
|
|
Scenario: Validation fails when guard nodes missing
|
|
Given a langgraph instance missing required guard nodes
|
|
When I validate the graph explicitly
|
|
Then a guard validation error should be raised
|
|
|
|
|
|
Scenario: Start uses default state and stop resets running flag
|
|
Given a langgraph instance ready to start
|
|
When I start without input then stop and fetch state
|
|
Then it should use default state and update running flags
|
|
|
|
|
|
Scenario: Executor completes via run_coroutine_threadsafe on running loop
|
|
Given a langgraph instance with a scheduler whose loop is running in a background thread
|
|
When a message is emitted on the worker stream via the running loop path
|
|
Then the executor should complete successfully via run_coroutine_threadsafe
|
|
|
|
|
|
Scenario: Execution history is capped at maximum size
|
|
Given a langgraph instance with execution history at capacity
|
|
When one more executor invocation is recorded
|
|
Then the history length should remain at the maximum and the oldest entry should be dropped
|
|
|
|
|
|
Scenario: Concurrent state updates are serialized by the lock
|
|
Given a state manager configured for concurrent access
|
|
When multiple threads perform state updates simultaneously
|
|
Then the final execution count should equal the total number of updates
|
|
|
|
|
|
Scenario: Executor times out via run_coroutine_threadsafe path
|
|
Given a langgraph instance with a slow executor on a background loop and a short timeout
|
|
When the slow executor is invoked via the running loop path
|
|
Then a TimeoutError should be raised with the node name and timeout duration
|
|
|
|
|
|
Scenario: Executor times out via thread pool path
|
|
Given a langgraph instance with a slow executor and a short timeout
|
|
When the slow executor is invoked via the thread pool path
|
|
Then a TimeoutError should be raised from the thread pool path with the node name
|
|
|
|
|
|
Scenario: Graph can be restarted after stop
|
|
Given a langgraph instance that has been started and stopped
|
|
When I restart the graph and invoke an executor
|
|
Then the executor should complete successfully after restart
|
|
|
|
|
|
Scenario: Executor raises when graph is not running
|
|
Given a langgraph instance that is not running
|
|
When I invoke the node executor directly
|
|
Then a RuntimeError should be raised indicating the graph is not running
|
|
|
|
|
|
Scenario: StateManager replace_state atomically replaces and emits deep copy
|
|
Given a state manager with an initial state
|
|
When I call replace_state with a new state
|
|
Then get_state should return the new state
|
|
And the state stream should have received a notification
|
|
And the emitted state should be a deep copy not the same object
|
|
And the emitted state should be distinct from the internal state
|
|
|
|
|
|
Scenario: CancelledError in thread pool path raises descriptive RuntimeError
|
|
Given a langgraph instance with a cancellable executor task
|
|
When the executor future is cancelled before completion
|
|
Then a RuntimeError should be raised indicating graph stopping
|
|
|
|
|
|
Scenario: LangGraph __del__ shuts down executor pool without error
|
|
Given a langgraph instance that was never stopped
|
|
When __del__ is called on the graph
|
|
Then no exception should propagate from __del__
|
|
|
|
|
|
Scenario: Execute raises when graph is not running via execute method
|
|
Given a langgraph instance that is not running for execute
|
|
When I call execute on the non-running graph
|
|
Then a RuntimeError should be raised indicating the graph is not running for execute
|
|
|
|
|
|
Scenario: TimeoutError through stream path logs warning not exception
|
|
Given a langgraph instance with an executor that raises TimeoutError
|
|
When a message is emitted on the timeout worker stream
|
|
Then the timeout should be logged at warning level
|
|
And logger exception should not be called for timeout
|
|
|
|
|
|
Scenario: CancelledError in run_coroutine_threadsafe path raises descriptive RuntimeError
|
|
Given a langgraph instance with a scheduler loop and a cancellable coroutine future
|
|
When the coroutine future is cancelled before completion
|
|
Then a RuntimeError should be raised indicating graph stopping from coroutine path
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: langgraph_nodes_additional_coverage.feature
|
|
# Feature: Additional LangGraph nodes coverage
|
|
# ============================================================
|
|
|
|
Scenario: Conversation history handles empty messages
|
|
Given a node with default history limits
|
|
When I prepare the conversation history for empty input
|
|
Then it should return empty history without truncation
|
|
|
|
|
|
Scenario: Conversation history falls back on invalid limits
|
|
Given a node with invalid history metadata and two messages
|
|
When I prepare the conversation history with invalid limits
|
|
Then it should return full history without truncation
|
|
|
|
|
|
Scenario: Agent node without configured agent returns failure metadata
|
|
Given an agent node without an agent configured
|
|
When I execute the node expecting an agent configuration error
|
|
Then the result should include failed_node and error text
|
|
|
|
|
|
|
|
Scenario: Agent receives empty input when no messages exist
|
|
Given an agent node with no prior messages
|
|
When I execute the agent node with empty history
|
|
Then the agent should receive empty input and track node metadata
|
|
|
|
|
|
Scenario: Agent merges nested metadata context and flags truncation
|
|
Given an agent node with nested metadata context and long history
|
|
When I execute the agent node with nested context
|
|
Then the agent context should merge nested values and mark truncation
|
|
|
|
|
|
Scenario: Agent error path captures processing failures
|
|
Given an agent node whose handler raises an exception
|
|
When I execute the failing agent node
|
|
Then the agent response should contain the error string
|
|
|
|
|
|
Scenario: Function node retries and reports missing function
|
|
Given a function node missing a target with retry policy
|
|
When I execute the function node with retries
|
|
Then it should report failure after retries
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: langgraph_nodes_coverage.feature
|
|
# Feature: LangGraph nodes coverage
|
|
# ============================================================
|
|
|
|
Scenario: Conversation history truncates by message count
|
|
Given a node with history limit 2 messages
|
|
And a conversation history of 3 short messages
|
|
When I prepare the conversation history
|
|
Then it should return 2 messages and indicate truncation
|
|
|
|
|
|
Scenario: Agent node falls back when agent instance is missing
|
|
Given an agent node referencing a missing agent
|
|
When I execute the agent node
|
|
Then it should return a not found assistant message
|
|
|
|
|
|
Scenario: Tool agent uses current_message metadata for input
|
|
Given a tool agent node with a current_message "override" and long history
|
|
When I execute the tool agent node
|
|
Then the tool agent should receive input "override"
|
|
And the response metadata should include last_agent_node
|
|
|
|
|
|
Scenario: Function node runs async function
|
|
Given a function node with an async function
|
|
When I execute the function node
|
|
Then the function result message should be "async-result"
|
|
|
|
|
|
Scenario: Function node awaits coroutine returned from sync function
|
|
Given a function node with a sync function that returns a coroutine
|
|
When I execute the function node
|
|
Then the function result message should be "sync-inner"
|
|
|
|
|
|
Scenario: Conditional node detects assistant field match
|
|
Given a conditional node looking for field "status" equals "OK"
|
|
When I execute the conditional node with matching assistant message
|
|
Then the condition_result should be true and metadata flagged
|
|
|
|
|
|
Scenario: Message router selects rule based on field condition
|
|
Given a message router node with a rule matching field "topic" value "urgent"
|
|
When I execute the message router with current_message containing topic "urgent"
|
|
Then it should route to "urgent_node"
|
|
|
|
|
|
Scenario: Edge condition checks equals against latest message
|
|
Given an edge with equals condition on field "content" value "done"
|
|
When I evaluate the edge condition with matching last message
|
|
Then the edge condition should be true
|
|
And evaluating with no messages should also default to true
|
|
|
|
|
|
Scenario: Eval condition supports equals and field comparisons
|
|
Given I evaluate equals condition on message "abc"
|
|
Then the equals evaluation should return true
|
|
When I evaluate field condition on message dict with foo value "bar"
|
|
Then the field evaluation should return true
|
|
When I evaluate field condition on non-dict message
|
|
Then it should return false
|
|
|
|
|
|
Scenario: Node parallel execution flag reflects configuration
|
|
Given a node configured for parallel execution
|
|
Then can_execute_parallel should return true
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: langgraph_nodes_uncovered_lines.feature
|
|
# Feature: LangGraph nodes uncovered lines
|
|
# ============================================================
|
|
|
|
Scenario: Tool node marks execution
|
|
Given a tool node configured
|
|
When I execute the tool node
|
|
Then it should mark tool executed
|
|
|
|
|
|
Scenario: ToolAgent branch uses current_message for input
|
|
Given a tool agent node using ToolAgent with current_message "override"
|
|
When I execute the tool agent node with ToolAgent branch
|
|
Then the ToolAgent should receive input "override"
|
|
|
|
|
|
Scenario: Agent falls back to last assistant message when no user entries exist
|
|
Given an agent node with only assistant history
|
|
When I execute the agent node for assistant fallback
|
|
Then the agent should receive the assistant fallback content
|
|
|
|
|
|
Scenario: Agent clearing context skips metadata merge
|
|
Given an agent that clears context during processing
|
|
When I execute the clearing agent node
|
|
Then only last_agent_node metadata should remain
|
|
|
|
|
|
Scenario: Agent mutation adds metadata updates
|
|
Given an agent that mutates context to add metadata
|
|
When I execute the mutating agent node
|
|
Then the metadata should include the mutated key
|
|
|
|
|
|
Scenario: Subgraph node reports invocation
|
|
Given a subgraph node named "child_subgraph"
|
|
When I execute the subgraph node
|
|
Then it should report the invoked subgraph "child_subgraph"
|
|
|
|
|
|
Scenario: Start node indicates start
|
|
Given a start node
|
|
When I execute the start node
|
|
Then it should indicate graph start
|
|
|
|
|
|
Scenario: End node indicates completion
|
|
Given an end node
|
|
When I execute the end node
|
|
Then it should indicate graph completion
|
|
|
|
|
|
Scenario: Non-dict tool result leaves base state updates
|
|
Given a tool node patched to return non dict
|
|
When I execute the patched tool node
|
|
Then the execute result should only include current_node
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: langgraph_state_additional_coverage.feature
|
|
# Feature: LangGraph state additional coverage
|
|
# ============================================================
|
|
|
|
Scenario: Merge mode trims messages beyond retention limit
|
|
Given the state management system is available
|
|
Given I have a GraphState starting with 49 messages
|
|
And I have merge updates containing 5 new messages
|
|
When I merge the updates into the state
|
|
Then the message list should be trimmed to 50 messages
|
|
And the newest message should be retained after trimming
|
|
|
|
|
|
Scenario: Append mode trims messages beyond retention limit
|
|
Given the state management system is available
|
|
Given the state management system is available
|
|
Given I have a GraphState starting with 50 messages
|
|
And I have append updates containing 5 new messages
|
|
When I append the updates into the state
|
|
Then the message list should be trimmed to 50 messages after append
|
|
And the newest appended message should be retained after trimming
|
|
|
|
|
|
|
|
Scenario: Time travel history is recorded and trimmed
|
|
Given the state management system is available
|
|
Given I have a StateManager with time travel enabled and history size 2
|
|
When I perform 4 sequential state updates with time travel
|
|
Then only the two most recent history snapshots should remain
|
|
And the earliest snapshots should be discarded
|
|
|
|
|
|
Scenario: Automatic checkpoint is saved at interval
|
|
Given the state management system is available
|
|
Given I have a StateManager with checkpointing enabled and interval 1
|
|
When I perform 2 sequential state updates with checkpointing
|
|
Then a checkpoint file should exist in the directory
|
|
And checkpoint saving should have been triggered
|
|
|
|
|
|
Scenario: Time travel clamps when steps exceed history
|
|
Given the state management system is available
|
|
Given I have a StateManager with time travel history of 2 snapshots
|
|
When I request time travel 5 steps back
|
|
Then time travel should return the earliest available snapshot
|
|
And the returned state should reflect the earliest snapshot
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: langgraph_state_branch_coverage.feature
|
|
# Feature: LangGraph state branch coverage
|
|
# ============================================================
|
|
|
|
Scenario: Replace mode overwrites existing fields
|
|
Given the state management system is available
|
|
Given I have a GraphState with metadata {"original": 1} and current node "old"
|
|
When I replace the state with updates containing metadata {"new": 2} and current node "new"
|
|
Then the state's metadata should equal {"new": 2}
|
|
And the state's current node should equal "new"
|
|
|
|
|
|
Scenario: Merge mode merges metadata dictionaries
|
|
Given the state management system is available
|
|
Given I have a GraphState with metadata {"alpha": 1}
|
|
And I have merge metadata updates {"beta": 2}
|
|
When I merge metadata updates into the state
|
|
Then the state's metadata should contain keys "alpha" and "beta"
|
|
|
|
|
|
Scenario: Append mode appends a single message item
|
|
Given the state management system is available
|
|
Given I have a GraphState with 1 message
|
|
When I append a single message with id 99 into the state
|
|
Then the state's messages should end with id 99
|
|
And the message list length should be 2
|
|
|
|
|
|
Scenario: Save checkpoint without directory is a no-op
|
|
Given the state management system is available
|
|
Given I have a StateManager without checkpointing
|
|
When I invoke checkpoint saving manually
|
|
Then the update count should remain at 0
|
|
|
|
|
|
Scenario: Load checkpoint restores state and counters
|
|
Given the state management system is available
|
|
Given I have a checkpoint file with execution count 7 and metadata {"restored": true}
|
|
When I load the checkpoint into a StateManager
|
|
Then the manager state should include metadata {"restored": true}
|
|
And the manager update count should be 7
|
|
|
|
|
|
Scenario: Latest checkpoint returns None when missing
|
|
Given the state management system is available
|
|
Given I have a StateManager without checkpointing
|
|
When I request the latest checkpoint
|
|
Then no checkpoint path should be returned
|
|
|
|
|
|
Scenario: Latest checkpoint selects the most recent file
|
|
Given the state management system is available
|
|
Given I have a StateManager with two checkpoints written at different times
|
|
When I request the latest checkpoint
|
|
Then the newest checkpoint path should be returned
|
|
|
|
|
|
Scenario: Time travel without history returns None
|
|
Given the state management system is available
|
|
Given I have a StateManager with time travel disabled
|
|
When I request time travel
|
|
Then the returned time travel state should be None
|
|
|
|
|
|
Scenario: Clear history and reset restore initial state
|
|
Given the state management system is available
|
|
Given I have a StateManager with time travel history entries
|
|
When I clear the history and then reset with metadata {"reset": true}
|
|
Then the history should be empty after reset
|
|
And the execution count should be zero after reset
|
|
And the state metadata should include {"reset": true}
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: pure_graph_coverage.feature
|
|
# Feature: PureGraph coverage
|
|
# ============================================================
|
|
|
|
Scenario: Topological order keeps start and end boundaries
|
|
Given a pure graph with nodes "alpha" and "beta"
|
|
When I list its topological order
|
|
Then the order should be start alpha beta end
|
|
|
|
|
|
Scenario: Execute applies registered node functions sequentially
|
|
Given a pure graph with function nodes "double" and "increment"
|
|
And registered functions that double then increment
|
|
When I execute the pure graph starting with 1
|
|
Then the final result should be 3
|
|
And the functions should run in declaration order
|
|
|
|
|
|
Scenario: Execute skips unregistered functions
|
|
Given a pure graph with a missing function node
|
|
When I execute the pure graph starting with 5
|
|
Then the result should remain 5
|
|
|
|
|
|
Scenario: Nodes without functions do not change value
|
|
Given a pure graph with non-functional nodes "alpha" and "beta"
|
|
When I execute the pure graph starting with 7
|
|
Then the result should remain 7
|
|
|
|
|
|
# ============================================================
|
|
# Originally from: agent_langgraph_port.feature
|
|
# Feature: Actor-first port of v2 agent and LangGraph suites
|
|
# ============================================================
|