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