11from collections .abc import AsyncIterator , Iterator
2+ import uuid
3+ from typing import Any
24
35import pytest
46import singlestoredb
57from singlestoredb .connection import Connection
8+ from langchain_core .runnables import RunnableConfig
9+ from langgraph .checkpoint .base import (
10+ Checkpoint ,
11+ CheckpointMetadata ,
12+ create_checkpoint ,
13+ empty_checkpoint ,
14+ )
615
716DEFAULT_URI_WITHOUT_DB = "root:test@127.0.0.1:33071"
8- DEFAULT_URI_WITH_DB = "root:test@127.0.0.1:33071/test_db"
17+ TEST_DB_NAME = f"test_db_{ uuid .uuid4 ().hex [:8 ]} "
18+ DEFAULT_URI_WITH_DB = f"root:test@127.0.0.1:33071/{ TEST_DB_NAME } "
919
1020
11- @pytest .fixture (scope = "function" )
21+ @pytest .fixture (scope = "session" , autouse = True )
22+ def setup_test_database ():
23+ """Create test database once for the entire test session."""
24+ # Create unique test database
25+ with singlestoredb .connect (DEFAULT_URI_WITHOUT_DB , autocommit = True , results_type = "dict" ) as conn :
26+ with conn .cursor () as cursor :
27+ cursor .execute (f"CREATE DATABASE IF NOT EXISTS { TEST_DB_NAME } " )
28+
29+ yield
30+
31+ # Clean up test database after all tests
32+ with singlestoredb .connect (DEFAULT_URI_WITHOUT_DB , autocommit = True , results_type = "dict" ) as conn :
33+ with conn .cursor () as cursor :
34+ cursor .execute (f"DROP DATABASE IF EXISTS { TEST_DB_NAME } " )
35+
36+
37+ @pytest .fixture (scope = "class" )
1238def conn () -> Iterator [Connection ]:
13- """Sync connection fixture for SingleStore."""
39+ """Class-scoped sync connection fixture for SingleStore."""
1440 with singlestoredb .connect (DEFAULT_URI_WITH_DB , autocommit = True , results_type = "dict" ) as conn :
1541 yield conn
1642
1743
18- @pytest .fixture (scope = "function " )
44+ @pytest .fixture (scope = "class " )
1945async def aconn () -> AsyncIterator [Connection ]:
20- """Async connection fixture for SingleStore."""
21- async with singlestoredb .connect (DEFAULT_URI_WITH_DB , autocommit = True , results_type = "dict" ) as conn :
46+ """Class-scoped async connection fixture for SingleStore."""
47+ # SingleStore doesn't support async context managers, so we use sync connection
48+ with singlestoredb .connect (DEFAULT_URI_WITH_DB , autocommit = True , results_type = "dict" ) as conn :
2249 yield conn
2350
2451
25- @pytest .fixture (scope = "function " , autouse = True )
52+ @pytest .fixture (scope = "class " , autouse = True )
2653def clear_test_db (conn : Connection ) -> None :
27- """Delete all tables before each test."""
54+ """Delete all tables before each test class."""
55+ try :
56+ with conn .cursor () as cursor :
57+ cursor .execute ("DELETE FROM checkpoints" )
58+ cursor .execute ("DELETE FROM checkpoint_blobs" )
59+ cursor .execute ("DELETE FROM checkpoint_writes" )
60+ cursor .execute ("DELETE FROM checkpoint_migrations" )
61+ except Exception :
62+ # Tables might not exist yet
63+ pass
64+
65+
66+ @pytest .fixture (scope = "function" , autouse = True )
67+ def clear_test_data (conn : Connection ) -> None :
68+ """Clear test data before each test to ensure isolation."""
2869 try :
2970 with conn .cursor () as cursor :
3071 cursor .execute ("DELETE FROM checkpoints" )
@@ -34,3 +75,88 @@ def clear_test_db(conn: Connection) -> None:
3475 except Exception :
3576 # Tables might not exist yet
3677 pass
78+
79+
80+ @pytest .fixture (scope = "class" )
81+ def sync_saver (conn : Connection ):
82+ """Class-scoped sync saver fixture."""
83+ from langgraph .checkpoint .singlestore import SingleStoreSaver
84+
85+ saver = SingleStoreSaver (conn )
86+ try :
87+ saver .setup ()
88+ except Exception as e :
89+ # Ignore duplicate index errors since we're reusing the database
90+ if "Duplicate key name" not in str (e ):
91+ raise
92+ return saver
93+
94+
95+ @pytest .fixture (scope = "class" )
96+ async def async_saver (aconn : Connection ):
97+ """Class-scoped async saver fixture."""
98+ from langgraph .checkpoint .singlestore .aio import AsyncSingleStoreSaver
99+
100+ saver = AsyncSingleStoreSaver (aconn )
101+ try :
102+ await saver .setup ()
103+ except Exception as e :
104+ # Ignore duplicate index errors since we're reusing the database
105+ if "Duplicate key name" not in str (e ):
106+ raise
107+ return saver
108+
109+
110+ @pytest .fixture (scope = "function" )
111+ def test_data ():
112+ """Function-scoped fixture providing test data for checkpoint tests."""
113+ import uuid
114+
115+ # Generate unique identifiers to prevent test conflicts
116+ unique_id = uuid .uuid4 ().hex [:8 ]
117+
118+ config_1 : RunnableConfig = {
119+ "configurable" : {
120+ "thread_id" : f"thread-1-{ unique_id } " ,
121+ "checkpoint_id" : "1" ,
122+ "checkpoint_ns" : "" ,
123+ }
124+ }
125+ config_2 : RunnableConfig = {
126+ "configurable" : {
127+ "thread_id" : f"thread-2-{ unique_id } " ,
128+ "checkpoint_id" : "2" ,
129+ "checkpoint_ns" : "" ,
130+ }
131+ }
132+ config_3 : RunnableConfig = {
133+ "configurable" : {
134+ "thread_id" : f"thread-2-{ unique_id } " ,
135+ "checkpoint_id" : "2-inner" ,
136+ "checkpoint_ns" : "inner" ,
137+ }
138+ }
139+
140+ chkpnt_1 : Checkpoint = empty_checkpoint ()
141+ chkpnt_2 : Checkpoint = create_checkpoint (chkpnt_1 , {}, 1 )
142+ chkpnt_3 : Checkpoint = empty_checkpoint ()
143+
144+ metadata_1 : CheckpointMetadata = {
145+ "source" : "input" ,
146+ "step" : 2 ,
147+ "writes" : {},
148+ "score" : 1 ,
149+ }
150+ metadata_2 : CheckpointMetadata = {
151+ "source" : "loop" ,
152+ "step" : 1 ,
153+ "writes" : {"foo" : "bar" },
154+ "score" : None ,
155+ }
156+ metadata_3 : CheckpointMetadata = {}
157+
158+ return {
159+ "configs" : [config_1 , config_2 , config_3 ],
160+ "checkpoints" : [chkpnt_1 , chkpnt_2 , chkpnt_3 ],
161+ "metadata" : [metadata_1 , metadata_2 , metadata_3 ],
162+ }
0 commit comments