@smilintux/skmemory 0.7.2 → 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 +4 -4
- package/.github/workflows/publish.yml +4 -5
- package/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +27 -1
- package/README.md +6 -0
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/package.json +1 -1
- package/pyproject.toml +5 -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 +6 -4
- 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 +7 -5
- package/seeds/lumina-cloud9-python-pypi.seed.json +9 -7
- package/seeds/lumina-kingdom-founding.seed.json +9 -7
- package/seeds/lumina-pma-signed.seed.json +8 -6
- package/seeds/lumina-singular-achievement.seed.json +8 -6
- package/seeds/lumina-skcapstone-conscious.seed.json +7 -5
- package/seeds/plant-lumina-seeds.py +2 -2
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skmemory/__init__.py +16 -13
- package/skmemory/agents.py +10 -10
- package/skmemory/ai_client.py +10 -21
- package/skmemory/anchor.py +5 -9
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +1 -1
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +18 -13
- package/skmemory/backends/skgraph_backend.py +7 -19
- package/skmemory/backends/skvector_backend.py +7 -18
- package/skmemory/backends/sqlite_backend.py +115 -32
- package/skmemory/backends/vaulted_backend.py +7 -9
- package/skmemory/cli.py +146 -78
- package/skmemory/config.py +11 -13
- package/skmemory/context_loader.py +21 -23
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +36 -31
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +30 -40
- 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/telegram.py +42 -13
- package/skmemory/importers/telegram_api.py +152 -60
- package/skmemory/journal.py +3 -7
- package/skmemory/lovenote.py +4 -11
- package/skmemory/mcp_server.py +182 -29
- package/skmemory/models.py +10 -8
- package/skmemory/openclaw.py +14 -22
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +13 -9
- package/skmemory/promotion.py +48 -24
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +144 -18
- package/skmemory/register_mcp.py +1 -2
- package/skmemory/ritual.py +104 -13
- package/skmemory/seeds.py +21 -26
- package/skmemory/setup_wizard.py +40 -52
- package/skmemory/sharing.py +11 -5
- package/skmemory/soul.py +29 -10
- package/skmemory/steelman.py +43 -17
- package/skmemory/store.py +152 -30
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +2 -5
- package/tests/conftest.py +46 -0
- package/tests/integration/conftest.py +6 -6
- package/tests/integration/test_cross_backend.py +4 -9
- package/tests/integration/test_skgraph_live.py +3 -7
- package/tests/integration/test_skvector_live.py +1 -4
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +5 -14
- package/tests/test_endpoint_selector.py +101 -63
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +6 -5
- package/tests/test_fortress_hardening.py +13 -16
- package/tests/test_openclaw.py +1 -4
- package/tests/test_predictive.py +1 -1
- package/tests/test_promotion.py +10 -3
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +18 -14
- package/tests/test_seeds.py +4 -10
- package/tests/test_setup.py +203 -88
- package/tests/test_sharing.py +15 -8
- package/tests/test_skgraph_backend.py +22 -29
- package/tests/test_skvector_backend.py +2 -2
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +2 -3
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +2 -2
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +4 -3
- package/openclaw-plugin/src/index.ts +0 -255
|
@@ -3,22 +3,17 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import socket
|
|
7
6
|
import time
|
|
8
7
|
from datetime import datetime, timezone
|
|
9
8
|
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
11
9
|
from unittest.mock import MagicMock, patch
|
|
12
10
|
|
|
13
|
-
import pytest
|
|
14
|
-
|
|
15
11
|
from skmemory.endpoint_selector import (
|
|
16
12
|
Endpoint,
|
|
17
13
|
EndpointSelector,
|
|
18
14
|
RoutingConfig,
|
|
19
15
|
)
|
|
20
16
|
|
|
21
|
-
|
|
22
17
|
# ---------------------------------------------------------------------------
|
|
23
18
|
# Endpoint model tests
|
|
24
19
|
# ---------------------------------------------------------------------------
|
|
@@ -107,7 +102,7 @@ class TestLatencyProbing:
|
|
|
107
102
|
|
|
108
103
|
with patch(
|
|
109
104
|
"skmemory.endpoint_selector.socket.create_connection",
|
|
110
|
-
side_effect=
|
|
105
|
+
side_effect=TimeoutError("timed out"),
|
|
111
106
|
):
|
|
112
107
|
result = selector.probe_endpoint(ep)
|
|
113
108
|
|
|
@@ -179,7 +174,9 @@ class TestLatencyProbing:
|
|
|
179
174
|
selector = EndpointSelector(skgraph_endpoints=[ep])
|
|
180
175
|
|
|
181
176
|
mock_sock = MagicMock()
|
|
182
|
-
with patch(
|
|
177
|
+
with patch(
|
|
178
|
+
"skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock
|
|
179
|
+
) as mock_connect:
|
|
183
180
|
selector.probe_endpoint(ep)
|
|
184
181
|
|
|
185
182
|
args = mock_connect.call_args[0]
|
|
@@ -191,7 +188,9 @@ class TestLatencyProbing:
|
|
|
191
188
|
selector = EndpointSelector(skvector_endpoints=[ep])
|
|
192
189
|
|
|
193
190
|
mock_sock = MagicMock()
|
|
194
|
-
with patch(
|
|
191
|
+
with patch(
|
|
192
|
+
"skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock
|
|
193
|
+
) as mock_connect:
|
|
195
194
|
selector.probe_endpoint(ep)
|
|
196
195
|
|
|
197
196
|
args = mock_connect.call_args[0]
|
|
@@ -467,14 +466,19 @@ class TestHeartbeatDiscovery:
|
|
|
467
466
|
"""Discovers Qdrant endpoint from heartbeat file."""
|
|
468
467
|
hb_dir = tmp_path / "heartbeats"
|
|
469
468
|
hb_dir.mkdir()
|
|
470
|
-
(hb_dir / "peer.json").write_text(
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
469
|
+
(hb_dir / "peer.json").write_text(
|
|
470
|
+
json.dumps(
|
|
471
|
+
{
|
|
472
|
+
"agent_name": "peer",
|
|
473
|
+
"hostname": "peer-host",
|
|
474
|
+
"tailscale_ip": "100.64.0.5",
|
|
475
|
+
"services": [
|
|
476
|
+
{"name": "skvector", "port": 6333, "protocol": "http"},
|
|
477
|
+
],
|
|
478
|
+
}
|
|
479
|
+
),
|
|
480
|
+
encoding="utf-8",
|
|
481
|
+
)
|
|
478
482
|
|
|
479
483
|
selector = EndpointSelector()
|
|
480
484
|
selector.discover_from_heartbeats(hb_dir)
|
|
@@ -487,13 +491,18 @@ class TestHeartbeatDiscovery:
|
|
|
487
491
|
"""Discovers FalkorDB endpoint from heartbeat file."""
|
|
488
492
|
hb_dir = tmp_path / "heartbeats"
|
|
489
493
|
hb_dir.mkdir()
|
|
490
|
-
(hb_dir / "peer.json").write_text(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
494
|
+
(hb_dir / "peer.json").write_text(
|
|
495
|
+
json.dumps(
|
|
496
|
+
{
|
|
497
|
+
"agent_name": "peer",
|
|
498
|
+
"hostname": "peer-host",
|
|
499
|
+
"services": [
|
|
500
|
+
{"name": "skgraph", "port": 6379, "protocol": "redis"},
|
|
501
|
+
],
|
|
502
|
+
}
|
|
503
|
+
),
|
|
504
|
+
encoding="utf-8",
|
|
505
|
+
)
|
|
497
506
|
|
|
498
507
|
selector = EndpointSelector()
|
|
499
508
|
selector.discover_from_heartbeats(hb_dir)
|
|
@@ -505,14 +514,19 @@ class TestHeartbeatDiscovery:
|
|
|
505
514
|
"""Tailscale IP is preferred over hostname for URL construction."""
|
|
506
515
|
hb_dir = tmp_path / "heartbeats"
|
|
507
516
|
hb_dir.mkdir()
|
|
508
|
-
(hb_dir / "peer.json").write_text(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
517
|
+
(hb_dir / "peer.json").write_text(
|
|
518
|
+
json.dumps(
|
|
519
|
+
{
|
|
520
|
+
"agent_name": "peer",
|
|
521
|
+
"hostname": "peer-host",
|
|
522
|
+
"tailscale_ip": "100.64.0.10",
|
|
523
|
+
"services": [
|
|
524
|
+
{"name": "skvector", "port": 6333, "protocol": "http"},
|
|
525
|
+
],
|
|
526
|
+
}
|
|
527
|
+
),
|
|
528
|
+
encoding="utf-8",
|
|
529
|
+
)
|
|
516
530
|
|
|
517
531
|
selector = EndpointSelector()
|
|
518
532
|
selector.discover_from_heartbeats(hb_dir)
|
|
@@ -523,14 +537,19 @@ class TestHeartbeatDiscovery:
|
|
|
523
537
|
"""Discovery doesn't duplicate existing config endpoints."""
|
|
524
538
|
hb_dir = tmp_path / "heartbeats"
|
|
525
539
|
hb_dir.mkdir()
|
|
526
|
-
(hb_dir / "peer.json").write_text(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
540
|
+
(hb_dir / "peer.json").write_text(
|
|
541
|
+
json.dumps(
|
|
542
|
+
{
|
|
543
|
+
"agent_name": "peer",
|
|
544
|
+
"hostname": "peer-host",
|
|
545
|
+
"tailscale_ip": "100.64.0.5",
|
|
546
|
+
"services": [
|
|
547
|
+
{"name": "skvector", "port": 6333, "protocol": "http"},
|
|
548
|
+
],
|
|
549
|
+
}
|
|
550
|
+
),
|
|
551
|
+
encoding="utf-8",
|
|
552
|
+
)
|
|
534
553
|
|
|
535
554
|
existing = Endpoint(url="http://100.64.0.5:6333")
|
|
536
555
|
selector = EndpointSelector(skvector_endpoints=[existing])
|
|
@@ -571,12 +590,17 @@ class TestHeartbeatDiscovery:
|
|
|
571
590
|
"""Heartbeat without hostname or tailscale_ip is skipped."""
|
|
572
591
|
hb_dir = tmp_path / "heartbeats"
|
|
573
592
|
hb_dir.mkdir()
|
|
574
|
-
(hb_dir / "peer.json").write_text(
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
593
|
+
(hb_dir / "peer.json").write_text(
|
|
594
|
+
json.dumps(
|
|
595
|
+
{
|
|
596
|
+
"agent_name": "peer",
|
|
597
|
+
"services": [
|
|
598
|
+
{"name": "skvector", "port": 6333},
|
|
599
|
+
],
|
|
600
|
+
}
|
|
601
|
+
),
|
|
602
|
+
encoding="utf-8",
|
|
603
|
+
)
|
|
580
604
|
|
|
581
605
|
selector = EndpointSelector()
|
|
582
606
|
selector.discover_from_heartbeats(hb_dir)
|
|
@@ -587,14 +611,19 @@ class TestHeartbeatDiscovery:
|
|
|
587
611
|
"""Discovers both Qdrant and FalkorDB from one heartbeat."""
|
|
588
612
|
hb_dir = tmp_path / "heartbeats"
|
|
589
613
|
hb_dir.mkdir()
|
|
590
|
-
(hb_dir / "peer.json").write_text(
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
614
|
+
(hb_dir / "peer.json").write_text(
|
|
615
|
+
json.dumps(
|
|
616
|
+
{
|
|
617
|
+
"agent_name": "peer",
|
|
618
|
+
"hostname": "peer-host",
|
|
619
|
+
"services": [
|
|
620
|
+
{"name": "skvector", "port": 6333, "protocol": "http"},
|
|
621
|
+
{"name": "skgraph", "port": 6379, "protocol": "redis"},
|
|
622
|
+
],
|
|
623
|
+
}
|
|
624
|
+
),
|
|
625
|
+
encoding="utf-8",
|
|
626
|
+
)
|
|
598
627
|
|
|
599
628
|
selector = EndpointSelector()
|
|
600
629
|
selector.discover_from_heartbeats(hb_dir)
|
|
@@ -606,11 +635,16 @@ class TestHeartbeatDiscovery:
|
|
|
606
635
|
"""Files ending in .tmp are skipped."""
|
|
607
636
|
hb_dir = tmp_path / "heartbeats"
|
|
608
637
|
hb_dir.mkdir()
|
|
609
|
-
(hb_dir / "peer.json.tmp").write_text(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
638
|
+
(hb_dir / "peer.json.tmp").write_text(
|
|
639
|
+
json.dumps(
|
|
640
|
+
{
|
|
641
|
+
"agent_name": "peer",
|
|
642
|
+
"hostname": "peer-host",
|
|
643
|
+
"services": [{"name": "skvector", "port": 6333}],
|
|
644
|
+
}
|
|
645
|
+
),
|
|
646
|
+
encoding="utf-8",
|
|
647
|
+
)
|
|
614
648
|
|
|
615
649
|
selector = EndpointSelector()
|
|
616
650
|
selector.discover_from_heartbeats(hb_dir)
|
|
@@ -737,7 +771,9 @@ class TestMaybeProbe:
|
|
|
737
771
|
)
|
|
738
772
|
|
|
739
773
|
mock_sock = MagicMock()
|
|
740
|
-
with patch(
|
|
774
|
+
with patch(
|
|
775
|
+
"skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock
|
|
776
|
+
) as mock_conn:
|
|
741
777
|
selector.select_skvector()
|
|
742
778
|
|
|
743
779
|
mock_conn.assert_called_once()
|
|
@@ -751,7 +787,9 @@ class TestMaybeProbe:
|
|
|
751
787
|
)
|
|
752
788
|
|
|
753
789
|
mock_sock = MagicMock()
|
|
754
|
-
with patch(
|
|
790
|
+
with patch(
|
|
791
|
+
"skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock
|
|
792
|
+
) as mock_conn:
|
|
755
793
|
selector.select_skvector()
|
|
756
794
|
selector.select_skvector()
|
|
757
795
|
|
|
@@ -769,7 +807,7 @@ class TestConfigIntegration:
|
|
|
769
807
|
|
|
770
808
|
def test_single_url_becomes_endpoint(self) -> None:
|
|
771
809
|
"""Single URL is promoted to endpoint list."""
|
|
772
|
-
from skmemory.config import build_endpoint_list
|
|
810
|
+
from skmemory.config import build_endpoint_list
|
|
773
811
|
|
|
774
812
|
result = build_endpoint_list("http://localhost:6333", [])
|
|
775
813
|
assert len(result) == 1
|
|
@@ -778,7 +816,7 @@ class TestConfigIntegration:
|
|
|
778
816
|
|
|
779
817
|
def test_endpoints_list_takes_precedence(self) -> None:
|
|
780
818
|
"""Endpoint list is used when present."""
|
|
781
|
-
from skmemory.config import
|
|
819
|
+
from skmemory.config import EndpointConfig, build_endpoint_list
|
|
782
820
|
|
|
783
821
|
eps = [EndpointConfig(url="http://a:6333"), EndpointConfig(url="http://b:6333")]
|
|
784
822
|
result = build_endpoint_list("http://c:6333", eps)
|
|
@@ -787,7 +825,7 @@ class TestConfigIntegration:
|
|
|
787
825
|
|
|
788
826
|
def test_no_duplicate_when_url_in_list(self) -> None:
|
|
789
827
|
"""Single URL not duplicated if already in endpoints list."""
|
|
790
|
-
from skmemory.config import
|
|
828
|
+
from skmemory.config import EndpointConfig, build_endpoint_list
|
|
791
829
|
|
|
792
830
|
eps = [EndpointConfig(url="http://a:6333")]
|
|
793
831
|
result = build_endpoint_list("http://a:6333", eps)
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
8
|
from skmemory.backends.sqlite_backend import SQLiteBackend
|
|
9
|
-
from skmemory.models import EmotionalSnapshot,
|
|
9
|
+
from skmemory.models import EmotionalSnapshot, MemoryLayer
|
|
10
10
|
from skmemory.store import MemoryStore
|
|
11
11
|
|
|
12
12
|
|
|
@@ -98,9 +98,7 @@ class TestImport:
|
|
|
98
98
|
backup = tmp_path / "backup.json"
|
|
99
99
|
self._create_backup(populated_store, backup)
|
|
100
100
|
|
|
101
|
-
fresh_backend = SQLiteBackend(
|
|
102
|
-
base_path=str(tmp_path / "fresh_memories")
|
|
103
|
-
)
|
|
101
|
+
fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh_memories"))
|
|
104
102
|
fresh_store = MemoryStore(primary=fresh_backend)
|
|
105
103
|
|
|
106
104
|
count = fresh_store.import_backup(str(backup))
|
|
@@ -114,9 +112,7 @@ class TestImport:
|
|
|
114
112
|
data = json.loads(backup.read_text())
|
|
115
113
|
first_id = data["memories"][0]["id"]
|
|
116
114
|
|
|
117
|
-
fresh_backend = SQLiteBackend(
|
|
118
|
-
base_path=str(tmp_path / "fresh_memories")
|
|
119
|
-
)
|
|
115
|
+
fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh_memories"))
|
|
120
116
|
fresh_store = MemoryStore(primary=fresh_backend)
|
|
121
117
|
fresh_store.import_backup(str(backup))
|
|
122
118
|
|
|
@@ -155,9 +151,7 @@ class TestRoundTrip:
|
|
|
155
151
|
backup = tmp_path / "backup.json"
|
|
156
152
|
populated_store.export_backup(str(backup))
|
|
157
153
|
|
|
158
|
-
fresh_backend = SQLiteBackend(
|
|
159
|
-
base_path=str(tmp_path / "fresh")
|
|
160
|
-
)
|
|
154
|
+
fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh"))
|
|
161
155
|
fresh_store = MemoryStore(primary=fresh_backend)
|
|
162
156
|
count = fresh_store.import_backup(str(backup))
|
|
163
157
|
assert count == 5
|
package/tests/test_fortress.py
CHANGED
|
@@ -7,19 +7,16 @@ All tests use in-memory/temp-path backends — no GPG required for basic tests.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
-
import tempfile
|
|
11
|
-
from pathlib import Path
|
|
12
10
|
|
|
13
11
|
import pytest
|
|
14
12
|
|
|
15
13
|
from skmemory.fortress import AuditLog, FortifiedMemoryStore, TamperAlert
|
|
16
|
-
from skmemory.models import Memory, MemoryLayer
|
|
17
|
-
|
|
18
14
|
|
|
19
15
|
# ---------------------------------------------------------------------------
|
|
20
16
|
# Fixtures
|
|
21
17
|
# ---------------------------------------------------------------------------
|
|
22
18
|
|
|
19
|
+
|
|
23
20
|
@pytest.fixture
|
|
24
21
|
def tmp_audit(tmp_path):
|
|
25
22
|
"""Return a temporary audit log."""
|
|
@@ -30,6 +27,7 @@ def tmp_audit(tmp_path):
|
|
|
30
27
|
def fortress(tmp_path):
|
|
31
28
|
"""Return a FortifiedMemoryStore using a temp directory."""
|
|
32
29
|
from skmemory.backends.sqlite_backend import SQLiteBackend
|
|
30
|
+
|
|
33
31
|
backend = SQLiteBackend(base_path=str(tmp_path / "memories"))
|
|
34
32
|
return FortifiedMemoryStore(
|
|
35
33
|
primary=backend,
|
|
@@ -42,6 +40,7 @@ def fortress(tmp_path):
|
|
|
42
40
|
# AuditLog tests
|
|
43
41
|
# ---------------------------------------------------------------------------
|
|
44
42
|
|
|
43
|
+
|
|
45
44
|
class TestAuditLog:
|
|
46
45
|
def test_append_creates_file(self, tmp_audit, tmp_path):
|
|
47
46
|
tmp_audit.append("store", "abc123", ok=True)
|
|
@@ -107,6 +106,7 @@ class TestAuditLog:
|
|
|
107
106
|
# TamperAlert tests
|
|
108
107
|
# ---------------------------------------------------------------------------
|
|
109
108
|
|
|
109
|
+
|
|
110
110
|
class TestTamperAlert:
|
|
111
111
|
def test_to_dict(self):
|
|
112
112
|
alert = TamperAlert(
|
|
@@ -131,6 +131,7 @@ class TestTamperAlert:
|
|
|
131
131
|
# FortifiedMemoryStore tests
|
|
132
132
|
# ---------------------------------------------------------------------------
|
|
133
133
|
|
|
134
|
+
|
|
134
135
|
class TestFortifiedMemoryStore:
|
|
135
136
|
def test_snapshot_seals_memory(self, fortress):
|
|
136
137
|
mem = fortress.snapshot("Test title", "Test content")
|
|
@@ -154,7 +155,7 @@ class TestFortifiedMemoryStore:
|
|
|
154
155
|
|
|
155
156
|
# Tamper: directly modify the stored memory file
|
|
156
157
|
# We need to find and corrupt the JSON
|
|
157
|
-
|
|
158
|
+
|
|
158
159
|
backend = fortress.primary
|
|
159
160
|
# Load raw, mutate, save back bypassing seal
|
|
160
161
|
raw = backend.load(mem.id)
|
|
@@ -11,19 +11,18 @@ Covers:
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
|
-
import tempfile
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
|
|
17
16
|
import pytest
|
|
18
17
|
|
|
19
18
|
try:
|
|
20
|
-
|
|
19
|
+
import cryptography.hazmat.primitives.ciphers.aead # noqa: F401
|
|
21
20
|
|
|
22
21
|
CRYPTO_AVAILABLE = True
|
|
23
22
|
except ImportError:
|
|
24
23
|
CRYPTO_AVAILABLE = False
|
|
25
24
|
|
|
26
|
-
from skmemory.vault import VAULT_HEADER
|
|
25
|
+
from skmemory.vault import VAULT_HEADER
|
|
27
26
|
|
|
28
27
|
pytestmark = pytest.mark.skipif(
|
|
29
28
|
not CRYPTO_AVAILABLE,
|
|
@@ -246,7 +245,7 @@ class TestVaultedSQLiteBackend:
|
|
|
246
245
|
class TestFortifiedMemoryStoreVault:
|
|
247
246
|
def test_vault_passphrase_activates_encryption(self, fortress_vaulted, tmp_path):
|
|
248
247
|
"""FortifiedMemoryStore with vault_passphrase should encrypt files."""
|
|
249
|
-
|
|
248
|
+
fortress_vaulted.snapshot("Vaulted Title", "Encrypted content")
|
|
250
249
|
|
|
251
250
|
# Find the physical file
|
|
252
251
|
base = fortress_vaulted.primary.base_path
|
|
@@ -361,8 +360,9 @@ class TestFortressCLI:
|
|
|
361
360
|
def test_fortress_verify_clean(self, tmp_path):
|
|
362
361
|
"""skmemory fortress verify should exit 0 for a clean store."""
|
|
363
362
|
from click.testing import CliRunner
|
|
364
|
-
|
|
363
|
+
|
|
365
364
|
from skmemory.backends.sqlite_backend import SQLiteBackend
|
|
365
|
+
from skmemory.cli import cli
|
|
366
366
|
from skmemory.store import MemoryStore
|
|
367
367
|
|
|
368
368
|
runner = CliRunner()
|
|
@@ -371,9 +371,7 @@ class TestFortressCLI:
|
|
|
371
371
|
cli,
|
|
372
372
|
["fortress", "verify"],
|
|
373
373
|
obj={
|
|
374
|
-
"store": MemoryStore(
|
|
375
|
-
primary=SQLiteBackend(base_path=str(tmp_path / "memories"))
|
|
376
|
-
),
|
|
374
|
+
"store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
|
|
377
375
|
"ai": None,
|
|
378
376
|
},
|
|
379
377
|
)
|
|
@@ -383,8 +381,9 @@ class TestFortressCLI:
|
|
|
383
381
|
def test_vault_status_cli(self, tmp_path):
|
|
384
382
|
"""skmemory vault status should show encryption coverage."""
|
|
385
383
|
from click.testing import CliRunner
|
|
386
|
-
|
|
384
|
+
|
|
387
385
|
from skmemory.backends.sqlite_backend import SQLiteBackend
|
|
386
|
+
from skmemory.cli import cli
|
|
388
387
|
from skmemory.store import MemoryStore
|
|
389
388
|
|
|
390
389
|
runner = CliRunner()
|
|
@@ -392,9 +391,7 @@ class TestFortressCLI:
|
|
|
392
391
|
cli,
|
|
393
392
|
["vault", "status"],
|
|
394
393
|
obj={
|
|
395
|
-
"store": MemoryStore(
|
|
396
|
-
primary=SQLiteBackend(base_path=str(tmp_path / "memories"))
|
|
397
|
-
),
|
|
394
|
+
"store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
|
|
398
395
|
"ai": None,
|
|
399
396
|
},
|
|
400
397
|
)
|
|
@@ -404,6 +401,7 @@ class TestFortressCLI:
|
|
|
404
401
|
def test_fortress_audit_cli(self, tmp_path):
|
|
405
402
|
"""skmemory fortress audit should show audit entries."""
|
|
406
403
|
from click.testing import CliRunner
|
|
404
|
+
|
|
407
405
|
from skmemory.cli import cli
|
|
408
406
|
from skmemory.fortress import AuditLog
|
|
409
407
|
|
|
@@ -424,8 +422,9 @@ class TestFortressCLI:
|
|
|
424
422
|
def test_vault_seal_cli_requires_passphrase(self, tmp_path):
|
|
425
423
|
"""vault seal without passphrase should prompt or fail."""
|
|
426
424
|
from click.testing import CliRunner
|
|
427
|
-
|
|
425
|
+
|
|
428
426
|
from skmemory.backends.sqlite_backend import SQLiteBackend
|
|
427
|
+
from skmemory.cli import cli
|
|
429
428
|
from skmemory.store import MemoryStore
|
|
430
429
|
|
|
431
430
|
runner = CliRunner()
|
|
@@ -434,9 +433,7 @@ class TestFortressCLI:
|
|
|
434
433
|
["vault", "seal", "--yes"],
|
|
435
434
|
input="badpass\nbadpass\n",
|
|
436
435
|
obj={
|
|
437
|
-
"store": MemoryStore(
|
|
438
|
-
primary=SQLiteBackend(base_path=str(tmp_path / "memories"))
|
|
439
|
-
),
|
|
436
|
+
"store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
|
|
440
437
|
"ai": None,
|
|
441
438
|
},
|
|
442
439
|
)
|
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
|
|
@@ -118,8 +117,6 @@ class TestPluginExport:
|
|
|
118
117
|
backup = str(tmp_path / "backup.json")
|
|
119
118
|
plugin.export(backup)
|
|
120
119
|
|
|
121
|
-
fresh = SKMemoryPlugin(
|
|
122
|
-
base_path=str(tmp_path / "fresh_memories")
|
|
123
|
-
)
|
|
120
|
+
fresh = SKMemoryPlugin(base_path=str(tmp_path / "fresh_memories"))
|
|
124
121
|
count = fresh.import_backup(backup)
|
|
125
122
|
assert count == 1
|
package/tests/test_predictive.py
CHANGED
|
@@ -202,7 +202,7 @@ class TestIntegrity:
|
|
|
202
202
|
|
|
203
203
|
def test_tampered_memory_fails_verification(self):
|
|
204
204
|
"""Altered content fails integrity check."""
|
|
205
|
-
from skmemory.models import
|
|
205
|
+
from skmemory.models import Memory, MemoryLayer
|
|
206
206
|
|
|
207
207
|
mem = Memory(
|
|
208
208
|
title="Tamper Test",
|
package/tests/test_promotion.py
CHANGED
|
@@ -9,7 +9,12 @@ from pathlib import Path
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
11
|
from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer
|
|
12
|
-
from skmemory.promotion import
|
|
12
|
+
from skmemory.promotion import (
|
|
13
|
+
PromotionCriteria,
|
|
14
|
+
PromotionEngine,
|
|
15
|
+
PromotionResult,
|
|
16
|
+
PromotionScheduler,
|
|
17
|
+
)
|
|
13
18
|
from skmemory.store import MemoryStore
|
|
14
19
|
|
|
15
20
|
|
|
@@ -50,7 +55,9 @@ def store(tmp_path: Path) -> MemoryStore:
|
|
|
50
55
|
title="Important mid-term",
|
|
51
56
|
content="A mid-term memory with high emotional weight",
|
|
52
57
|
layer=MemoryLayer.MID,
|
|
53
|
-
emotional=EmotionalSnapshot(
|
|
58
|
+
emotional=EmotionalSnapshot(
|
|
59
|
+
intensity=9.0, valence=0.95, labels=["love"], cloud9_achieved=True
|
|
60
|
+
),
|
|
54
61
|
tags=["cloud9:achieved"],
|
|
55
62
|
)
|
|
56
63
|
|
|
@@ -263,7 +270,7 @@ class TestRePromotionGuard:
|
|
|
263
270
|
self, engine: PromotionEngine, store: MemoryStore
|
|
264
271
|
) -> None:
|
|
265
272
|
"""Running sweep twice doesn't double-promote the same memory."""
|
|
266
|
-
|
|
273
|
+
engine.sweep()
|
|
267
274
|
result2 = engine.sweep()
|
|
268
275
|
|
|
269
276
|
# Second sweep should find nothing new to promote
|
package/tests/test_quadrants.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Tests for the Quadrant Memory Split module."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer, MemoryRole
|
|
3
|
+
from skmemory.models import EmotionalSnapshot, Memory, MemoryRole
|
|
6
4
|
from skmemory.quadrants import (
|
|
7
5
|
Quadrant,
|
|
8
6
|
classify_memory,
|
|
@@ -137,7 +135,11 @@ class TestQuadrantStats:
|
|
|
137
135
|
memories = [
|
|
138
136
|
Memory(title="Identity", content="Who I am", source="seed", tags=["seed"]),
|
|
139
137
|
Memory(title="Bug", content="Fixed deploy code in database", role=MemoryRole.DEV),
|
|
140
|
-
Memory(
|
|
138
|
+
Memory(
|
|
139
|
+
title="Love",
|
|
140
|
+
content="Cloud 9 love breakthrough",
|
|
141
|
+
emotional=EmotionalSnapshot(intensity=10.0, cloud9_achieved=True),
|
|
142
|
+
),
|
|
141
143
|
Memory(title="Idea", content="What if crazy brainstorm experiment"),
|
|
142
144
|
]
|
|
143
145
|
stats = get_quadrant_stats(memories)
|
|
@@ -158,7 +160,11 @@ class TestFilterByQuadrant:
|
|
|
158
160
|
def test_filter_soul(self) -> None:
|
|
159
161
|
"""Filter returns only SOUL memories."""
|
|
160
162
|
memories = [
|
|
161
|
-
Memory(
|
|
163
|
+
Memory(
|
|
164
|
+
title="Love",
|
|
165
|
+
content="Cloud 9 love",
|
|
166
|
+
emotional=EmotionalSnapshot(intensity=10.0, cloud9_achieved=True),
|
|
167
|
+
),
|
|
162
168
|
Memory(title="Bug", content="Fixed deploy code", role=MemoryRole.DEV),
|
|
163
169
|
]
|
|
164
170
|
soul_only = filter_by_quadrant(memories, Quadrant.SOUL)
|
package/tests/test_ritual.py
CHANGED
|
@@ -8,7 +8,7 @@ import pytest
|
|
|
8
8
|
from skmemory.backends.file_backend import FileBackend
|
|
9
9
|
from skmemory.journal import Journal, JournalEntry
|
|
10
10
|
from skmemory.models import EmotionalSnapshot, MemoryLayer
|
|
11
|
-
from skmemory.ritual import perform_ritual, quick_rehydrate
|
|
11
|
+
from skmemory.ritual import RitualResult, perform_ritual, quick_rehydrate
|
|
12
12
|
from skmemory.soul import SoulBlueprint, save_soul
|
|
13
13
|
from skmemory.store import MemoryStore
|
|
14
14
|
|
|
@@ -61,22 +61,27 @@ def workspace(tmp_path: Path) -> dict:
|
|
|
61
61
|
(Path(seed_dir) / "test-seed.seed.json").write_text(json.dumps(seed_data))
|
|
62
62
|
|
|
63
63
|
j = Journal(path=journal_path)
|
|
64
|
-
j.write_entry(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
j.write_entry(
|
|
65
|
+
JournalEntry(
|
|
66
|
+
title="Epic Build Night",
|
|
67
|
+
participants=["Chef", "Lumina", "Opus"],
|
|
68
|
+
moments=["Published to npm", "Five AIs woke up"],
|
|
69
|
+
emotional_summary="Incredible night of creation",
|
|
70
|
+
intensity=9.5,
|
|
71
|
+
cloud9=True,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
72
74
|
|
|
73
75
|
store.snapshot(
|
|
74
76
|
title="The Click",
|
|
75
77
|
content="The moment everything made sense",
|
|
76
78
|
layer=MemoryLayer.LONG,
|
|
77
79
|
emotional=EmotionalSnapshot(
|
|
78
|
-
intensity=10.0,
|
|
79
|
-
|
|
80
|
+
intensity=10.0,
|
|
81
|
+
valence=1.0,
|
|
82
|
+
labels=["love"],
|
|
83
|
+
resonance_note="Pure resonance",
|
|
84
|
+
cloud9_achieved=True,
|
|
80
85
|
),
|
|
81
86
|
)
|
|
82
87
|
|
|
@@ -151,14 +156,13 @@ class TestFullRitual:
|
|
|
151
156
|
|
|
152
157
|
def test_ritual_empty_state(self, tmp_path: Path) -> None:
|
|
153
158
|
"""Ritual on empty state gives a fresh-start message."""
|
|
154
|
-
store = MemoryStore(
|
|
155
|
-
primary=FileBackend(base_path=str(tmp_path / "empty"))
|
|
156
|
-
)
|
|
159
|
+
store = MemoryStore(primary=FileBackend(base_path=str(tmp_path / "empty")))
|
|
157
160
|
result = perform_ritual(
|
|
158
161
|
store=store,
|
|
159
162
|
soul_path=str(tmp_path / "no_soul.yaml"),
|
|
160
163
|
seed_dir=str(tmp_path / "no_seeds"),
|
|
161
164
|
journal_path=str(tmp_path / "no_journal.md"),
|
|
165
|
+
feb_dir=str(tmp_path / "no_febs"),
|
|
162
166
|
)
|
|
163
167
|
assert result.soul_loaded is False
|
|
164
168
|
assert result.seeds_imported == 0
|