From 6b3d0dc3141d9a3537c7c6bf36f76cb5c4df78b2 Mon Sep 17 00:00:00 2001 From: Elior Nataf Lackritz Date: Thu, 6 Aug 2026 11:36:52 -0400 Subject: [PATCH] test(langgraph): compare read-back checkpoints in the immutability saver MemorySaverAssertImmutable recorded the checkpoint object handed to put, then compared it against one read back through get. Those two are not the same shape: channel_values are stored per (channel, version), so a channel a step did not write is refilled from the blob its inherited version still points at. Every channel except DeltaChannel writes its value into channel_values on every checkpoint, so the two agreed by accident. A DeltaChannel stores nothing except at a snapshot, so once one snapshots and a later step does not write it, the saver reports a checkpoint that changed after it was written when nothing was mutated. Reproducible on main with no fork involved: a delta channel with snapshot_frequency=1 written by the first node and left alone by the next two trips the assertion. Existing delta tests miss it only because they all use snapshot_frequency=1000. Record what the saver reads back instead. Comparing read-back against read-back still catches a checkpoint whose stored data really changed. --- libs/langgraph/tests/memory_assert.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/libs/langgraph/tests/memory_assert.py b/libs/langgraph/tests/memory_assert.py index d9ca4904c..74de903dd 100644 --- a/libs/langgraph/tests/memory_assert.py +++ b/libs/langgraph/tests/memory_assert.py @@ -86,11 +86,19 @@ class MemorySaverAssertImmutable(InMemorySaver): ) == saved ), config["configurable"]["checkpoint_ns"] - self.storage_for_copies[thread_id][checkpoint_ns][checkpoint["id"]] = ( - self.serde.dumps_typed(checkpoint) - ) # call super to write checkpoint - return super().put(config, checkpoint, metadata, new_versions) + next_config = super().put(config, checkpoint, metadata, new_versions) + # Record the checkpoint as the saver reads it back, not the object it + # was handed. `channel_values` are stored per (channel, version), so a + # channel a step did not write is refilled from the blob its inherited + # version still points at. A `DeltaChannel` omits its value except at a + # snapshot, which makes the two representations differ for reasons that + # are not mutation. Comparing read-back against read-back still catches + # a checkpoint whose stored data actually changed. + self.storage_for_copies[thread_id][checkpoint_ns][checkpoint["id"]] = ( + self.serde.dumps_typed(super().get(next_config)) + ) + return next_config class MemorySaverNoPending(InMemorySaver):