Files
cleveragents-core/features/consolidated_langgraph.feature
hurui200320 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
fix(langgraph): wire node stream on_next handlers to registered executors
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
2026-04-21 05:14:07 +00:00

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
# ============================================================