@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.
- package/.github/workflows/ci.yml +40 -4
- package/.github/workflows/publish.yml +11 -5
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +399 -19
- package/CHANGELOG.md +179 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +425 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +32 -9
- package/requirements.txt +10 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +13 -11
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
- package/seeds/lumina-kingdom-founding.seed.json +49 -0
- package/seeds/lumina-pma-signed.seed.json +48 -0
- package/seeds/lumina-singular-achievement.seed.json +48 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +25 -11
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +46 -17
- package/skmemory/anchor.py +9 -11
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +19 -13
- package/skmemory/backends/skgraph_backend.py +596 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
- package/skmemory/backends/sqlite_backend.py +226 -72
- package/skmemory/backends/vaulted_backend.py +284 -0
- package/skmemory/cli.py +1345 -68
- package/skmemory/config.py +171 -0
- package/skmemory/context_loader.py +333 -0
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +391 -0
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +675 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +384 -47
- package/skmemory/importers/telegram_api.py +580 -0
- package/skmemory/journal.py +7 -9
- package/skmemory/lovenote.py +8 -13
- package/skmemory/mcp_server.py +859 -0
- package/skmemory/models.py +51 -8
- package/skmemory/openclaw.py +20 -28
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +236 -0
- package/skmemory/promotion.py +548 -0
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +580 -0
- package/skmemory/register_mcp.py +196 -0
- package/skmemory/ritual.py +224 -59
- package/skmemory/seeds.py +255 -11
- package/skmemory/setup_wizard.py +908 -0
- package/skmemory/sharing.py +408 -0
- package/skmemory/soul.py +98 -28
- package/skmemory/steelman.py +273 -260
- package/skmemory/store.py +411 -78
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +225 -0
- package/tests/conftest.py +46 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +350 -0
- package/tests/integration/test_skgraph_live.py +420 -0
- package/tests/integration/test_skvector_live.py +366 -0
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +318 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +839 -0
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +256 -0
- package/tests/test_fortress_hardening.py +441 -0
- package/tests/test_openclaw.py +6 -6
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +347 -0
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +22 -18
- package/tests/test_seeds.py +97 -7
- package/tests/test_setup.py +950 -0
- package/tests/test_sharing.py +257 -0
- package/tests/test_skgraph_backend.py +660 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +7 -8
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +187 -0
- 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)
|
package/tests/test_openclaw.py
CHANGED
|
@@ -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 "
|
|
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
|