test(langgraph): pin subgraph persistence modes that stay unchanged

Adds controls for the two cases where an empty subgraph read is correct:
a `checkpointer=False` subgraph persists nothing, and a completed subgraph
exposes no task state through `subgraphs=True`. Both pass before and after
the fix, so the hydration change is pinned to the cases it should affect.
This commit is contained in:
Elior Nataf Lackritz
2026-08-05 21:10:03 -04:00
parent 7912a1ce50
commit 99f1e4e962
2 changed files with 42 additions and 2 deletions
+1 -1
View File
@@ -1159,7 +1159,7 @@ class Pregel(
saver: Checkpointer to read with, as resolved by the caller from
`CONFIG_KEY_CHECKPOINTER` before falling back to `self.checkpointer`.
Required rather than defaulted because `self.checkpointer` is `None`
for a subgraph — it borrows the parent's saver through the config
for a subgraph, which borrows the parent's saver through the config,
and a `DeltaChannel` silently hydrates empty without one.
recurse: When set, resolve subgraph task states with this checkpointer
instead of returning a config that merely signals they exist.
@@ -2,7 +2,7 @@
Regression suite for #8470.
A subgraph is compiled without a checkpointer of its own — the parent lends it
A subgraph is compiled without a checkpointer of its own. The parent lends it
one through `CONFIG_KEY_CHECKPOINTER` at read time. `DeltaChannel` stores no
value in `channel_values`, so hydrating it requires that saver to walk ancestors
and replay their writes. Reading with `self.checkpointer` instead of the
@@ -286,3 +286,43 @@ def test_root_graph_update_state_preserves_delta_channel_history() -> None:
app.update_state(config, {"msgs": ["manual"]})
assert app.get_state(config).values == {"msgs": ["a1", "b1", "b2", "manual"]}
def test_stateless_subgraph_persists_nothing() -> None:
"""A `checkpointer=False` subgraph opts out of persistence entirely.
Its state is unavailable by design, and passing the caller's saver must not
start surfacing state for a subgraph that asked not to be checkpointed.
"""
child = _child_builder(snapshot_frequency=1000).compile(checkpointer=False)
builder = StateGraph(child.builder.state_schema)
builder.add_node("child", child)
builder.add_edge(START, "child")
builder.add_edge("child", END)
app = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
app.invoke({}, config)
subgraph_namespaces = [
task.state["configurable"]["checkpoint_ns"]
for snapshot in app.get_state_history(config)
for task in snapshot.tasks
if task.name == "child" and isinstance(task.state, dict)
]
assert subgraph_namespaces == []
assert app.get_state(config).values == {"msgs": ["a1", "b1", "b2"]}
def test_completed_subgraph_exposes_no_task_state() -> None:
"""`subgraphs=True` surfaces task state only while a task is pending.
Once the subgraph has finished there is no task to attach state to, for every
channel type alike. This is subgraph behaviour rather than delta replay, and
the fix leaves it untouched.
"""
app = _build_nested_graph(InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
app.invoke({}, config)
assert app.get_state(config, subgraphs=True).tasks == ()