Skip to content

Commit 2a3909c

Browse files
committed
test(adapters): add integration tests for langchain and llama-index adapters
- Add tests/integration/adapters/test_langchain.py: CoordinodeGraph connect, schema, query, add_graph_documents (upsert nodes, create relationships, idempotency), schema invalidation - Add tests/integration/adapters/test_llama_index.py: CoordinodePropertyGraphStore connect, schema, structured_query, upsert_nodes, upsert_relations, get_triplets, get_rel_map, delete (by id and name) - Fix namespace package declarations in llama_index/__init__.py and llama_index/graph_stores/__init__.py: use pkgutil.extend_path so llama_index.core remains importable when package is installed editable - Add llama-index-core and langchain-community to dev dependencies so adapter tests can be collected and run from the workspace root - 19/19 integration tests pass against localhost:17080 All 19 adapter tests pass. Relates to #14.
1 parent d59e18f commit 2a3909c

7 files changed

Lines changed: 286 additions & 2 deletions

File tree

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
# namespace package
1+
from pkgutil import extend_path
2+
3+
__path__ = extend_path(__path__, __name__)
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
# namespace package
1+
from pkgutil import extend_path
2+
3+
__path__ = extend_path(__path__, __name__)

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ coordinode = { workspace = true }
2828
dev = [
2929
"build>=1.2",
3030
"grpcio-tools>=1.60",
31+
"langchain-community>=0.3",
32+
"langchain-core>=0.3",
33+
"llama-index-core>=0.12",
3134
"pytest>=8",
3235
"pytest-asyncio>=0.23",
3336
"pytest-timeout>=2",

tests/integration/adapters/__init__.py

Whitespace-only changes.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Integration tests for CoordinodeGraph (LangChain adapter).
2+
3+
Requires a running CoordiNode instance. Set COORDINODE_ADDR env var
4+
(default: localhost:7080).
5+
6+
Run via:
7+
COORDINODE_ADDR=localhost:17080 pytest tests/integration/adapters/test_langchain.py -v
8+
"""
9+
10+
import os
11+
import uuid
12+
13+
import pytest
14+
from langchain_community.graphs.graph_document import GraphDocument, Node, Relationship
15+
from langchain_core.documents import Document
16+
17+
from langchain_coordinode import CoordinodeGraph
18+
19+
ADDR = os.environ.get("COORDINODE_ADDR", "localhost:7080")
20+
21+
22+
@pytest.fixture(scope="module")
23+
def graph():
24+
with CoordinodeGraph(ADDR) as g:
25+
yield g
26+
27+
28+
# ── Basic connectivity ────────────────────────────────────────────────────────
29+
30+
def test_connect(graph):
31+
assert graph is not None
32+
33+
34+
def test_schema_returns_string(graph):
35+
schema = graph.schema
36+
assert isinstance(schema, str)
37+
38+
39+
def test_refresh_schema_does_not_raise(graph):
40+
graph.refresh_schema()
41+
assert isinstance(graph.schema, str)
42+
assert isinstance(graph.structured_schema, dict)
43+
assert "node_props" in graph.structured_schema
44+
assert "rel_props" in graph.structured_schema
45+
assert "relationships" in graph.structured_schema
46+
47+
48+
# ── Cypher query ──────────────────────────────────────────────────────────────
49+
50+
def test_query_returns_list(graph):
51+
result = graph.query("RETURN 1 AS n")
52+
assert isinstance(result, list)
53+
assert result[0]["n"] == 1
54+
55+
56+
def test_query_count(graph):
57+
result = graph.query("MATCH (n) RETURN count(n) AS total")
58+
assert isinstance(result, list)
59+
assert isinstance(result[0]["total"], int)
60+
61+
62+
# ── add_graph_documents ───────────────────────────────────────────────────────
63+
64+
@pytest.fixture
65+
def unique_tag():
66+
return uuid.uuid4().hex[:8]
67+
68+
69+
def test_add_graph_documents_upserts_nodes(graph, unique_tag):
70+
node_a = Node(id=f"Alice-{unique_tag}", type="LCPerson", properties={"role": "researcher"})
71+
node_b = Node(id=f"Bob-{unique_tag}", type="LCPerson", properties={"role": "engineer"})
72+
doc = GraphDocument(nodes=[node_a, node_b], relationships=[], source=Document(page_content="test"))
73+
74+
graph.add_graph_documents([doc])
75+
76+
result = graph.query(
77+
"MATCH (n:LCPerson {name: $name}) RETURN n.name AS name",
78+
params={"name": f"Alice-{unique_tag}"},
79+
)
80+
assert len(result) >= 1
81+
assert result[0]["name"] == f"Alice-{unique_tag}"
82+
83+
84+
def test_add_graph_documents_creates_relationship(graph, unique_tag):
85+
node_a = Node(id=f"Charlie-{unique_tag}", type="LCPerson2")
86+
node_b = Node(id=f"GraphRAG-{unique_tag}", type="LCConcept")
87+
rel = Relationship(source=node_a, target=node_b, type="LC_RESEARCHES")
88+
doc = GraphDocument(
89+
nodes=[node_a, node_b],
90+
relationships=[rel],
91+
source=Document(page_content="test"),
92+
)
93+
94+
graph.add_graph_documents([doc])
95+
96+
# Verify both nodes exist
97+
result = graph.query(
98+
"MATCH (n:LCPerson2 {name: $name}) RETURN n.name AS name",
99+
params={"name": f"Charlie-{unique_tag}"},
100+
)
101+
assert len(result) >= 1, f"source node not found: {result}"
102+
103+
104+
def test_add_graph_documents_idempotent(graph, unique_tag):
105+
"""Calling add_graph_documents twice must not raise."""
106+
node = Node(id=f"Idempotent-{unique_tag}", type="LCIdempotent")
107+
doc = GraphDocument(nodes=[node], relationships=[], source=Document(page_content="test"))
108+
109+
graph.add_graph_documents([doc])
110+
graph.add_graph_documents([doc]) # second call must not raise
111+
112+
result = graph.query(
113+
"MATCH (n:LCIdempotent {name: $name}) RETURN count(n) AS cnt",
114+
params={"name": f"Idempotent-{unique_tag}"},
115+
)
116+
assert result[0]["cnt"] >= 1
117+
118+
119+
def test_schema_refreshes_after_add(graph, unique_tag):
120+
"""structured_schema is invalidated and re-fetched after add_graph_documents."""
121+
graph._schema = None # force refresh
122+
schema_before = graph.schema
123+
124+
node = Node(id=f"SchemaNode-{unique_tag}", type="LCSchemaTest")
125+
doc = GraphDocument(nodes=[node], relationships=[], source=Document(page_content="test"))
126+
graph.add_graph_documents([doc])
127+
128+
graph.refresh_schema()
129+
# schema must still be a string after refresh (content depends on server)
130+
assert isinstance(graph.schema, str)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""Integration tests for CoordinodePropertyGraphStore (LlamaIndex adapter).
2+
3+
Requires a running CoordiNode instance. Set COORDINODE_ADDR env var
4+
(default: localhost:7080).
5+
6+
Run via:
7+
COORDINODE_ADDR=localhost:17080 pytest tests/integration/adapters/test_llama_index.py -v
8+
"""
9+
10+
import os
11+
import uuid
12+
13+
import pytest
14+
from llama_index.core.graph_stores.types import EntityNode, Relation
15+
16+
from llama_index.graph_stores.coordinode import CoordinodePropertyGraphStore
17+
18+
ADDR = os.environ.get("COORDINODE_ADDR", "localhost:7080")
19+
20+
21+
@pytest.fixture(scope="module")
22+
def store():
23+
with CoordinodePropertyGraphStore(ADDR) as s:
24+
yield s
25+
26+
27+
@pytest.fixture
28+
def tag():
29+
return uuid.uuid4().hex[:8]
30+
31+
32+
# ── Basic connectivity ────────────────────────────────────────────────────────
33+
34+
def test_connect(store):
35+
assert store is not None
36+
37+
38+
def test_get_schema(store):
39+
schema = store.get_schema()
40+
assert isinstance(schema, str)
41+
42+
43+
def test_structured_query_literal(store):
44+
result = store.structured_query("RETURN 1 AS n")
45+
assert isinstance(result, list)
46+
assert result[0]["n"] == 1
47+
48+
49+
# ── Node operations ───────────────────────────────────────────────────────────
50+
51+
def test_upsert_and_get_nodes(store, tag):
52+
nodes = [
53+
EntityNode(label="LITestPerson", name=f"Alice-{tag}", properties={"role": "researcher"}),
54+
EntityNode(label="LITestConcept", name=f"GraphRAG-{tag}", properties={"field": "AI"}),
55+
]
56+
store.upsert_nodes(nodes)
57+
58+
found = store.get(properties={"name": f"Alice-{tag}"})
59+
assert len(found) >= 1
60+
assert any(getattr(n, "name", None) == f"Alice-{tag}" for n in found)
61+
62+
63+
def test_upsert_nodes_idempotent(store, tag):
64+
"""Upserting the same node twice must not raise and must not duplicate."""
65+
node = EntityNode(label="LIIdempotent", name=f"Idem-{tag}")
66+
store.upsert_nodes([node])
67+
store.upsert_nodes([node]) # second call must not raise
68+
69+
found = store.get(properties={"name": f"Idem-{tag}"})
70+
assert len(found) >= 1
71+
72+
73+
def test_get_by_id(store, tag):
74+
node = EntityNode(label="LIGetById", name=f"ById-{tag}")
75+
node_id = node.id
76+
store.upsert_nodes([node])
77+
78+
found = store.get(ids=[node_id])
79+
assert len(found) >= 1
80+
81+
82+
# ── Relation operations ───────────────────────────────────────────────────────
83+
84+
def test_upsert_and_get_triplets(store, tag):
85+
src = EntityNode(label="LIRelPerson", name=f"Src-{tag}")
86+
dst = EntityNode(label="LIRelConcept", name=f"Dst-{tag}")
87+
store.upsert_nodes([src, dst])
88+
89+
rel = Relation(
90+
label="LI_RESEARCHES",
91+
source_id=src.id,
92+
target_id=dst.id,
93+
properties={"since": 2024},
94+
)
95+
store.upsert_relations([rel])
96+
97+
# CoordiNode does not support wildcard [r] patterns yet — must pass relation_names.
98+
# See: get_triplets() implementation note.
99+
triplets = store.get_triplets(
100+
entity_names=[f"Src-{tag}"],
101+
relation_names=["LI_RESEARCHES"],
102+
)
103+
assert isinstance(triplets, list)
104+
assert len(triplets) >= 1
105+
106+
labels = [t[1].label for t in triplets]
107+
assert "LI_RESEARCHES" in labels
108+
109+
110+
def test_get_rel_map(store, tag):
111+
src = EntityNode(label="LIRelMap", name=f"RMapSrc-{tag}")
112+
dst = EntityNode(label="LIRelMap", name=f"RMapDst-{tag}")
113+
store.upsert_nodes([src, dst])
114+
115+
rel = Relation(label="LI_RELATED", source_id=src.id, target_id=dst.id)
116+
store.upsert_relations([rel])
117+
118+
result = store.get_rel_map([src], depth=1, limit=10)
119+
assert isinstance(result, list)
120+
121+
122+
# ── Delete ────────────────────────────────────────────────────────────────────
123+
124+
def test_delete_by_id(store, tag):
125+
node = EntityNode(label="LIDelete", name=f"Del-{tag}")
126+
store.upsert_nodes([node])
127+
128+
store.delete(ids=[node.id])
129+
130+
found = store.get(ids=[node.id])
131+
assert len(found) == 0
132+
133+
134+
def test_delete_by_entity_name(store, tag):
135+
node = EntityNode(label="LIDeleteByName", name=f"DelNamed-{tag}")
136+
store.upsert_nodes([node])
137+
138+
store.delete(entity_names=[f"DelNamed-{tag}"])
139+
140+
found = store.get(properties={"name": f"DelNamed-{tag}"})
141+
assert len(found) == 0

uv.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)