Skip to content

Commit 3ccdf77

Browse files
committed
test: add regression tests for issue #96 cluster mode AttributeError (#96)
Issue #96 reported AttributeError: 'ClusterPipeline' object has no attribute 'nodes_manager' when using AsyncRedisStore with Redis Cluster. The issue was fixed upstream in redisvl 0.9.0 via redisvl issue #365, which added a safe wrapper around get_protocol_version() to handle ClusterPipeline objects that don't have the nodes_manager attribute. This commit adds comprehensive regression tests to prevent this issue from recurring: - test_async_store_batch_put_no_attribute_error: Tests standalone mode - test_async_store_cluster_mode_batch_put: Tests cluster mode (exact scenario from #96) - test_async_store_large_batch_cluster_mode: Stress tests with 50 items - test_async_store_update_operations_cluster_mode: Tests delete+insert operations All tests pass with redisvl 0.9.1, confirming the fix works correctly. Fixes #96
1 parent 96732e6 commit 3ccdf77

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Tests for issue #96: AttributeError with AsyncRedisStore in cluster mode.
2+
3+
Issue: https://github.com/redis-developer/langgraph-redis/issues/96
4+
5+
The issue was caused by redisvl's get_protocol_version() accessing nodes_manager
6+
attribute on ClusterPipeline objects which don't always have this attribute.
7+
8+
Fixed in redisvl 0.9.0 via issue #365.
9+
10+
This test suite focuses on AsyncRedisStore as that was where the issue was
11+
originally reported. The issue occurred in redisvl's SearchIndex.load() method
12+
which is used by both sync and async stores, so testing async coverage is
13+
sufficient.
14+
"""
15+
16+
import pytest
17+
18+
from langgraph.store.redis import AsyncRedisStore
19+
20+
21+
@pytest.mark.asyncio
22+
async def test_async_store_batch_put_no_attribute_error(redis_url: str) -> None:
23+
"""Test that AsyncRedisStore batch put operations don't raise AttributeError.
24+
25+
This is the primary test for issue #96 which was originally reported
26+
with AsyncRedisStore.
27+
"""
28+
store = AsyncRedisStore(redis_url, cluster_mode=False)
29+
await store.setup()
30+
31+
try:
32+
namespace = ("test", "issue_96_async")
33+
34+
# Put multiple items to trigger batch operations
35+
items = [
36+
(f"async_item_{i}", {"data": f"async_value_{i}", "index": i})
37+
for i in range(10)
38+
]
39+
40+
for key, value in items:
41+
await store.aput(namespace, key, value)
42+
43+
# Verify items were stored correctly
44+
retrieved = await store.aget(namespace, "async_item_0")
45+
assert retrieved is not None
46+
assert retrieved.value["data"] == "async_value_0"
47+
48+
finally:
49+
# Cleanup
50+
for key, _ in items:
51+
await store.adelete(namespace, key)
52+
await store._redis.aclose()
53+
54+
55+
@pytest.mark.asyncio
56+
async def test_async_store_cluster_mode_batch_put(redis_url: str) -> None:
57+
"""Test AsyncRedisStore with cluster_mode=True for batch operations.
58+
59+
This is the exact scenario from issue #96 - using AsyncRedisStore with
60+
cluster mode enabled, which should trigger the code path that was causing
61+
the AttributeError about nodes_manager.
62+
"""
63+
store = AsyncRedisStore(redis_url, cluster_mode=True)
64+
await store.setup()
65+
66+
try:
67+
namespace = ("test", "issue_96_async_cluster")
68+
69+
# Put multiple items to trigger batch operations
70+
items = [
71+
(
72+
f"async_cluster_item_{i}",
73+
{"data": f"async_cluster_value_{i}", "index": i},
74+
)
75+
for i in range(10)
76+
]
77+
78+
# This was raising AttributeError: 'ClusterPipeline' object has no attribute 'nodes_manager'
79+
# Should work now with redisvl 0.9.0
80+
for key, value in items:
81+
await store.aput(namespace, key, value)
82+
83+
# Verify items were stored
84+
retrieved = await store.aget(namespace, "async_cluster_item_0")
85+
assert retrieved is not None
86+
assert retrieved.value["data"] == "async_cluster_value_0"
87+
88+
finally:
89+
# Cleanup
90+
for key, _ in items:
91+
await store.adelete(namespace, key)
92+
await store._redis.aclose()
93+
94+
95+
@pytest.mark.asyncio
96+
async def test_async_store_large_batch_cluster_mode(redis_url: str) -> None:
97+
"""Test AsyncRedisStore with larger batch to stress test the fix.
98+
99+
This ensures the fix works with more substantial batch operations.
100+
"""
101+
store = AsyncRedisStore(redis_url, cluster_mode=True)
102+
await store.setup()
103+
104+
try:
105+
namespace = ("test", "issue_96_large_batch")
106+
107+
# Put a larger batch of items
108+
items = [
109+
(f"large_batch_item_{i}", {"data": f"large_batch_value_{i}", "index": i})
110+
for i in range(50)
111+
]
112+
113+
# This should handle larger batches without AttributeError
114+
for key, value in items:
115+
await store.aput(namespace, key, value)
116+
117+
# Verify some items were stored
118+
retrieved_first = await store.aget(namespace, "large_batch_item_0")
119+
assert retrieved_first is not None
120+
assert retrieved_first.value["index"] == 0
121+
122+
retrieved_last = await store.aget(namespace, "large_batch_item_49")
123+
assert retrieved_last is not None
124+
assert retrieved_last.value["index"] == 49
125+
126+
finally:
127+
# Cleanup
128+
for key, _ in items:
129+
await store.adelete(namespace, key)
130+
await store._redis.aclose()
131+
132+
133+
@pytest.mark.asyncio
134+
async def test_async_store_update_operations_cluster_mode(redis_url: str) -> None:
135+
"""Test AsyncRedisStore update operations in cluster mode.
136+
137+
Updates trigger both delete and insert operations in batch_put_ops,
138+
exercising the code path that was problematic in issue #96.
139+
"""
140+
store = AsyncRedisStore(redis_url, cluster_mode=True)
141+
await store.setup()
142+
143+
try:
144+
namespace = ("test", "issue_96_updates")
145+
146+
# Initial put
147+
await store.aput(namespace, "update_test", {"version": 1, "data": "initial"})
148+
149+
# Update the same key - this triggers delete + insert in batch_put_ops
150+
await store.aput(namespace, "update_test", {"version": 2, "data": "updated"})
151+
152+
# Verify the update worked
153+
retrieved = await store.aget(namespace, "update_test")
154+
assert retrieved is not None
155+
assert retrieved.value["version"] == 2
156+
assert retrieved.value["data"] == "updated"
157+
158+
finally:
159+
# Cleanup
160+
await store.adelete(namespace, "update_test")
161+
await store._redis.aclose()

0 commit comments

Comments
 (0)