PlanOpticon

planopticon / tests / test_graph_store.py
Blame History Raw 275 lines
1
"""Tests for graph storage backends."""
2
3
from video_processor.integrators.graph_store import InMemoryStore, SQLiteStore, create_store
4
5
6
class TestInMemoryStore:
7
def test_merge_entity_creates_new(self):
8
store = InMemoryStore()
9
store.merge_entity("Python", "technology", ["A programming language"])
10
assert store.get_entity_count() == 1
11
entity = store.get_entity("python")
12
assert entity is not None
13
assert entity["name"] == "Python"
14
assert entity["type"] == "technology"
15
assert "A programming language" in entity["descriptions"]
16
17
def test_merge_entity_case_insensitive_dedup(self):
18
store = InMemoryStore()
19
store.merge_entity("Python", "technology", ["Language"])
20
store.merge_entity("python", "technology", ["Snake-based"])
21
store.merge_entity("PYTHON", "technology", ["Popular"])
22
assert store.get_entity_count() == 1
23
entity = store.get_entity("Python")
24
assert entity is not None
25
assert "Language" in entity["descriptions"]
26
assert "Snake-based" in entity["descriptions"]
27
assert "Popular" in entity["descriptions"]
28
29
def test_add_occurrence(self):
30
store = InMemoryStore()
31
store.merge_entity("Alice", "person", ["Engineer"])
32
store.add_occurrence("Alice", "transcript_0", timestamp=10.5, text="Alice said...")
33
entity = store.get_entity("alice")
34
assert len(entity["occurrences"]) == 1
35
assert entity["occurrences"][0]["source"] == "transcript_0"
36
assert entity["occurrences"][0]["timestamp"] == 10.5
37
38
def test_add_occurrence_nonexistent_entity(self):
39
store = InMemoryStore()
40
store.add_occurrence("Ghost", "transcript_0")
41
# Should not crash, just no-op
42
assert store.get_entity_count() == 0
43
44
def test_add_relationship(self):
45
store = InMemoryStore()
46
store.merge_entity("Alice", "person", [])
47
store.merge_entity("Bob", "person", [])
48
store.add_relationship("Alice", "Bob", "knows", content_source="t0", timestamp=5.0)
49
assert store.get_relationship_count() == 1
50
rels = store.get_all_relationships()
51
assert rels[0]["source"] == "Alice"
52
assert rels[0]["target"] == "Bob"
53
assert rels[0]["type"] == "knows"
54
55
def test_has_entity(self):
56
store = InMemoryStore()
57
assert not store.has_entity("Python")
58
store.merge_entity("Python", "technology", [])
59
assert store.has_entity("Python")
60
assert store.has_entity("python")
61
assert store.has_entity("PYTHON")
62
63
def test_get_entity_not_found(self):
64
store = InMemoryStore()
65
assert store.get_entity("nonexistent") is None
66
67
def test_get_all_entities(self):
68
store = InMemoryStore()
69
store.merge_entity("Alice", "person", ["Engineer"])
70
store.merge_entity("Bob", "person", ["Manager"])
71
entities = store.get_all_entities()
72
assert len(entities) == 2
73
names = {e["name"] for e in entities}
74
assert names == {"Alice", "Bob"}
75
76
def test_to_dict_format(self):
77
store = InMemoryStore()
78
store.merge_entity("Python", "technology", ["A language"])
79
store.merge_entity("Django", "technology", ["A framework"])
80
store.add_relationship("Django", "Python", "uses")
81
store.add_occurrence("Python", "transcript_0", timestamp=1.0, text="mentioned Python")
82
83
data = store.to_dict()
84
assert "nodes" in data
85
assert "relationships" in data
86
assert len(data["nodes"]) == 2
87
assert len(data["relationships"]) == 1
88
89
# Descriptions should be lists (not sets)
90
for node in data["nodes"]:
91
assert isinstance(node["descriptions"], list)
92
assert "id" in node
93
assert "name" in node
94
assert "type" in node
95
96
def test_to_dict_roundtrip(self):
97
"""Verify to_dict produces data that can reload into a new store."""
98
store = InMemoryStore()
99
store.merge_entity("Alice", "person", ["Engineer"])
100
store.merge_entity("Bob", "person", ["Manager"])
101
store.add_relationship("Alice", "Bob", "reports_to")
102
store.add_occurrence("Alice", "src", timestamp=1.0, text="hello")
103
104
data = store.to_dict()
105
106
# Reload into a new store
107
store2 = InMemoryStore()
108
for node in data["nodes"]:
109
store2.merge_entity(
110
node["name"], node["type"], node["descriptions"], node.get("source")
111
)
112
for occ in node.get("occurrences", []):
113
store2.add_occurrence(
114
node["name"], occ["source"], occ.get("timestamp"), occ.get("text")
115
)
116
for rel in data["relationships"]:
117
store2.add_relationship(
118
rel["source"],
119
rel["target"],
120
rel["type"],
121
rel.get("content_source"),
122
rel.get("timestamp"),
123
)
124
125
assert store2.get_entity_count() == 2
126
assert store2.get_relationship_count() == 1
127
128
def test_empty_store(self):
129
store = InMemoryStore()
130
assert store.get_entity_count() == 0
131
assert store.get_relationship_count() == 0
132
assert store.get_all_entities() == []
133
assert store.get_all_relationships() == []
134
data = store.to_dict()
135
assert data == {"nodes": [], "relationships": []}
136
137
138
class TestCreateStore:
139
def test_returns_in_memory_without_path(self):
140
store = create_store()
141
assert isinstance(store, InMemoryStore)
142
143
def test_returns_in_memory_with_none_path(self):
144
store = create_store(db_path=None)
145
assert isinstance(store, InMemoryStore)
146
147
def test_returns_sqlite_with_path(self, tmp_path):
148
store = create_store(db_path=tmp_path / "test.db")
149
assert isinstance(store, SQLiteStore)
150
store.merge_entity("Test", "concept", ["test entity"])
151
assert store.get_entity_count() == 1
152
store.close()
153
154
155
class TestSQLiteStore:
156
def test_create_and_query_entity(self, tmp_path):
157
store = SQLiteStore(tmp_path / "test.db")
158
store.merge_entity("Python", "technology", ["A language"])
159
assert store.get_entity_count() == 1
160
entity = store.get_entity("python")
161
assert entity is not None
162
assert entity["name"] == "Python"
163
store.close()
164
165
def test_case_insensitive_merge(self, tmp_path):
166
store = SQLiteStore(tmp_path / "test.db")
167
store.merge_entity("Python", "technology", ["Language"])
168
store.merge_entity("python", "technology", ["Snake-based"])
169
assert store.get_entity_count() == 1
170
entity = store.get_entity("python")
171
assert "Language" in entity["descriptions"]
172
assert "Snake-based" in entity["descriptions"]
173
store.close()
174
175
def test_relationships(self, tmp_path):
176
store = SQLiteStore(tmp_path / "test.db")
177
store.merge_entity("Alice", "person", [])
178
store.merge_entity("Bob", "person", [])
179
store.add_relationship("Alice", "Bob", "knows")
180
assert store.get_relationship_count() == 1
181
rels = store.get_all_relationships()
182
assert rels[0]["source"] == "Alice"
183
assert rels[0]["target"] == "Bob"
184
store.close()
185
186
def test_occurrences(self, tmp_path):
187
store = SQLiteStore(tmp_path / "test.db")
188
store.merge_entity("Alice", "person", ["Engineer"])
189
store.add_occurrence("Alice", "transcript_0", timestamp=10.5, text="Alice said...")
190
entity = store.get_entity("alice")
191
assert len(entity["occurrences"]) == 1
192
assert entity["occurrences"][0]["source"] == "transcript_0"
193
store.close()
194
195
def test_occurrence_nonexistent_entity(self, tmp_path):
196
store = SQLiteStore(tmp_path / "test.db")
197
store.add_occurrence("Ghost", "transcript_0")
198
assert store.get_entity_count() == 0
199
store.close()
200
201
def test_persistence(self, tmp_path):
202
db_path = tmp_path / "persist.db"
203
204
store1 = SQLiteStore(db_path)
205
store1.merge_entity("Python", "technology", ["A language"])
206
store1.close()
207
208
store2 = SQLiteStore(db_path)
209
assert store2.get_entity_count() == 1
210
entity = store2.get_entity("python")
211
assert entity["name"] == "Python"
212
store2.close()
213
214
def test_to_dict_format(self, tmp_path):
215
store = SQLiteStore(tmp_path / "test.db")
216
store.merge_entity("Python", "technology", ["A language"])
217
store.merge_entity("Django", "technology", ["A framework"])
218
store.add_relationship("Django", "Python", "uses")
219
220
data = store.to_dict()
221
assert len(data["nodes"]) == 2
222
assert len(data["relationships"]) == 1
223
224
for node in data["nodes"]:
225
assert isinstance(node["descriptions"], list)
226
assert "id" in node
227
assert "name" in node
228
229
store.close()
230
231
def test_has_entity(self, tmp_path):
232
store = SQLiteStore(tmp_path / "test.db")
233
assert not store.has_entity("Python")
234
store.merge_entity("Python", "technology", [])
235
assert store.has_entity("Python")
236
assert store.has_entity("python")
237
store.close()
238
239
def test_raw_query(self, tmp_path):
240
store = SQLiteStore(tmp_path / "test.db")
241
store.merge_entity("Alice", "person", ["Engineer"])
242
rows = store.raw_query("SELECT name FROM entities")
243
assert len(rows) >= 1
244
assert rows[0][0] == "Alice"
245
store.close()
246
247
def test_typed_relationship(self, tmp_path):
248
store = SQLiteStore(tmp_path / "test.db")
249
store.merge_entity("Django", "technology", [])
250
store.merge_entity("Python", "technology", [])
251
store.add_typed_relationship("Django", "Python", "DEPENDS_ON", {"version": "3.10"})
252
rels = store.get_all_relationships()
253
assert len(rels) == 1
254
assert rels[0]["type"] == "DEPENDS_ON"
255
store.close()
256
257
def test_set_entity_properties(self, tmp_path):
258
store = SQLiteStore(tmp_path / "test.db")
259
store.merge_entity("Python", "technology", [])
260
assert store.set_entity_properties("Python", {"version": "3.12", "stable": True})
261
assert not store.set_entity_properties("Ghost", {"key": "val"})
262
store.close()
263
264
def test_has_relationship(self, tmp_path):
265
store = SQLiteStore(tmp_path / "test.db")
266
store.merge_entity("Alice", "person", [])
267
store.merge_entity("Bob", "person", [])
268
store.add_relationship("Alice", "Bob", "knows")
269
assert store.has_relationship("Alice", "Bob")
270
assert store.has_relationship("alice", "bob")
271
assert store.has_relationship("Alice", "Bob", "knows")
272
assert not store.has_relationship("Alice", "Bob", "hates")
273
assert not store.has_relationship("Bob", "Alice")
274
store.close()
275

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button