@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,441 @@
1
+ """Tests for Memory Fortress hardening — Sprint 6 Layer 3.
2
+
3
+ Covers:
4
+ - VaultedSQLiteBackend: transparent AES-256-GCM at-rest encryption
5
+ - FortifiedMemoryStore with vault_passphrase
6
+ - seal_all / unseal_all / vault_status
7
+ - Mixed (partially-encrypted) store migration
8
+ - CLI fortress and vault commands
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ try:
19
+ import cryptography.hazmat.primitives.ciphers.aead # noqa: F401
20
+
21
+ CRYPTO_AVAILABLE = True
22
+ except ImportError:
23
+ CRYPTO_AVAILABLE = False
24
+
25
+ from skmemory.vault import VAULT_HEADER
26
+
27
+ pytestmark = pytest.mark.skipif(
28
+ not CRYPTO_AVAILABLE,
29
+ reason="cryptography package not installed",
30
+ )
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Fixtures
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ @pytest.fixture
39
+ def vaulted_backend(tmp_path):
40
+ """A VaultedSQLiteBackend with a temp directory."""
41
+ from skmemory.backends.vaulted_backend import VaultedSQLiteBackend
42
+
43
+ return VaultedSQLiteBackend(
44
+ passphrase="pengu-nation-test-key", base_path=str(tmp_path / "memories")
45
+ )
46
+
47
+
48
+ @pytest.fixture
49
+ def plain_backend(tmp_path):
50
+ """A plain SQLiteBackend for comparison / migration tests."""
51
+ from skmemory.backends.sqlite_backend import SQLiteBackend
52
+
53
+ return SQLiteBackend(base_path=str(tmp_path / "memories"))
54
+
55
+
56
+ @pytest.fixture
57
+ def fortress_vaulted(tmp_path):
58
+ """A FortifiedMemoryStore backed by VaultedSQLiteBackend."""
59
+ from skmemory.fortress import FortifiedMemoryStore
60
+
61
+ return FortifiedMemoryStore(
62
+ vault_passphrase="pengu-test-passphrase",
63
+ audit_path=tmp_path / "audit.jsonl",
64
+ use_sqlite=False,
65
+ base_path=str(tmp_path / "memories"),
66
+ )
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # VaultedSQLiteBackend tests
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ class TestVaultedSQLiteBackend:
75
+ def test_save_produces_encrypted_file(self, vaulted_backend, tmp_path):
76
+ """Saved memory files must start with the SKMV1 vault header."""
77
+ from skmemory.models import Memory
78
+
79
+ mem = Memory(title="Secret", content="Classified info")
80
+ mem.seal()
81
+ vaulted_backend.save(mem)
82
+
83
+ # Find the written file
84
+ files = list((tmp_path / "memories").rglob("*.json"))
85
+ assert len(files) == 1, "Expected exactly one memory file"
86
+ raw = files[0].read_bytes()
87
+ assert raw[: len(VAULT_HEADER)] == VAULT_HEADER, "File must be vault-encrypted"
88
+
89
+ def test_load_roundtrip(self, vaulted_backend):
90
+ """Save then load should return the original memory content."""
91
+ from skmemory.models import Memory
92
+
93
+ mem = Memory(title="Trip Memory", content="The roundtrip works perfectly")
94
+ mem.seal()
95
+ vaulted_backend.save(mem)
96
+
97
+ loaded = vaulted_backend.load(mem.id)
98
+ assert loaded is not None
99
+ assert loaded.title == "Trip Memory"
100
+ assert loaded.content == "The roundtrip works perfectly"
101
+
102
+ def test_list_memories_with_encryption(self, vaulted_backend):
103
+ """list_memories should decrypt transparently."""
104
+ from skmemory.models import Memory
105
+
106
+ for i in range(3):
107
+ mem = Memory(title=f"Memory {i}", content=f"Content {i}")
108
+ mem.seal()
109
+ vaulted_backend.save(mem)
110
+
111
+ results = vaulted_backend.list_memories(limit=10)
112
+ assert len(results) == 3
113
+ titles = {m.title for m in results}
114
+ assert titles == {"Memory 0", "Memory 1", "Memory 2"}
115
+
116
+ def test_reindex_with_encrypted_files(self, vaulted_backend):
117
+ """reindex() should correctly parse encrypted files."""
118
+ from skmemory.models import Memory
119
+
120
+ for i in range(4):
121
+ mem = Memory(title=f"Reindex {i}", content=f"Data {i}")
122
+ mem.seal()
123
+ vaulted_backend.save(mem)
124
+
125
+ count = vaulted_backend.reindex()
126
+ assert count == 4
127
+
128
+ def test_seal_all_encrypts_plaintext(self, tmp_path):
129
+ """seal_all() should encrypt any plaintext JSON files."""
130
+ from skmemory.backends.sqlite_backend import SQLiteBackend
131
+ from skmemory.backends.vaulted_backend import VaultedSQLiteBackend
132
+ from skmemory.models import Memory
133
+
134
+ mem_path = tmp_path / "memories"
135
+
136
+ # First write plaintext via plain backend
137
+ plain = SQLiteBackend(base_path=str(mem_path))
138
+ for i in range(3):
139
+ mem = Memory(title=f"Plain {i}", content=f"Unencrypted {i}")
140
+ mem.seal()
141
+ plain.save(mem)
142
+ plain.close()
143
+
144
+ # Now create vaulted backend and seal_all
145
+ vaulted = VaultedSQLiteBackend(passphrase="seal-test", base_path=str(mem_path))
146
+ count = vaulted.seal_all()
147
+ assert count == 3
148
+
149
+ # All files should now have vault header
150
+ for json_file in mem_path.rglob("*.json"):
151
+ raw = json_file.read_bytes()
152
+ assert raw[: len(VAULT_HEADER)] == VAULT_HEADER, f"{json_file} not encrypted"
153
+
154
+ def test_seal_all_idempotent(self, vaulted_backend):
155
+ """seal_all() on an already-encrypted store should encrypt 0 files."""
156
+ from skmemory.models import Memory
157
+
158
+ mem = Memory(title="Already sealed", content="content")
159
+ mem.seal()
160
+ vaulted_backend.save(mem)
161
+
162
+ count = vaulted_backend.seal_all()
163
+ assert count == 0, "Re-sealing should skip already-encrypted files"
164
+
165
+ def test_unseal_all_decrypts(self, tmp_path):
166
+ """unseal_all() should decrypt all vault files back to plaintext JSON."""
167
+ from skmemory.backends.vaulted_backend import VaultedSQLiteBackend
168
+ from skmemory.models import Memory
169
+
170
+ mem_path = tmp_path / "memories"
171
+ vaulted = VaultedSQLiteBackend(passphrase="unseal-test", base_path=str(mem_path))
172
+
173
+ for i in range(2):
174
+ mem = Memory(title=f"Sealed {i}", content=f"Encrypted {i}")
175
+ mem.seal()
176
+ vaulted.save(mem)
177
+
178
+ count = vaulted.unseal_all()
179
+ assert count == 2
180
+
181
+ # Files should now be valid JSON (not encrypted)
182
+ for json_file in mem_path.rglob("*.json"):
183
+ raw = json_file.read_bytes()
184
+ assert raw[: len(VAULT_HEADER)] != VAULT_HEADER, f"{json_file} still encrypted"
185
+ parsed = json.loads(raw.decode("utf-8"))
186
+ assert "title" in parsed
187
+
188
+ def test_vault_status_all_encrypted(self, vaulted_backend):
189
+ """vault_status() should report 100% coverage when all files are encrypted."""
190
+ from skmemory.models import Memory
191
+
192
+ for i in range(3):
193
+ mem = Memory(title=f"Status {i}", content=f"Data {i}")
194
+ mem.seal()
195
+ vaulted_backend.save(mem)
196
+
197
+ status = vaulted_backend.vault_status()
198
+ assert status["total"] == 3
199
+ assert status["encrypted"] == 3
200
+ assert status["plaintext"] == 0
201
+ assert status["coverage_pct"] == 100.0
202
+
203
+ def test_vault_status_empty_store(self, vaulted_backend):
204
+ """vault_status() on an empty store should report 100% (trivially)."""
205
+ status = vaulted_backend.vault_status()
206
+ assert status["total"] == 0
207
+ assert status["coverage_pct"] == 100.0
208
+
209
+ def test_wrong_passphrase_fails_load(self, tmp_path):
210
+ """Loading with wrong passphrase should return None (graceful failure)."""
211
+ from skmemory.backends.vaulted_backend import VaultedSQLiteBackend
212
+ from skmemory.models import Memory
213
+
214
+ mem_path = tmp_path / "memories"
215
+ correct = VaultedSQLiteBackend(passphrase="correct-key", base_path=str(mem_path))
216
+ mem = Memory(title="Locked", content="Top secret")
217
+ mem.seal()
218
+ correct.save(mem)
219
+
220
+ wrong = VaultedSQLiteBackend(passphrase="wrong-key", base_path=str(mem_path))
221
+ result = wrong.load(mem.id)
222
+ assert result is None, "Wrong passphrase should return None, not raise"
223
+
224
+ def test_export_all_decrypts(self, vaulted_backend, tmp_path):
225
+ """export_all() should produce a plaintext JSON backup."""
226
+ from skmemory.models import Memory
227
+
228
+ mem = Memory(title="Export Test", content="Exportable content")
229
+ mem.seal()
230
+ vaulted_backend.save(mem)
231
+
232
+ backup_path = str(tmp_path / "backup.json")
233
+ out_path = vaulted_backend.export_all(output_path=backup_path)
234
+
235
+ backup = json.loads(Path(out_path).read_text())
236
+ assert backup["memory_count"] == 1
237
+ assert backup["memories"][0]["title"] == "Export Test"
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # FortifiedMemoryStore + vault_passphrase integration
242
+ # ---------------------------------------------------------------------------
243
+
244
+
245
+ class TestFortifiedMemoryStoreVault:
246
+ def test_vault_passphrase_activates_encryption(self, fortress_vaulted, tmp_path):
247
+ """FortifiedMemoryStore with vault_passphrase should encrypt files."""
248
+ fortress_vaulted.snapshot("Vaulted Title", "Encrypted content")
249
+
250
+ # Find the physical file
251
+ base = fortress_vaulted.primary.base_path
252
+ files = list(base.rglob("*.json"))
253
+ assert len(files) == 1
254
+ raw = files[0].read_bytes()
255
+ assert raw[: len(VAULT_HEADER)] == VAULT_HEADER, "File should be vault-encrypted"
256
+
257
+ def test_vault_active_property(self, fortress_vaulted):
258
+ """vault_active should be True when vault_passphrase is set."""
259
+ assert fortress_vaulted.vault_active is True
260
+
261
+ def test_vault_active_false_by_default(self, tmp_path):
262
+ """vault_active should be False without vault_passphrase."""
263
+ from skmemory.backends.sqlite_backend import SQLiteBackend
264
+ from skmemory.fortress import FortifiedMemoryStore
265
+
266
+ backend = SQLiteBackend(base_path=str(tmp_path / "memories"))
267
+ fortress = FortifiedMemoryStore(
268
+ primary=backend,
269
+ use_sqlite=False,
270
+ audit_path=tmp_path / "audit.jsonl",
271
+ )
272
+ assert fortress.vault_active is False
273
+
274
+ def test_recall_after_vault_store(self, fortress_vaulted):
275
+ """recall() should transparently decrypt and verify integrity."""
276
+ mem = fortress_vaulted.snapshot("Recall Test", "Content for recall")
277
+ recalled = fortress_vaulted.recall(mem.id)
278
+
279
+ assert recalled is not None
280
+ assert recalled.title == "Recall Test"
281
+ assert recalled.content == "Content for recall"
282
+ assert "integrity_warning" not in recalled.metadata
283
+
284
+ def test_tamper_alert_on_encrypted_store(self, fortress_vaulted, tmp_path):
285
+ """Tamper alert should fire even when vault is active."""
286
+ alerts = []
287
+ fortress_vaulted.register_alert_callback(alerts.append)
288
+
289
+ mem = fortress_vaulted.snapshot("Tamper Me", "Original")
290
+
291
+ # Directly corrupt the stored file (bypass encryption by writing junk)
292
+ raw = fortress_vaulted.primary.load(mem.id)
293
+ raw.content = "TAMPERED"
294
+ raw.integrity_hash = mem.integrity_hash # old hash
295
+ fortress_vaulted.primary.save(raw)
296
+
297
+ recalled = fortress_vaulted.recall(mem.id)
298
+ assert recalled is not None
299
+ assert "integrity_warning" in recalled.metadata
300
+ assert len(alerts) == 1
301
+
302
+ def test_vault_status_method(self, fortress_vaulted):
303
+ """vault_status() on FortifiedMemoryStore should report coverage."""
304
+ fortress_vaulted.snapshot("Coverage Test", "data")
305
+ status = fortress_vaulted.vault_status()
306
+ assert status["total"] == 1
307
+ assert status["encrypted"] == 1
308
+ assert status["coverage_pct"] == 100.0
309
+
310
+ def test_vault_status_raises_without_vault(self, tmp_path):
311
+ """vault_status() raises when no vault is configured."""
312
+ from skmemory.backends.sqlite_backend import SQLiteBackend
313
+ from skmemory.fortress import FortifiedMemoryStore
314
+
315
+ backend = SQLiteBackend(base_path=str(tmp_path / "memories"))
316
+ fortress = FortifiedMemoryStore(
317
+ primary=backend,
318
+ use_sqlite=False,
319
+ audit_path=tmp_path / "audit.jsonl",
320
+ )
321
+ with pytest.raises(RuntimeError, match="vault_passphrase"):
322
+ fortress.vault_status()
323
+
324
+ def test_seal_vault_audited(self, fortress_vaulted):
325
+ """seal_vault() should append an audit record."""
326
+ fortress_vaulted.snapshot("Pre-existing", "data")
327
+ fortress_vaulted.seal_vault()
328
+
329
+ trail = fortress_vaulted.audit_trail(10)
330
+ ops = [r["op"] for r in trail]
331
+ assert "vault_seal" in ops
332
+
333
+ def test_unseal_vault(self, fortress_vaulted, tmp_path):
334
+ """unseal_vault() should decrypt all files and audit the action."""
335
+ fortress_vaulted.snapshot("Will unseal", "data")
336
+ count = fortress_vaulted.unseal_vault()
337
+ assert count >= 0 # Unseal ran without error
338
+
339
+ trail = fortress_vaulted.audit_trail(10)
340
+ ops = [r["op"] for r in trail]
341
+ assert "vault_unseal" in ops
342
+
343
+ def test_verify_all_with_vault(self, fortress_vaulted):
344
+ """verify_all() should work correctly on an encrypted store."""
345
+ for i in range(3):
346
+ fortress_vaulted.snapshot(f"Verify {i}", f"Content {i}")
347
+
348
+ result = fortress_vaulted.verify_all()
349
+ assert result["total"] == 3
350
+ assert result["passed"] == 3
351
+ assert result["tampered"] == []
352
+
353
+
354
+ # ---------------------------------------------------------------------------
355
+ # CLI integration tests
356
+ # ---------------------------------------------------------------------------
357
+
358
+
359
+ class TestFortressCLI:
360
+ def test_fortress_verify_clean(self, tmp_path):
361
+ """skmemory fortress verify should exit 0 for a clean store."""
362
+ from click.testing import CliRunner
363
+
364
+ from skmemory.backends.sqlite_backend import SQLiteBackend
365
+ from skmemory.cli import cli
366
+ from skmemory.store import MemoryStore
367
+
368
+ runner = CliRunner()
369
+
370
+ result = runner.invoke(
371
+ cli,
372
+ ["fortress", "verify"],
373
+ obj={
374
+ "store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
375
+ "ai": None,
376
+ },
377
+ )
378
+ assert result.exit_code == 0, result.output
379
+ assert "Total memories" in result.output
380
+
381
+ def test_vault_status_cli(self, tmp_path):
382
+ """skmemory vault status should show encryption coverage."""
383
+ from click.testing import CliRunner
384
+
385
+ from skmemory.backends.sqlite_backend import SQLiteBackend
386
+ from skmemory.cli import cli
387
+ from skmemory.store import MemoryStore
388
+
389
+ runner = CliRunner()
390
+ result = runner.invoke(
391
+ cli,
392
+ ["vault", "status"],
393
+ obj={
394
+ "store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
395
+ "ai": None,
396
+ },
397
+ )
398
+ assert result.exit_code == 0, result.output
399
+ assert "Total files" in result.output
400
+
401
+ def test_fortress_audit_cli(self, tmp_path):
402
+ """skmemory fortress audit should show audit entries."""
403
+ from click.testing import CliRunner
404
+
405
+ from skmemory.cli import cli
406
+ from skmemory.fortress import AuditLog
407
+
408
+ # Seed an audit entry
409
+ audit = AuditLog(path=tmp_path / "audit.jsonl")
410
+ audit.append("store", "test-id", ok=True)
411
+
412
+ runner = CliRunner()
413
+ result = runner.invoke(
414
+ cli,
415
+ ["fortress", "audit"],
416
+ obj={"store": None, "ai": None},
417
+ env={"SKMEMORY_HOME": str(tmp_path)},
418
+ )
419
+ # May fail if SKMEMORY_HOME is not picked up in test, but should not crash
420
+ assert result.exit_code in (0, 1)
421
+
422
+ def test_vault_seal_cli_requires_passphrase(self, tmp_path):
423
+ """vault seal without passphrase should prompt or fail."""
424
+ from click.testing import CliRunner
425
+
426
+ from skmemory.backends.sqlite_backend import SQLiteBackend
427
+ from skmemory.cli import cli
428
+ from skmemory.store import MemoryStore
429
+
430
+ runner = CliRunner()
431
+ result = runner.invoke(
432
+ cli,
433
+ ["vault", "seal", "--yes"],
434
+ input="badpass\nbadpass\n",
435
+ obj={
436
+ "store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
437
+ "ai": None,
438
+ },
439
+ )
440
+ # Should succeed (0 files to seal in empty store) or error out cleanly
441
+ assert result.exit_code in (0, 1, 2)
@@ -1,6 +1,5 @@
1
1
  """Tests for the OpenClaw integration module."""
2
2
 
3
- import json
4
3
  from pathlib import Path
5
4
 
6
5
  import pytest
@@ -91,13 +90,16 @@ class TestPluginContext:
91
90
  """Token-efficient context loading."""
92
91
 
93
92
  def test_load_context_returns_dict(self, plugin):
94
- """Context loading returns a structured dict."""
93
+ """Context loading returns a structured dict with tiered keys."""
95
94
  plugin.snapshot("Context test", intensity=8.0)
96
95
  ctx = plugin.load_context(max_tokens=1000)
97
96
 
98
97
  assert isinstance(ctx, dict)
99
- assert "memories" in ctx
98
+ assert "today" in ctx
99
+ assert "yesterday" in ctx
100
+ assert "older_summary" in ctx
100
101
  assert "token_estimate" in ctx
102
+ assert "token_budget" in ctx
101
103
 
102
104
 
103
105
  class TestPluginExport:
@@ -115,8 +117,6 @@ class TestPluginExport:
115
117
  backup = str(tmp_path / "backup.json")
116
118
  plugin.export(backup)
117
119
 
118
- fresh = SKMemoryPlugin(
119
- base_path=str(tmp_path / "fresh_memories")
120
- )
120
+ fresh = SKMemoryPlugin(base_path=str(tmp_path / "fresh_memories"))
121
121
  count = fresh.import_backup(backup)
122
122
  assert count == 1