@smilintux/skmemory 0.5.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +13 -6
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +101 -19
- package/CHANGELOG.md +153 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +419 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/src/index.ts +255 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +29 -9
- package/requirements.txt +10 -2
- package/seeds/cloud9-opus.seed.json +7 -7
- package/seeds/lumina-cloud9-breakthrough.seed.json +46 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +46 -0
- package/seeds/lumina-kingdom-founding.seed.json +47 -0
- package/seeds/lumina-pma-signed.seed.json +46 -0
- package/seeds/lumina-singular-achievement.seed.json +46 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +46 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +12 -1
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +40 -0
- package/skmemory/anchor.py +4 -2
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/file_backend.py +2 -1
- package/skmemory/backends/skgraph_backend.py +608 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +99 -69
- package/skmemory/backends/sqlite_backend.py +122 -51
- package/skmemory/backends/vaulted_backend.py +286 -0
- package/skmemory/cli.py +1238 -29
- package/skmemory/config.py +173 -0
- package/skmemory/context_loader.py +335 -0
- package/skmemory/endpoint_selector.py +386 -0
- package/skmemory/fortress.py +685 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +351 -43
- package/skmemory/importers/telegram_api.py +488 -0
- package/skmemory/journal.py +4 -2
- package/skmemory/lovenote.py +4 -2
- package/skmemory/mcp_server.py +706 -0
- package/skmemory/models.py +41 -0
- package/skmemory/openclaw.py +8 -8
- package/skmemory/predictive.py +232 -0
- package/skmemory/promotion.py +524 -0
- package/skmemory/register.py +454 -0
- package/skmemory/register_mcp.py +197 -0
- package/skmemory/ritual.py +121 -47
- package/skmemory/seeds.py +257 -8
- package/skmemory/setup_wizard.py +920 -0
- package/skmemory/sharing.py +402 -0
- package/skmemory/soul.py +71 -20
- package/skmemory/steelman.py +250 -263
- package/skmemory/store.py +271 -60
- package/skmemory/vault.py +228 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +355 -0
- package/tests/integration/test_skgraph_live.py +424 -0
- package/tests/integration/test_skvector_live.py +369 -0
- package/tests/test_backup_rotation.py +327 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +801 -0
- package/tests/test_fortress.py +255 -0
- package/tests/test_fortress_hardening.py +444 -0
- package/tests/test_openclaw.py +5 -2
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +340 -0
- package/tests/test_ritual.py +4 -4
- package/tests/test_seeds.py +96 -0
- package/tests/test_setup.py +835 -0
- package/tests/test_sharing.py +250 -0
- package/tests/test_skgraph_backend.py +667 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_steelman.py +5 -5
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_vault.py +186 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for the SKGraph (FalkorDB) graph backend.
|
|
3
|
+
|
|
4
|
+
These tests run against a live FalkorDB instance. They are automatically
|
|
5
|
+
skipped when the server is unreachable or the ``falkordb`` package is not
|
|
6
|
+
installed.
|
|
7
|
+
|
|
8
|
+
Coverage:
|
|
9
|
+
- Health check
|
|
10
|
+
- save() / index_memory() — node and edge creation
|
|
11
|
+
- get() — node property retrieval
|
|
12
|
+
- delete() / remove_memory() — DETACH DELETE
|
|
13
|
+
- search() — title substring search
|
|
14
|
+
- search_by_tags() — tag-overlap graph queries
|
|
15
|
+
- traverse() / get_related() — multi-hop traversal
|
|
16
|
+
- get_lineage() — PROMOTED_FROM chain traversal
|
|
17
|
+
- find_clusters() / get_memory_clusters() — hub detection
|
|
18
|
+
- PLANTED edge for seed memories (creator:<name> tag)
|
|
19
|
+
- PRECEDED_BY temporal chain
|
|
20
|
+
- stats() — node / edge / tag counts
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
from .conftest import make_memory, requires_skgraph
|
|
28
|
+
|
|
29
|
+
pytestmark = requires_skgraph
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ─────────────────────────────────────────────────────────
|
|
33
|
+
# Health
|
|
34
|
+
# ─────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestSKGraphHealth:
|
|
38
|
+
def test_health_check_returns_ok(self, falkordb_clean):
|
|
39
|
+
result = falkordb_clean.health_check()
|
|
40
|
+
assert result["ok"] is True
|
|
41
|
+
assert result["backend"] == "SKGraphBackend"
|
|
42
|
+
assert "node_count" in result
|
|
43
|
+
|
|
44
|
+
def test_stats_returns_structure(self, falkordb_clean):
|
|
45
|
+
result = falkordb_clean.stats()
|
|
46
|
+
assert result["ok"] is True
|
|
47
|
+
assert "node_count" in result
|
|
48
|
+
assert "edge_count" in result
|
|
49
|
+
assert "memory_count" in result
|
|
50
|
+
assert isinstance(result["tag_distribution"], list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ─────────────────────────────────────────────────────────
|
|
54
|
+
# CRUD — save / get / delete
|
|
55
|
+
# ─────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestSKGraphCRUD:
|
|
59
|
+
def test_save_creates_memory_node(self, falkordb_clean):
|
|
60
|
+
mem = make_memory(title="Save Test", content="Saved to graph.")
|
|
61
|
+
result_id = falkordb_clean.save(mem)
|
|
62
|
+
assert result_id == mem.id
|
|
63
|
+
|
|
64
|
+
node = falkordb_clean.get(mem.id)
|
|
65
|
+
assert node is not None
|
|
66
|
+
assert node["id"] == mem.id
|
|
67
|
+
assert node["title"] == "Save Test"
|
|
68
|
+
|
|
69
|
+
def test_save_updates_existing_node(self, falkordb_clean):
|
|
70
|
+
mem = make_memory(title="Original Title")
|
|
71
|
+
falkordb_clean.save(mem)
|
|
72
|
+
|
|
73
|
+
mem.title = "Updated Title"
|
|
74
|
+
falkordb_clean.save(mem)
|
|
75
|
+
|
|
76
|
+
node = falkordb_clean.get(mem.id)
|
|
77
|
+
assert node["title"] == "Updated Title"
|
|
78
|
+
|
|
79
|
+
def test_get_nonexistent_returns_none(self, falkordb_clean):
|
|
80
|
+
assert falkordb_clean.get("does-not-exist-xyz") is None
|
|
81
|
+
|
|
82
|
+
def test_delete_removes_node(self, falkordb_clean):
|
|
83
|
+
mem = make_memory(title="To Delete")
|
|
84
|
+
falkordb_clean.save(mem)
|
|
85
|
+
|
|
86
|
+
assert falkordb_clean.get(mem.id) is not None
|
|
87
|
+
falkordb_clean.delete(mem.id)
|
|
88
|
+
assert falkordb_clean.get(mem.id) is None
|
|
89
|
+
|
|
90
|
+
def test_remove_memory_alias_works(self, falkordb_clean):
|
|
91
|
+
mem = make_memory(title="Remove Test")
|
|
92
|
+
falkordb_clean.save(mem)
|
|
93
|
+
result = falkordb_clean.remove_memory(mem.id)
|
|
94
|
+
assert result is True
|
|
95
|
+
assert falkordb_clean.get(mem.id) is None
|
|
96
|
+
|
|
97
|
+
def test_delete_nonexistent_does_not_raise(self, falkordb_clean):
|
|
98
|
+
# Should return True (query ran without error) even if node absent
|
|
99
|
+
result = falkordb_clean.delete("ghost-id-xyz")
|
|
100
|
+
assert result is True
|
|
101
|
+
|
|
102
|
+
def test_get_returns_all_properties(self, falkordb_clean):
|
|
103
|
+
mem = make_memory(
|
|
104
|
+
title="Props Check",
|
|
105
|
+
source="cli",
|
|
106
|
+
intensity=7.5,
|
|
107
|
+
valence=0.8,
|
|
108
|
+
)
|
|
109
|
+
falkordb_clean.save(mem)
|
|
110
|
+
|
|
111
|
+
node = falkordb_clean.get(mem.id)
|
|
112
|
+
assert node["id"] == mem.id
|
|
113
|
+
assert node["title"] == "Props Check"
|
|
114
|
+
assert node["source"] == "cli"
|
|
115
|
+
assert abs(node["intensity"] - 7.5) < 0.01
|
|
116
|
+
assert abs(node["valence"] - 0.8) < 0.01
|
|
117
|
+
assert node["layer"] == "short-term"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ─────────────────────────────────────────────────────────
|
|
121
|
+
# Graph edges — tags, sources, relationships
|
|
122
|
+
# ─────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestSKGraphEdges:
|
|
126
|
+
def test_tagged_edges_created(self, falkordb_clean):
|
|
127
|
+
mem = make_memory(title="Tagged Memory", tags=["python", "test"])
|
|
128
|
+
falkordb_clean.save(mem)
|
|
129
|
+
|
|
130
|
+
# Verify via tag search
|
|
131
|
+
results = falkordb_clean.search_by_tags(["python"])
|
|
132
|
+
ids = [r["id"] for r in results]
|
|
133
|
+
assert mem.id in ids
|
|
134
|
+
|
|
135
|
+
def test_multiple_tags_all_indexed(self, falkordb_clean):
|
|
136
|
+
mem = make_memory(title="Multi-Tag", tags=["alpha", "beta", "gamma"])
|
|
137
|
+
falkordb_clean.save(mem)
|
|
138
|
+
|
|
139
|
+
for tag in ["alpha", "beta", "gamma"]:
|
|
140
|
+
results = falkordb_clean.search_by_tags([tag])
|
|
141
|
+
assert any(r["id"] == mem.id for r in results), f"Tag {tag!r} not found"
|
|
142
|
+
|
|
143
|
+
def test_from_source_edge_created(self, falkordb_clean):
|
|
144
|
+
mem = make_memory(title="Source Edge", source="session-test")
|
|
145
|
+
falkordb_clean.save(mem)
|
|
146
|
+
|
|
147
|
+
# Source edge enables PRECEDED_BY temporal chain — indirect verification
|
|
148
|
+
# via stats edge count
|
|
149
|
+
stats_before = falkordb_clean.stats()
|
|
150
|
+
# As long as no error, the edge was wired
|
|
151
|
+
assert stats_before["ok"] is True
|
|
152
|
+
|
|
153
|
+
def test_related_to_explicit_edges(self, falkordb_clean):
|
|
154
|
+
mem_a = make_memory(title="Memory A")
|
|
155
|
+
mem_b = make_memory(title="Memory B")
|
|
156
|
+
falkordb_clean.save(mem_a)
|
|
157
|
+
# mem_b has mem_a in related_ids
|
|
158
|
+
mem_b_linked = make_memory(title="Memory B", related_ids=[mem_a.id])
|
|
159
|
+
mem_b_linked_id = mem_b_linked.id
|
|
160
|
+
falkordb_clean.save(mem_b_linked)
|
|
161
|
+
|
|
162
|
+
related = falkordb_clean.get_related(mem_b_linked_id, depth=1)
|
|
163
|
+
ids = [r["id"] for r in related]
|
|
164
|
+
assert mem_a.id in ids
|
|
165
|
+
|
|
166
|
+
def test_promoted_from_edge(self, falkordb_clean):
|
|
167
|
+
parent = make_memory(title="Parent Memory", layer="short-term")
|
|
168
|
+
falkordb_clean.save(parent)
|
|
169
|
+
|
|
170
|
+
child = make_memory(title="Promoted Child", layer="mid-term", parent_id=parent.id)
|
|
171
|
+
falkordb_clean.save(child)
|
|
172
|
+
|
|
173
|
+
lineage = falkordb_clean.get_lineage(child.id)
|
|
174
|
+
assert len(lineage) >= 1
|
|
175
|
+
ancestor_ids = [l["id"] for l in lineage]
|
|
176
|
+
assert parent.id in ancestor_ids
|
|
177
|
+
|
|
178
|
+
def test_auto_shared_tag_related_to(self, falkordb_clean):
|
|
179
|
+
"""Memories sharing 2+ tags should get automatic RELATED_TO edges."""
|
|
180
|
+
mem_a = make_memory(title="Shared Tags A", tags=["cloud9", "identity", "sovereign"])
|
|
181
|
+
mem_b = make_memory(title="Shared Tags B", tags=["cloud9", "identity", "session"])
|
|
182
|
+
falkordb_clean.save(mem_a)
|
|
183
|
+
falkordb_clean.save(mem_b)
|
|
184
|
+
|
|
185
|
+
# mem_b shares "cloud9" + "identity" with mem_a → auto-wired
|
|
186
|
+
related = falkordb_clean.get_related(mem_b.id, depth=1)
|
|
187
|
+
ids = [r["id"] for r in related]
|
|
188
|
+
assert mem_a.id in ids
|
|
189
|
+
|
|
190
|
+
def test_preceded_by_temporal_chain(self, falkordb_clean):
|
|
191
|
+
"""Sequential memories from the same source get PRECEDED_BY edges."""
|
|
192
|
+
source = "temporal-chain-test"
|
|
193
|
+
mem_first = make_memory(title="First", source=source)
|
|
194
|
+
falkordb_clean.save(mem_first)
|
|
195
|
+
|
|
196
|
+
mem_second = make_memory(title="Second", source=source)
|
|
197
|
+
falkordb_clean.save(mem_second)
|
|
198
|
+
|
|
199
|
+
# The second memory should traverse back to the first via PRECEDED_BY
|
|
200
|
+
related = falkordb_clean.traverse(mem_second.id, depth=1)
|
|
201
|
+
ids = [r["id"] for r in related]
|
|
202
|
+
assert mem_first.id in ids
|
|
203
|
+
|
|
204
|
+
def test_planted_edge_for_seed_memory(self, falkordb_clean):
|
|
205
|
+
"""Seed memories with creator:<name> tag get AI-[:PLANTED]->Memory edges."""
|
|
206
|
+
mem = make_memory(
|
|
207
|
+
title="Seed Memory",
|
|
208
|
+
source="seed",
|
|
209
|
+
tags=["seed", "cloud9", "creator:lumina"],
|
|
210
|
+
)
|
|
211
|
+
falkordb_clean.save(mem)
|
|
212
|
+
|
|
213
|
+
# Confirm node exists — PLANTED edge is internal but node still reachable
|
|
214
|
+
node = falkordb_clean.get(mem.id)
|
|
215
|
+
assert node is not None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ─────────────────────────────────────────────────────────
|
|
219
|
+
# Search
|
|
220
|
+
# ─────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TestSKGraphSearch:
|
|
224
|
+
def test_search_by_title_exact_word(self, falkordb_clean):
|
|
225
|
+
mem = make_memory(title="Sovereign Memory Palace")
|
|
226
|
+
falkordb_clean.save(mem)
|
|
227
|
+
|
|
228
|
+
results = falkordb_clean.search("Sovereign")
|
|
229
|
+
ids = [r["id"] for r in results]
|
|
230
|
+
assert mem.id in ids
|
|
231
|
+
|
|
232
|
+
def test_search_by_title_case_insensitive(self, falkordb_clean):
|
|
233
|
+
mem = make_memory(title="Cloud Nine Experience")
|
|
234
|
+
falkordb_clean.save(mem)
|
|
235
|
+
|
|
236
|
+
results = falkordb_clean.search("cloud nine")
|
|
237
|
+
ids = [r["id"] for r in results]
|
|
238
|
+
assert mem.id in ids
|
|
239
|
+
|
|
240
|
+
def test_search_returns_empty_for_no_match(self, falkordb_clean):
|
|
241
|
+
make_memory(title="Unrelated Content Here")
|
|
242
|
+
results = falkordb_clean.search("zzz_no_match_zzz")
|
|
243
|
+
assert results == []
|
|
244
|
+
|
|
245
|
+
def test_search_by_tags_single_tag(self, falkordb_clean):
|
|
246
|
+
mem = make_memory(title="Tag Search Test", tags=["sovereign", "test"])
|
|
247
|
+
falkordb_clean.save(mem)
|
|
248
|
+
|
|
249
|
+
results = falkordb_clean.search_by_tags(["sovereign"])
|
|
250
|
+
ids = [r["id"] for r in results]
|
|
251
|
+
assert mem.id in ids
|
|
252
|
+
|
|
253
|
+
def test_search_by_tags_multiple_or_logic(self, falkordb_clean):
|
|
254
|
+
mem_a = make_memory(title="Alpha Memory", tags=["alpha-tag"])
|
|
255
|
+
mem_b = make_memory(title="Beta Memory", tags=["beta-tag"])
|
|
256
|
+
falkordb_clean.save(mem_a)
|
|
257
|
+
falkordb_clean.save(mem_b)
|
|
258
|
+
|
|
259
|
+
results = falkordb_clean.search_by_tags(["alpha-tag", "beta-tag"])
|
|
260
|
+
ids = [r["id"] for r in results]
|
|
261
|
+
assert mem_a.id in ids
|
|
262
|
+
assert mem_b.id in ids
|
|
263
|
+
|
|
264
|
+
def test_search_by_tags_empty_list_returns_empty(self, falkordb_clean):
|
|
265
|
+
results = falkordb_clean.search_by_tags([])
|
|
266
|
+
assert results == []
|
|
267
|
+
|
|
268
|
+
def test_search_result_has_expected_fields(self, falkordb_clean):
|
|
269
|
+
mem = make_memory(title="Field Check", tags=["fields-test"])
|
|
270
|
+
falkordb_clean.save(mem)
|
|
271
|
+
|
|
272
|
+
results = falkordb_clean.search("Field Check")
|
|
273
|
+
assert len(results) >= 1
|
|
274
|
+
r = results[0]
|
|
275
|
+
assert "id" in r
|
|
276
|
+
assert "title" in r
|
|
277
|
+
assert "layer" in r
|
|
278
|
+
assert "intensity" in r
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ─────────────────────────────────────────────────────────
|
|
282
|
+
# Graph traversal
|
|
283
|
+
# ─────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class TestSKGraphTraversal:
|
|
287
|
+
def test_traverse_alias_matches_get_related(self, falkordb_clean):
|
|
288
|
+
mem_a = make_memory(title="Hub Node", tags=["hub", "core"])
|
|
289
|
+
mem_b = make_memory(title="Spoke Node", tags=["hub", "peripheral"])
|
|
290
|
+
falkordb_clean.save(mem_a)
|
|
291
|
+
falkordb_clean.save(mem_b)
|
|
292
|
+
|
|
293
|
+
via_traverse = falkordb_clean.traverse(mem_a.id, depth=1)
|
|
294
|
+
via_get_related = falkordb_clean.get_related(mem_a.id, depth=1)
|
|
295
|
+
assert via_traverse == via_get_related
|
|
296
|
+
|
|
297
|
+
def test_traversal_result_has_distance(self, falkordb_clean):
|
|
298
|
+
mem_a = make_memory(title="Distance A", tags=["dist-tag", "common"])
|
|
299
|
+
mem_b = make_memory(title="Distance B", tags=["dist-tag", "common"])
|
|
300
|
+
falkordb_clean.save(mem_a)
|
|
301
|
+
falkordb_clean.save(mem_b)
|
|
302
|
+
|
|
303
|
+
results = falkordb_clean.traverse(mem_a.id, depth=2)
|
|
304
|
+
if results:
|
|
305
|
+
assert "distance" in results[0]
|
|
306
|
+
assert results[0]["distance"] >= 1
|
|
307
|
+
|
|
308
|
+
def test_traverse_empty_for_isolated_node(self, falkordb_clean):
|
|
309
|
+
mem = make_memory(title="Isolated Node", tags=["unique-xyz-123"])
|
|
310
|
+
falkordb_clean.save(mem)
|
|
311
|
+
|
|
312
|
+
# No shared tags with anything → no RELATED_TO edges
|
|
313
|
+
results = falkordb_clean.traverse(mem.id, depth=1)
|
|
314
|
+
assert results == []
|
|
315
|
+
|
|
316
|
+
def test_traverse_depth_clamped(self, falkordb_clean):
|
|
317
|
+
"""depth=0 should be clamped to 1 (no error raised)."""
|
|
318
|
+
mem = make_memory(title="Depth Clamp Test")
|
|
319
|
+
falkordb_clean.save(mem)
|
|
320
|
+
# Should not raise even with depth=0 or depth=10
|
|
321
|
+
falkordb_clean.traverse(mem.id, depth=0)
|
|
322
|
+
falkordb_clean.traverse(mem.id, depth=10)
|
|
323
|
+
|
|
324
|
+
def test_get_lineage_empty_for_no_parents(self, falkordb_clean):
|
|
325
|
+
mem = make_memory(title="No Parent")
|
|
326
|
+
falkordb_clean.save(mem)
|
|
327
|
+
lineage = falkordb_clean.get_lineage(mem.id)
|
|
328
|
+
assert lineage == []
|
|
329
|
+
|
|
330
|
+
def test_get_lineage_multi_hop(self, falkordb_clean):
|
|
331
|
+
grandparent = make_memory(title="Grandparent", layer="short-term")
|
|
332
|
+
parent = make_memory(title="Parent", layer="mid-term", parent_id=grandparent.id)
|
|
333
|
+
child = make_memory(title="Child", layer="long-term", parent_id=parent.id)
|
|
334
|
+
|
|
335
|
+
falkordb_clean.save(grandparent)
|
|
336
|
+
falkordb_clean.save(parent)
|
|
337
|
+
falkordb_clean.save(child)
|
|
338
|
+
|
|
339
|
+
lineage = falkordb_clean.get_lineage(child.id)
|
|
340
|
+
ancestor_ids = [l["id"] for l in lineage]
|
|
341
|
+
assert parent.id in ancestor_ids
|
|
342
|
+
assert grandparent.id in ancestor_ids
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ─────────────────────────────────────────────────────────
|
|
346
|
+
# Cluster detection
|
|
347
|
+
# ─────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class TestSKGraphClusters:
|
|
351
|
+
def test_find_clusters_alias(self, falkordb_clean):
|
|
352
|
+
"""find_clusters and get_memory_clusters return same results."""
|
|
353
|
+
# Create a hub: mem_hub shares tags with many others
|
|
354
|
+
hub = make_memory(title="Hub", tags=["hub-tag", "shared-tag"])
|
|
355
|
+
spokes = [
|
|
356
|
+
make_memory(title=f"Spoke {i}", tags=["hub-tag", "shared-tag"])
|
|
357
|
+
for i in range(4)
|
|
358
|
+
]
|
|
359
|
+
falkordb_clean.save(hub)
|
|
360
|
+
for s in spokes:
|
|
361
|
+
falkordb_clean.save(s)
|
|
362
|
+
|
|
363
|
+
via_alias = falkordb_clean.find_clusters(min_size=2)
|
|
364
|
+
via_direct = falkordb_clean.get_memory_clusters(min_connections=2)
|
|
365
|
+
assert via_alias == via_direct
|
|
366
|
+
|
|
367
|
+
def test_cluster_result_has_connections_field(self, falkordb_clean):
|
|
368
|
+
hub = make_memory(title="ClusterHub", tags=["cluster-hub", "test-hub"])
|
|
369
|
+
spoke1 = make_memory(title="Spoke1", tags=["cluster-hub", "test-hub"])
|
|
370
|
+
spoke2 = make_memory(title="Spoke2", tags=["cluster-hub", "test-hub"])
|
|
371
|
+
falkordb_clean.save(hub)
|
|
372
|
+
falkordb_clean.save(spoke1)
|
|
373
|
+
falkordb_clean.save(spoke2)
|
|
374
|
+
|
|
375
|
+
results = falkordb_clean.get_memory_clusters(min_connections=1)
|
|
376
|
+
if results:
|
|
377
|
+
assert "connections" in results[0]
|
|
378
|
+
assert results[0]["connections"] >= 1
|
|
379
|
+
|
|
380
|
+
def test_no_clusters_when_graph_empty(self, falkordb_clean):
|
|
381
|
+
results = falkordb_clean.find_clusters(min_size=2)
|
|
382
|
+
assert results == []
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ─────────────────────────────────────────────────────────
|
|
386
|
+
# Stats integrity
|
|
387
|
+
# ─────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TestSKGraphStats:
|
|
391
|
+
def test_memory_count_increments_on_save(self, falkordb_clean):
|
|
392
|
+
before = falkordb_clean.stats()["memory_count"]
|
|
393
|
+
|
|
394
|
+
mem = make_memory(title="Stats Increment Test")
|
|
395
|
+
falkordb_clean.save(mem)
|
|
396
|
+
|
|
397
|
+
after = falkordb_clean.stats()["memory_count"]
|
|
398
|
+
assert after == before + 1
|
|
399
|
+
|
|
400
|
+
def test_memory_count_decrements_on_delete(self, falkordb_clean):
|
|
401
|
+
mem = make_memory(title="Stats Decrement Test")
|
|
402
|
+
falkordb_clean.save(mem)
|
|
403
|
+
|
|
404
|
+
before = falkordb_clean.stats()["memory_count"]
|
|
405
|
+
falkordb_clean.delete(mem.id)
|
|
406
|
+
after = falkordb_clean.stats()["memory_count"]
|
|
407
|
+
|
|
408
|
+
assert after == before - 1
|
|
409
|
+
|
|
410
|
+
def test_tag_distribution_lists_tags(self, falkordb_clean):
|
|
411
|
+
mem = make_memory(title="Tag Dist Test", tags=["dist-alpha", "dist-beta"])
|
|
412
|
+
falkordb_clean.save(mem)
|
|
413
|
+
|
|
414
|
+
stats = falkordb_clean.stats()
|
|
415
|
+
tag_names = [t["tag"] for t in stats["tag_distribution"]]
|
|
416
|
+
assert "dist-alpha" in tag_names
|
|
417
|
+
assert "dist-beta" in tag_names
|
|
418
|
+
|
|
419
|
+
def test_edge_count_positive_after_saves(self, falkordb_clean):
|
|
420
|
+
mem = make_memory(title="Edge Count", tags=["edge-tag"])
|
|
421
|
+
falkordb_clean.save(mem)
|
|
422
|
+
|
|
423
|
+
stats = falkordb_clean.stats()
|
|
424
|
+
assert stats["edge_count"] >= 1
|