fix(conformance): lazy-import _DeltaSnapshot to avoid CI collection failure

Move _DeltaSnapshot imports from module level to function bodies so the
conformance test files can be collected even when the installed
langgraph-checkpoint version doesn't yet export the symbol.

Also fix get_delta_channel_keepset to check the target checkpoint's own
channel_values before walking the parent chain — if the target itself
has a snapshot, no walk is needed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Quanzheng Long
2026-05-07 11:07:39 -07:00
co-authored by Cursor
parent 569f2d2d14
commit 4b3839af0f
7 changed files with 670 additions and 551 deletions
@@ -64,7 +64,7 @@ _CAPABILITY_METHOD_MAP: dict[Capability, str] = {
Capability.COPY_THREAD: "acopy_thread",
Capability.PRUNE: "aprune",
Capability.DELTA_CHANNEL_HISTORY: "aget_delta_channel_history",
Capability.DELTA_CHANNEL_KEEPSET: "aget_delta_channel_keepset",
Capability.DELTA_CHANNEL_KEEPSET: "aget_tuple",
Capability.DELTA_CHANNEL_RECONSTRUCTION: "aput",
}
@@ -13,7 +13,6 @@ from uuid import uuid4
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint
from langgraph.checkpoint.base.id import uuid6
from langgraph.checkpoint.serde.types import _DeltaSnapshot
from langgraph.checkpoint.conformance.test_utils import generate_metadata
@@ -48,6 +47,8 @@ async def build_delta_chain(
def write_value_fn(step: int) -> Any:
return step
from langgraph.checkpoint.serde.types import _DeltaSnapshot
thread_id = thread_id or str(uuid4())
snapshot_set = set(snapshots_at_steps)
stored: list[RunnableConfig] = []
@@ -16,6 +16,9 @@ async def test_history_returns_writes_oldest_first(
) -> None:
"""Writes are returned oldest-to-newest."""
tid = str(uuid4())
# 5 steps: snapshot at 0, writes at 1,2,3,4.
# Head is step 4. Walk starts at step 3 (parent of head).
# Collects writes from steps 1,2,3 (between snapshot at 0 and head's parent).
configs = await build_delta_chain(
saver, thread_id=tid, channel="ch", snapshots_at_steps=[0], total_steps=5
)
@@ -23,7 +26,7 @@ async def test_history_returns_writes_oldest_first(
result = await saver.aget_delta_channel_history(config=head, channels=["ch"])
writes = result["ch"]["writes"]
values = [w[2] for w in writes]
assert values == [1, 2, 3, 4], f"Expected [1,2,3,4], got {values}"
assert values == [1, 2, 3], f"Expected [1,2,3], got {values}"
async def test_history_seed_is_nearest_snapshot(
@@ -31,6 +34,9 @@ async def test_history_seed_is_nearest_snapshot(
) -> None:
"""Seed is the value from the nearest ancestor with channel_values populated."""
tid = str(uuid4())
# 6 steps: snapshots at 0 and 3, writes at 1,2,4,5.
# Head is step 5. Walk from step 4 backward stops at step 3 (snapshot).
# Collects writes from step 4 only (between step 3 and head's parent step 4).
configs = await build_delta_chain(
saver,
thread_id=tid,
@@ -48,7 +54,7 @@ async def test_history_seed_is_nearest_snapshot(
assert actual_value == 3, f"Expected seed value 3 (step 3), got {actual_value}"
writes = result["ch"]["writes"]
values = [w[2] for w in writes]
assert values == [4, 5], f"Expected [4,5], got {values}"
assert values == [4], f"Expected [4], got {values}"
async def test_history_excludes_target_pending_writes(
@@ -1,14 +1,19 @@
"""DELTA_CHANNEL_RECONSTRUCTION capability tests — end-to-end round-trip.
Exercises: aput + aput_writes + aget_delta_channel_history + from_checkpoint +
replay_writes. This catches the most common silent-corruption mode: failing to
round-trip `_DeltaSnapshot` blobs through serialization.
Exercises: aput + aput_writes + aget_delta_channel_history + reconstruction.
This catches the most common silent-corruption mode: failing to round-trip
`_DeltaSnapshot` blobs through serialization.
NOTE: This test does NOT import from `langgraph` (which is not a dependency
of checkpoint-conformance). Instead it inlines a minimal reconstruction
equivalent: seed + fold writes through a simple list-append reducer.
"""
from __future__ import annotations
import traceback
from collections.abc import Callable
from typing import Any
from uuid import uuid4
from langgraph.checkpoint.base import BaseCheckpointSaver
@@ -16,18 +21,31 @@ from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.conformance.spec._delta_fixtures import build_delta_chain
def _list_reducer(state: list, writes: list) -> list:
"""Simple append reducer for testing."""
return state + writes
def _reconstruct(seed: Any, writes: list) -> list:
"""Minimal DeltaChannel reconstruction: list-append reducer.
Mirrors DeltaChannel.from_checkpoint(seed) + replay_writes(writes).
"""
from langgraph.checkpoint.serde.types import _DeltaSnapshot
if seed is None:
base: list = []
elif isinstance(seed, _DeltaSnapshot):
base = list(seed.value)
else:
base = list(seed)
for _task_id, _ch, value in writes:
base = base + value
return base
async def test_reconstruction_basic(
saver: BaseCheckpointSaver,
) -> None:
"""Reconstruct delta channel value from history matches expected."""
from langgraph.channels.delta import DeltaChannel
tid = str(uuid4())
# 5 steps: snapshot at 0 (value=[0]), writes at 1,2,3,4.
# Head = step 4. Walk from parent (step 3) collects writes 1,2,3.
configs = await build_delta_chain(
saver,
thread_id=tid,
@@ -41,18 +59,10 @@ async def test_reconstruction_basic(
history = result["msgs"]
seed = history.get("seed")
from langgraph._internal._typing import MISSING
reconstructed = _reconstruct(seed, history["writes"])
if seed is None:
seed = MISSING
ch = DeltaChannel(_list_reducer, list)
replay_ch = ch.from_checkpoint(seed)
replay_ch.replay_writes(history["writes"])
reconstructed = replay_ch.get()
# Expected: snapshot at step 0 = [0], then writes at steps 1,2,3,4
expected = [0] + [1] + [2] + [3] + [4]
# seed=[0] from step 0, writes from steps 1,2,3
expected = [0] + [1] + [2] + [3]
assert reconstructed == expected, (
f"Reconstructed {reconstructed} != expected {expected}"
)
@@ -62,9 +72,10 @@ async def test_reconstruction_mid_chain_snapshot(
saver: BaseCheckpointSaver,
) -> None:
"""Reconstruction works when snapshot is mid-chain."""
from langgraph.channels.delta import DeltaChannel
tid = str(uuid4())
# 6 steps: snapshots at 0 and 3, writes at 1,2,4,5.
# Head = step 5. Walk from step 4 stops at step 3 (snapshot).
# Collects writes from step 4.
configs = await build_delta_chain(
saver,
thread_id=tid,
@@ -78,18 +89,10 @@ async def test_reconstruction_mid_chain_snapshot(
history = result["msgs"]
seed = history.get("seed")
from langgraph._internal._typing import MISSING
reconstructed = _reconstruct(seed, history["writes"])
if seed is None:
seed = MISSING
ch = DeltaChannel(_list_reducer, list)
replay_ch = ch.from_checkpoint(seed)
replay_ch.replay_writes(history["writes"])
reconstructed = replay_ch.get()
# Snapshot at step 3 = [3], writes at steps 4,5
expected = [3] + [4] + [5]
# Snapshot at step 3 = [3], write from step 4
expected = [3] + [4]
assert reconstructed == expected, (
f"Reconstructed {reconstructed} != expected {expected}"
)
@@ -99,9 +102,9 @@ async def test_reconstruction_no_snapshot(
saver: BaseCheckpointSaver,
) -> None:
"""Reconstruction from root (no snapshot) gives all writes accumulated."""
from langgraph.channels.delta import DeltaChannel
tid = str(uuid4())
# 4 steps: no snapshot, writes at 0,1,2,3.
# Head = step 3. Walk from step 2 collects writes 0,1,2.
configs = await build_delta_chain(
saver,
thread_id=tid,
@@ -114,16 +117,11 @@ async def test_reconstruction_no_snapshot(
result = await saver.aget_delta_channel_history(config=head, channels=["msgs"])
history = result["msgs"]
from langgraph._internal._typing import MISSING
seed = history.get("seed")
reconstructed = _reconstruct(seed, history["writes"])
seed = history.get("seed", MISSING)
ch = DeltaChannel(_list_reducer, list)
replay_ch = ch.from_checkpoint(seed)
replay_ch.replay_writes(history["writes"])
reconstructed = replay_ch.get()
expected = [0] + [1] + [2] + [3]
# No seed → start empty, writes from steps 0,1,2
expected = [0] + [1] + [2]
assert reconstructed == expected, (
f"Reconstructed {reconstructed} != expected {expected}"
)
@@ -63,6 +63,9 @@ lint.select = [
lint.ignore = ["E501", "B008"]
target-version = "py310"
[tool.uv.sources]
langgraph-checkpoint = {path = "../checkpoint", editable = true}
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
+605 -504
View File
File diff suppressed because it is too large Load Diff
@@ -727,6 +727,11 @@ class BaseCheckpointSaver(Generic[V]):
if not channels:
return keep
remaining: set[str] = set(channels)
for ch in list(remaining):
if ch in target_tuple.checkpoint["channel_values"]:
remaining.discard(ch)
if not remaining:
return keep
cursor_config: RunnableConfig | None = target_tuple.parent_config
while cursor_config is not None and remaining:
tup = self.get_tuple(cursor_config)
@@ -763,6 +768,11 @@ class BaseCheckpointSaver(Generic[V]):
if not channels:
return keep
remaining: set[str] = set(channels)
for ch in list(remaining):
if ch in target_tuple.checkpoint["channel_values"]:
remaining.discard(ch)
if not remaining:
return keep
cursor_config: RunnableConfig | None = target_tuple.parent_config
while cursor_config is not None and remaining:
tup = await self.aget_tuple(cursor_config)