@@ -99,3 +99,84 @@ def test_ttl_resets_on_read(short_ttl_saver, client, aerospike_namespace):
9999 # ttl_after would be roughly ttl_before - 1 or less.
100100 # Allowing equality handles coarse timer resolution.
101101 assert ttl_after >= ttl_before , f"Expected ttl_after ({ ttl_after } ) >= ttl_before ({ ttl_before } )"
102+
103+
104+ def test_timeline_refreshes_on_get (short_ttl_saver , client ):
105+ """Verify that calling get_tuple refreshes the timeline record's TTL."""
106+ cfg = {
107+ "configurable" : {
108+ "thread_id" : "timeline-refresh-demo" ,
109+ "checkpoint_ns" : "demo-ns" ,
110+ }
111+ }
112+ checkpoint = {"id" : "ck-timeline-1" , "foo" : "bar" }
113+ metadata = {}
114+
115+ saved_config = short_ttl_saver .put (cfg , checkpoint , metadata , {})
116+
117+ timeline_key = short_ttl_saver ._key_timeline (
118+ cfg ["configurable" ]["thread_id" ], cfg ["configurable" ]["checkpoint_ns" ]
119+ )
120+
121+ # Wait to let TTL decrease
122+ time .sleep (10 )
123+
124+ _ , meta_before , _ = client .get (timeline_key )
125+ ttl_before = meta_before ["ttl" ]
126+
127+ # Call get_tuple to trigger refresh
128+ short_ttl_saver .get_tuple (saved_config )
129+
130+ # Wait a bit to let policy touch and register
131+ time .sleep (1 )
132+
133+ _ , meta_after , _ = client .get (timeline_key )
134+ ttl_after = meta_after ["ttl" ]
135+
136+ assert ttl_after >= ttl_before , (
137+ f"Expected timeline TTL to be refreshed, got { ttl_after } (was { ttl_before } )"
138+ )
139+
140+
141+ def test_timeline_refreshes_on_get_mocked ():
142+ """Verify via mocking that get_tuple refreshes the timeline record's TTL when refresh_on_read is True."""
143+ from unittest .mock import MagicMock
144+
145+ from langgraph .checkpoint .aerospike import AerospikeSaver
146+
147+ # Create mock client
148+ mock_client = MagicMock ()
149+
150+ # Aerospike client.get returns a tuple: (key, meta, bins)
151+ # where bins contains the checkpoint data
152+ checkpoint_bins = {
153+ "cp_type" : "json" ,
154+ "checkpoint" : b'{"id": "ck-1", "ts": "2026-06-26T00:00:00Z"}' ,
155+ "meta_type" : "json" ,
156+ "metadata" : b"{}" ,
157+ }
158+ mock_client .get .side_effect = lambda key , policy = None : (key , {"ttl" : 60 }, checkpoint_bins )
159+
160+ saver = AerospikeSaver (
161+ client = mock_client , namespace = "test" , ttl = {"default_ttl" : 60 , "refresh_on_read" : True }
162+ )
163+
164+ # Call get_tuple
165+ config = {
166+ "configurable" : {"thread_id" : "thread-1" , "checkpoint_ns" : "ns-1" , "checkpoint_id" : "ck-1" }
167+ }
168+
169+ saver .get_tuple (config )
170+
171+ # Verify that client.get was called for the timeline key with the read_touch policy
172+ timeline_key = saver ._key_timeline ("thread-1" , "ns-1" )
173+
174+ # Let's inspect all calls to client.get
175+ get_calls = mock_client .get .call_args_list
176+
177+ # We expect get_calls to contain the timeline key read
178+ timeline_call = next ((call for call in get_calls if call [0 ][0 ] == timeline_key ), None )
179+
180+ assert timeline_call is not None , "Expected client.get to be called on timeline record key"
181+ # The policy should have read_touch_ttl_percent = 100
182+ assert timeline_call [1 ].get ("policy" ) == {"read_touch_ttl_percent" : 100 }
0 commit comments