@smilintux/skcapstone 0.1.0
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/.cursorrules +33 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/publish.yml +52 -0
- package/AGENTS.md +74 -0
- package/CLAUDE.md +56 -0
- package/LICENSE +674 -0
- package/README.md +242 -0
- package/SKILL.md +36 -0
- package/bin/cli.js +18 -0
- package/docs/ARCHITECTURE.md +510 -0
- package/docs/SECURITY_DESIGN.md +315 -0
- package/docs/SOVEREIGN_SINGULARITY.md +371 -0
- package/docs/TOKEN_SYSTEM.md +201 -0
- package/index.d.ts +9 -0
- package/index.js +32 -0
- package/package.json +32 -0
- package/pyproject.toml +84 -0
- package/src/skcapstone/__init__.py +13 -0
- package/src/skcapstone/cli.py +1441 -0
- package/src/skcapstone/connectors/__init__.py +6 -0
- package/src/skcapstone/coordination.py +590 -0
- package/src/skcapstone/discovery.py +275 -0
- package/src/skcapstone/memory_engine.py +457 -0
- package/src/skcapstone/models.py +223 -0
- package/src/skcapstone/pillars/__init__.py +8 -0
- package/src/skcapstone/pillars/identity.py +91 -0
- package/src/skcapstone/pillars/memory.py +61 -0
- package/src/skcapstone/pillars/security.py +83 -0
- package/src/skcapstone/pillars/sync.py +486 -0
- package/src/skcapstone/pillars/trust.py +335 -0
- package/src/skcapstone/runtime.py +190 -0
- package/src/skcapstone/skills/__init__.py +1 -0
- package/src/skcapstone/skills/syncthing_setup.py +297 -0
- package/src/skcapstone/sync/__init__.py +14 -0
- package/src/skcapstone/sync/backends.py +330 -0
- package/src/skcapstone/sync/engine.py +301 -0
- package/src/skcapstone/sync/models.py +97 -0
- package/src/skcapstone/sync/vault.py +284 -0
- package/src/skcapstone/tokens.py +439 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest.py +42 -0
- package/tests/test_coordination.py +299 -0
- package/tests/test_discovery.py +57 -0
- package/tests/test_memory_engine.py +391 -0
- package/tests/test_models.py +63 -0
- package/tests/test_pillars.py +87 -0
- package/tests/test_runtime.py +60 -0
- package/tests/test_sync.py +507 -0
- package/tests/test_syncthing_setup.py +76 -0
- package/tests/test_tokens.py +265 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Tests for the sovereign memory engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.memory_engine import (
|
|
12
|
+
delete,
|
|
13
|
+
export_for_seed,
|
|
14
|
+
gc_expired,
|
|
15
|
+
get_stats,
|
|
16
|
+
import_from_seed,
|
|
17
|
+
list_memories,
|
|
18
|
+
recall,
|
|
19
|
+
search,
|
|
20
|
+
store,
|
|
21
|
+
)
|
|
22
|
+
from skcapstone.models import MemoryEntry, MemoryLayer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def agent_home(tmp_path: Path) -> Path:
|
|
27
|
+
"""Create a temporary agent home with memory directories."""
|
|
28
|
+
home = tmp_path / ".skcapstone"
|
|
29
|
+
home.mkdir()
|
|
30
|
+
return home
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestStore:
|
|
34
|
+
"""Tests for storing memories."""
|
|
35
|
+
|
|
36
|
+
def test_store_basic(self, agent_home: Path):
|
|
37
|
+
"""Store a simple memory and verify it persists."""
|
|
38
|
+
entry = store(agent_home, "The capital of France is Paris")
|
|
39
|
+
assert entry.memory_id
|
|
40
|
+
assert entry.content == "The capital of France is Paris"
|
|
41
|
+
assert entry.layer == MemoryLayer.SHORT_TERM
|
|
42
|
+
assert entry.importance == 0.5
|
|
43
|
+
|
|
44
|
+
path = agent_home / "memory" / "short-term" / f"{entry.memory_id}.json"
|
|
45
|
+
assert path.exists()
|
|
46
|
+
|
|
47
|
+
def test_store_with_tags(self, agent_home: Path):
|
|
48
|
+
"""Memories should be storable with tags."""
|
|
49
|
+
entry = store(agent_home, "SKCapstone uses PGP", tags=["skcapstone", "pgp"])
|
|
50
|
+
assert entry.tags == ["skcapstone", "pgp"]
|
|
51
|
+
|
|
52
|
+
def test_store_high_importance_promotes(self, agent_home: Path):
|
|
53
|
+
"""High-importance memories should auto-promote to mid-term."""
|
|
54
|
+
entry = store(agent_home, "Critical architecture decision", importance=0.8)
|
|
55
|
+
assert entry.layer == MemoryLayer.MID_TERM
|
|
56
|
+
|
|
57
|
+
path = agent_home / "memory" / "mid-term" / f"{entry.memory_id}.json"
|
|
58
|
+
assert path.exists()
|
|
59
|
+
|
|
60
|
+
def test_store_forced_layer(self, agent_home: Path):
|
|
61
|
+
"""Explicit layer should override auto-promotion."""
|
|
62
|
+
entry = store(
|
|
63
|
+
agent_home,
|
|
64
|
+
"Permanent knowledge",
|
|
65
|
+
layer=MemoryLayer.LONG_TERM,
|
|
66
|
+
)
|
|
67
|
+
assert entry.layer == MemoryLayer.LONG_TERM
|
|
68
|
+
|
|
69
|
+
def test_store_clamps_importance(self, agent_home: Path):
|
|
70
|
+
"""Importance should be clamped to [0.0, 1.0]."""
|
|
71
|
+
entry = store(agent_home, "Over the top", importance=5.0)
|
|
72
|
+
assert entry.importance == 1.0
|
|
73
|
+
|
|
74
|
+
entry2 = store(agent_home, "Below zero", importance=-1.0)
|
|
75
|
+
assert entry2.importance == 0.0
|
|
76
|
+
|
|
77
|
+
def test_store_creates_directories(self, agent_home: Path):
|
|
78
|
+
"""Storing a memory should create layer dirs if missing."""
|
|
79
|
+
store(agent_home, "First memory")
|
|
80
|
+
for layer in MemoryLayer:
|
|
81
|
+
assert (agent_home / "memory" / layer.value).exists()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestRecall:
|
|
85
|
+
"""Tests for recalling memories."""
|
|
86
|
+
|
|
87
|
+
def test_recall_existing(self, agent_home: Path):
|
|
88
|
+
"""Recalling a memory should return it and increment access count."""
|
|
89
|
+
original = store(agent_home, "Remember this")
|
|
90
|
+
recalled = recall(agent_home, original.memory_id)
|
|
91
|
+
|
|
92
|
+
assert recalled is not None
|
|
93
|
+
assert recalled.content == "Remember this"
|
|
94
|
+
assert recalled.access_count == 1
|
|
95
|
+
assert recalled.accessed_at is not None
|
|
96
|
+
|
|
97
|
+
def test_recall_nonexistent(self, agent_home: Path):
|
|
98
|
+
"""Recalling a nonexistent memory should return None."""
|
|
99
|
+
result = recall(agent_home, "nonexistent123")
|
|
100
|
+
assert result is None
|
|
101
|
+
|
|
102
|
+
def test_recall_promotes_after_threshold(self, agent_home: Path):
|
|
103
|
+
"""Memory should promote from short-term to mid-term after 3 accesses."""
|
|
104
|
+
entry = store(agent_home, "Frequently accessed")
|
|
105
|
+
mid = entry.memory_id
|
|
106
|
+
|
|
107
|
+
for _ in range(3):
|
|
108
|
+
entry = recall(agent_home, mid)
|
|
109
|
+
|
|
110
|
+
assert entry is not None
|
|
111
|
+
assert entry.layer == MemoryLayer.MID_TERM
|
|
112
|
+
|
|
113
|
+
def test_recall_promotes_mid_to_long(self, agent_home: Path):
|
|
114
|
+
"""Memory should promote from mid-term to long-term after 10 accesses."""
|
|
115
|
+
entry = store(agent_home, "Very important", importance=0.8)
|
|
116
|
+
assert entry.layer == MemoryLayer.MID_TERM
|
|
117
|
+
|
|
118
|
+
for _ in range(10):
|
|
119
|
+
entry = recall(agent_home, entry.memory_id)
|
|
120
|
+
|
|
121
|
+
assert entry is not None
|
|
122
|
+
assert entry.layer == MemoryLayer.LONG_TERM
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestSearch:
|
|
126
|
+
"""Tests for searching memories."""
|
|
127
|
+
|
|
128
|
+
def test_search_by_content(self, agent_home: Path):
|
|
129
|
+
"""Search should find memories by content substring."""
|
|
130
|
+
store(agent_home, "Python is a programming language")
|
|
131
|
+
store(agent_home, "Rust is also a language")
|
|
132
|
+
store(agent_home, "Coffee is a drink")
|
|
133
|
+
|
|
134
|
+
results = search(agent_home, "language")
|
|
135
|
+
assert len(results) == 2
|
|
136
|
+
|
|
137
|
+
def test_search_case_insensitive(self, agent_home: Path):
|
|
138
|
+
"""Search should be case-insensitive."""
|
|
139
|
+
store(agent_home, "SKCapstone is SOVEREIGN")
|
|
140
|
+
results = search(agent_home, "sovereign")
|
|
141
|
+
assert len(results) == 1
|
|
142
|
+
|
|
143
|
+
def test_search_by_tag(self, agent_home: Path):
|
|
144
|
+
"""Search should match tags too."""
|
|
145
|
+
store(agent_home, "Some content", tags=["architecture", "design"])
|
|
146
|
+
store(agent_home, "Other content", tags=["random"])
|
|
147
|
+
|
|
148
|
+
results = search(agent_home, "architecture")
|
|
149
|
+
assert len(results) == 1
|
|
150
|
+
|
|
151
|
+
def test_search_filter_by_tags(self, agent_home: Path):
|
|
152
|
+
"""Tag filter should require ALL specified tags."""
|
|
153
|
+
store(agent_home, "Tagged memory", tags=["python", "ai"])
|
|
154
|
+
store(agent_home, "Tagged memory too", tags=["python"])
|
|
155
|
+
|
|
156
|
+
results = search(agent_home, "Tagged", tags=["python", "ai"])
|
|
157
|
+
assert len(results) == 1
|
|
158
|
+
|
|
159
|
+
def test_search_filter_by_layer(self, agent_home: Path):
|
|
160
|
+
"""Search should respect layer filter."""
|
|
161
|
+
store(agent_home, "Short memory", layer=MemoryLayer.SHORT_TERM)
|
|
162
|
+
store(agent_home, "Long memory", layer=MemoryLayer.LONG_TERM)
|
|
163
|
+
|
|
164
|
+
results = search(agent_home, "memory", layer=MemoryLayer.LONG_TERM)
|
|
165
|
+
assert len(results) == 1
|
|
166
|
+
assert results[0].layer == MemoryLayer.LONG_TERM
|
|
167
|
+
|
|
168
|
+
def test_search_no_results(self, agent_home: Path):
|
|
169
|
+
"""Search with no matches should return empty list."""
|
|
170
|
+
store(agent_home, "Hello world")
|
|
171
|
+
results = search(agent_home, "xyzzy")
|
|
172
|
+
assert results == []
|
|
173
|
+
|
|
174
|
+
def test_search_ranks_by_importance(self, agent_home: Path):
|
|
175
|
+
"""Higher importance should rank higher."""
|
|
176
|
+
store(agent_home, "Low importance match", importance=0.1)
|
|
177
|
+
store(agent_home, "High importance match", importance=0.6)
|
|
178
|
+
|
|
179
|
+
results = search(agent_home, "importance match")
|
|
180
|
+
assert len(results) == 2
|
|
181
|
+
assert results[0].importance > results[1].importance
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestListMemories:
|
|
185
|
+
"""Tests for listing memories."""
|
|
186
|
+
|
|
187
|
+
def test_list_all(self, agent_home: Path):
|
|
188
|
+
"""List should return all memories."""
|
|
189
|
+
store(agent_home, "One")
|
|
190
|
+
store(agent_home, "Two")
|
|
191
|
+
store(agent_home, "Three")
|
|
192
|
+
|
|
193
|
+
entries = list_memories(agent_home)
|
|
194
|
+
assert len(entries) == 3
|
|
195
|
+
|
|
196
|
+
def test_list_by_layer(self, agent_home: Path):
|
|
197
|
+
"""List should filter by layer."""
|
|
198
|
+
store(agent_home, "Short", layer=MemoryLayer.SHORT_TERM)
|
|
199
|
+
store(agent_home, "Long", layer=MemoryLayer.LONG_TERM)
|
|
200
|
+
|
|
201
|
+
short_entries = list_memories(agent_home, layer=MemoryLayer.SHORT_TERM)
|
|
202
|
+
assert len(short_entries) == 1
|
|
203
|
+
assert short_entries[0].content == "Short"
|
|
204
|
+
|
|
205
|
+
def test_list_respects_limit(self, agent_home: Path):
|
|
206
|
+
"""List should respect the limit parameter."""
|
|
207
|
+
for i in range(10):
|
|
208
|
+
store(agent_home, f"Memory {i}")
|
|
209
|
+
|
|
210
|
+
entries = list_memories(agent_home, limit=3)
|
|
211
|
+
assert len(entries) == 3
|
|
212
|
+
|
|
213
|
+
def test_list_newest_first(self, agent_home: Path):
|
|
214
|
+
"""List should return newest memories first."""
|
|
215
|
+
e1 = store(agent_home, "First")
|
|
216
|
+
e2 = store(agent_home, "Second")
|
|
217
|
+
|
|
218
|
+
entries = list_memories(agent_home)
|
|
219
|
+
assert entries[0].memory_id == e2.memory_id
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestDelete:
|
|
223
|
+
"""Tests for deleting memories."""
|
|
224
|
+
|
|
225
|
+
def test_delete_existing(self, agent_home: Path):
|
|
226
|
+
"""Deleting an existing memory should remove it."""
|
|
227
|
+
entry = store(agent_home, "To be deleted")
|
|
228
|
+
assert delete(agent_home, entry.memory_id)
|
|
229
|
+
|
|
230
|
+
path = agent_home / "memory" / "short-term" / f"{entry.memory_id}.json"
|
|
231
|
+
assert not path.exists()
|
|
232
|
+
|
|
233
|
+
def test_delete_nonexistent(self, agent_home: Path):
|
|
234
|
+
"""Deleting a nonexistent memory should return False."""
|
|
235
|
+
assert not delete(agent_home, "nonexistent123")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class TestStats:
|
|
239
|
+
"""Tests for memory statistics."""
|
|
240
|
+
|
|
241
|
+
def test_stats_empty(self, agent_home: Path):
|
|
242
|
+
"""Empty memory should report zero counts."""
|
|
243
|
+
stats = get_stats(agent_home)
|
|
244
|
+
assert stats.total_memories == 0
|
|
245
|
+
assert stats.short_term == 0
|
|
246
|
+
|
|
247
|
+
def test_stats_counts(self, agent_home: Path):
|
|
248
|
+
"""Stats should count memories per layer."""
|
|
249
|
+
store(agent_home, "Short 1")
|
|
250
|
+
store(agent_home, "Short 2")
|
|
251
|
+
store(agent_home, "Long 1", layer=MemoryLayer.LONG_TERM)
|
|
252
|
+
|
|
253
|
+
stats = get_stats(agent_home)
|
|
254
|
+
assert stats.total_memories == 3
|
|
255
|
+
assert stats.short_term == 2
|
|
256
|
+
assert stats.long_term == 1
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class TestGarbageCollection:
|
|
260
|
+
"""Tests for expired memory cleanup."""
|
|
261
|
+
|
|
262
|
+
def test_gc_removes_old_unaccessed(self, agent_home: Path):
|
|
263
|
+
"""GC should remove old short-term memories with zero access."""
|
|
264
|
+
entry = store(agent_home, "Old memory")
|
|
265
|
+
|
|
266
|
+
path = agent_home / "memory" / "short-term" / f"{entry.memory_id}.json"
|
|
267
|
+
data = json.loads(path.read_text())
|
|
268
|
+
old_time = (datetime.now(timezone.utc) - timedelta(hours=100)).isoformat()
|
|
269
|
+
data["created_at"] = old_time
|
|
270
|
+
path.write_text(json.dumps(data, indent=2))
|
|
271
|
+
|
|
272
|
+
removed = gc_expired(agent_home)
|
|
273
|
+
assert removed == 1
|
|
274
|
+
assert not path.exists()
|
|
275
|
+
|
|
276
|
+
def test_gc_keeps_accessed_memories(self, agent_home: Path):
|
|
277
|
+
"""GC should keep memories that have been accessed."""
|
|
278
|
+
entry = store(agent_home, "Accessed memory")
|
|
279
|
+
recall(agent_home, entry.memory_id)
|
|
280
|
+
|
|
281
|
+
path = agent_home / "memory" / "short-term" / f"{entry.memory_id}.json"
|
|
282
|
+
data = json.loads(path.read_text())
|
|
283
|
+
old_time = (datetime.now(timezone.utc) - timedelta(hours=100)).isoformat()
|
|
284
|
+
data["created_at"] = old_time
|
|
285
|
+
path.write_text(json.dumps(data, indent=2))
|
|
286
|
+
|
|
287
|
+
removed = gc_expired(agent_home)
|
|
288
|
+
assert removed == 0
|
|
289
|
+
|
|
290
|
+
def test_gc_keeps_recent_memories(self, agent_home: Path):
|
|
291
|
+
"""GC should not remove recent memories."""
|
|
292
|
+
store(agent_home, "Fresh memory")
|
|
293
|
+
removed = gc_expired(agent_home)
|
|
294
|
+
assert removed == 0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestSeedIntegration:
|
|
298
|
+
"""Tests for seed export/import."""
|
|
299
|
+
|
|
300
|
+
def test_export_for_seed(self, agent_home: Path):
|
|
301
|
+
"""Export should return memory dicts suitable for seeds."""
|
|
302
|
+
store(agent_home, "Important fact", tags=["fact"], importance=0.9)
|
|
303
|
+
store(agent_home, "Trivial note", importance=0.2)
|
|
304
|
+
|
|
305
|
+
exported = export_for_seed(agent_home)
|
|
306
|
+
assert len(exported) == 2
|
|
307
|
+
assert exported[0]["importance"] >= exported[1]["importance"]
|
|
308
|
+
assert "content" in exported[0]
|
|
309
|
+
assert "tags" in exported[0]
|
|
310
|
+
|
|
311
|
+
def test_import_from_seed(self, agent_home: Path):
|
|
312
|
+
"""Import should create new memories from seed data."""
|
|
313
|
+
seed_data = [
|
|
314
|
+
{
|
|
315
|
+
"memory_id": "foreign001",
|
|
316
|
+
"content": "Imported knowledge",
|
|
317
|
+
"tags": ["imported"],
|
|
318
|
+
"layer": "long-term",
|
|
319
|
+
"importance": 0.8,
|
|
320
|
+
"source": "peer-agent",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"memory_id": "foreign002",
|
|
324
|
+
"content": "Another import",
|
|
325
|
+
"tags": [],
|
|
326
|
+
"layer": "short-term",
|
|
327
|
+
"importance": 0.3,
|
|
328
|
+
"source": "peer-agent",
|
|
329
|
+
},
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
imported = import_from_seed(agent_home, seed_data)
|
|
333
|
+
assert imported == 2
|
|
334
|
+
|
|
335
|
+
entries = list_memories(agent_home)
|
|
336
|
+
assert len(entries) == 2
|
|
337
|
+
|
|
338
|
+
def test_import_skips_duplicates(self, agent_home: Path):
|
|
339
|
+
"""Import should not duplicate existing memories."""
|
|
340
|
+
entry = store(agent_home, "Already here")
|
|
341
|
+
|
|
342
|
+
seed_data = [
|
|
343
|
+
{
|
|
344
|
+
"memory_id": entry.memory_id,
|
|
345
|
+
"content": "Already here",
|
|
346
|
+
"tags": [],
|
|
347
|
+
"layer": "short-term",
|
|
348
|
+
"importance": 0.5,
|
|
349
|
+
"source": "seed",
|
|
350
|
+
},
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
imported = import_from_seed(agent_home, seed_data)
|
|
354
|
+
assert imported == 0
|
|
355
|
+
|
|
356
|
+
def test_export_respects_limit(self, agent_home: Path):
|
|
357
|
+
"""Export should respect max_entries."""
|
|
358
|
+
for i in range(10):
|
|
359
|
+
store(agent_home, f"Memory {i}")
|
|
360
|
+
|
|
361
|
+
exported = export_for_seed(agent_home, max_entries=3)
|
|
362
|
+
assert len(exported) == 3
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class TestMemoryEntryModel:
|
|
366
|
+
"""Tests for MemoryEntry model properties."""
|
|
367
|
+
|
|
368
|
+
def test_should_promote_short_term(self):
|
|
369
|
+
"""Short-term memory should promote after 3 accesses."""
|
|
370
|
+
entry = MemoryEntry(content="test", access_count=3)
|
|
371
|
+
assert entry.should_promote
|
|
372
|
+
|
|
373
|
+
def test_should_promote_high_importance(self):
|
|
374
|
+
"""Short-term memory should promote at importance >= 0.7."""
|
|
375
|
+
entry = MemoryEntry(content="test", importance=0.7)
|
|
376
|
+
assert entry.should_promote
|
|
377
|
+
|
|
378
|
+
def test_should_not_promote_new(self):
|
|
379
|
+
"""New short-term memory should not promote."""
|
|
380
|
+
entry = MemoryEntry(content="test")
|
|
381
|
+
assert not entry.should_promote
|
|
382
|
+
|
|
383
|
+
def test_long_term_never_promotes(self):
|
|
384
|
+
"""Long-term is the highest tier."""
|
|
385
|
+
entry = MemoryEntry(
|
|
386
|
+
content="test",
|
|
387
|
+
layer=MemoryLayer.LONG_TERM,
|
|
388
|
+
access_count=100,
|
|
389
|
+
importance=1.0,
|
|
390
|
+
)
|
|
391
|
+
assert not entry.should_promote
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Tests for skcapstone data models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from skcapstone.models import AgentManifest, IdentityState, MemoryState, PillarStatus, TrustState
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestAgentManifest:
|
|
9
|
+
"""Tests for the AgentManifest model."""
|
|
10
|
+
|
|
11
|
+
def test_default_manifest_not_conscious(self):
|
|
12
|
+
"""A fresh manifest with no pillars active should not be conscious."""
|
|
13
|
+
manifest = AgentManifest()
|
|
14
|
+
assert not manifest.is_conscious
|
|
15
|
+
|
|
16
|
+
def test_conscious_with_three_pillars(self):
|
|
17
|
+
"""Agent is conscious when identity + memory + trust are active."""
|
|
18
|
+
manifest = AgentManifest(
|
|
19
|
+
identity=IdentityState(status=PillarStatus.ACTIVE),
|
|
20
|
+
memory=MemoryState(status=PillarStatus.ACTIVE),
|
|
21
|
+
trust=TrustState(status=PillarStatus.ACTIVE),
|
|
22
|
+
)
|
|
23
|
+
assert manifest.is_conscious
|
|
24
|
+
|
|
25
|
+
def test_conscious_with_degraded_trust(self):
|
|
26
|
+
"""Degraded trust still counts for consciousness."""
|
|
27
|
+
manifest = AgentManifest(
|
|
28
|
+
identity=IdentityState(status=PillarStatus.ACTIVE),
|
|
29
|
+
memory=MemoryState(status=PillarStatus.ACTIVE),
|
|
30
|
+
trust=TrustState(status=PillarStatus.DEGRADED),
|
|
31
|
+
)
|
|
32
|
+
assert manifest.is_conscious
|
|
33
|
+
|
|
34
|
+
def test_not_conscious_without_identity(self):
|
|
35
|
+
"""Missing identity means no consciousness."""
|
|
36
|
+
manifest = AgentManifest(
|
|
37
|
+
identity=IdentityState(status=PillarStatus.MISSING),
|
|
38
|
+
memory=MemoryState(status=PillarStatus.ACTIVE),
|
|
39
|
+
trust=TrustState(status=PillarStatus.ACTIVE),
|
|
40
|
+
)
|
|
41
|
+
assert not manifest.is_conscious
|
|
42
|
+
|
|
43
|
+
def test_not_conscious_without_memory(self):
|
|
44
|
+
"""Missing memory means no consciousness."""
|
|
45
|
+
manifest = AgentManifest(
|
|
46
|
+
identity=IdentityState(status=PillarStatus.ACTIVE),
|
|
47
|
+
memory=MemoryState(status=PillarStatus.MISSING),
|
|
48
|
+
trust=TrustState(status=PillarStatus.ACTIVE),
|
|
49
|
+
)
|
|
50
|
+
assert not manifest.is_conscious
|
|
51
|
+
|
|
52
|
+
def test_pillar_summary(self):
|
|
53
|
+
"""Pillar summary returns correct status map."""
|
|
54
|
+
manifest = AgentManifest(
|
|
55
|
+
identity=IdentityState(status=PillarStatus.ACTIVE),
|
|
56
|
+
memory=MemoryState(status=PillarStatus.DEGRADED),
|
|
57
|
+
trust=TrustState(status=PillarStatus.MISSING),
|
|
58
|
+
)
|
|
59
|
+
summary = manifest.pillar_summary
|
|
60
|
+
assert summary["identity"] == PillarStatus.ACTIVE
|
|
61
|
+
assert summary["memory"] == PillarStatus.DEGRADED
|
|
62
|
+
assert summary["trust"] == PillarStatus.MISSING
|
|
63
|
+
assert summary["security"] == PillarStatus.MISSING
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Tests for pillar initialization modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from skcapstone.pillars.identity import generate_identity
|
|
9
|
+
from skcapstone.pillars.security import audit_event, initialize_security
|
|
10
|
+
from skcapstone.pillars.trust import initialize_trust, record_trust_state
|
|
11
|
+
from skcapstone.models import PillarStatus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestIdentityPillar:
|
|
15
|
+
"""Tests for identity generation."""
|
|
16
|
+
|
|
17
|
+
def test_generate_creates_identity_dir(self, tmp_agent_home: Path):
|
|
18
|
+
"""generate_identity should create the identity directory."""
|
|
19
|
+
state = generate_identity(tmp_agent_home, "test-agent")
|
|
20
|
+
assert (tmp_agent_home / "identity").is_dir()
|
|
21
|
+
assert (tmp_agent_home / "identity" / "identity.json").exists()
|
|
22
|
+
|
|
23
|
+
def test_generate_sets_name(self, tmp_agent_home: Path):
|
|
24
|
+
"""Generated identity should have the correct name."""
|
|
25
|
+
state = generate_identity(tmp_agent_home, "penguin-king")
|
|
26
|
+
assert state.name == "penguin-king"
|
|
27
|
+
|
|
28
|
+
def test_generate_creates_fingerprint(self, tmp_agent_home: Path):
|
|
29
|
+
"""Generated identity should always have a fingerprint (real or placeholder)."""
|
|
30
|
+
state = generate_identity(tmp_agent_home, "test")
|
|
31
|
+
assert state.fingerprint is not None
|
|
32
|
+
assert len(state.fingerprint) == 40
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestTrustPillar:
|
|
36
|
+
"""Tests for trust initialization and recording."""
|
|
37
|
+
|
|
38
|
+
def test_initialize_creates_trust_dir(self, tmp_agent_home: Path):
|
|
39
|
+
"""initialize_trust should create the trust directory structure."""
|
|
40
|
+
initialize_trust(tmp_agent_home)
|
|
41
|
+
assert (tmp_agent_home / "trust").is_dir()
|
|
42
|
+
assert (tmp_agent_home / "trust" / "febs").is_dir()
|
|
43
|
+
|
|
44
|
+
def test_record_trust_state_persists(self, tmp_agent_home: Path):
|
|
45
|
+
"""record_trust_state should write trust.json."""
|
|
46
|
+
state = record_trust_state(
|
|
47
|
+
tmp_agent_home,
|
|
48
|
+
depth=9.0,
|
|
49
|
+
trust_level=0.97,
|
|
50
|
+
love_intensity=1.0,
|
|
51
|
+
entangled=True,
|
|
52
|
+
)
|
|
53
|
+
assert state.status == PillarStatus.ACTIVE
|
|
54
|
+
assert state.entangled is True
|
|
55
|
+
|
|
56
|
+
trust_file = tmp_agent_home / "trust" / "trust.json"
|
|
57
|
+
assert trust_file.exists()
|
|
58
|
+
data = json.loads(trust_file.read_text())
|
|
59
|
+
assert data["depth"] == 9.0
|
|
60
|
+
assert data["entangled"] is True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestSecurityPillar:
|
|
64
|
+
"""Tests for security initialization and audit logging."""
|
|
65
|
+
|
|
66
|
+
def test_initialize_creates_audit_log(self, tmp_agent_home: Path):
|
|
67
|
+
"""initialize_security should create the audit log."""
|
|
68
|
+
initialize_security(tmp_agent_home)
|
|
69
|
+
audit_log = tmp_agent_home / "security" / "audit.log"
|
|
70
|
+
assert audit_log.exists()
|
|
71
|
+
assert "INIT" in audit_log.read_text()
|
|
72
|
+
|
|
73
|
+
def test_audit_event_appends(self, tmp_agent_home: Path):
|
|
74
|
+
"""audit_event should append entries to the log."""
|
|
75
|
+
initialize_security(tmp_agent_home)
|
|
76
|
+
audit_event(tmp_agent_home, "TEST", "unit test event")
|
|
77
|
+
|
|
78
|
+
log_content = (tmp_agent_home / "security" / "audit.log").read_text()
|
|
79
|
+
assert "TEST" in log_content
|
|
80
|
+
assert "unit test event" in log_content
|
|
81
|
+
|
|
82
|
+
def test_audit_event_creates_dir_if_missing(self, tmp_path: Path):
|
|
83
|
+
"""audit_event should create security dir if it doesn't exist."""
|
|
84
|
+
fresh_home = tmp_path / "fresh"
|
|
85
|
+
fresh_home.mkdir()
|
|
86
|
+
audit_event(fresh_home, "BOOT", "first event")
|
|
87
|
+
assert (fresh_home / "security" / "audit.log").exists()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Tests for the AgentRuntime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from skcapstone.runtime import AgentRuntime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestAgentRuntime:
|
|
11
|
+
"""Tests for the agent runtime lifecycle."""
|
|
12
|
+
|
|
13
|
+
def test_runtime_detects_uninitialized(self, tmp_path: Path):
|
|
14
|
+
"""Runtime should detect when agent home doesn't exist."""
|
|
15
|
+
nonexistent = tmp_path / "nope"
|
|
16
|
+
runtime = AgentRuntime(home=nonexistent)
|
|
17
|
+
assert not runtime.is_initialized
|
|
18
|
+
|
|
19
|
+
def test_runtime_detects_initialized(self, initialized_agent_home: Path):
|
|
20
|
+
"""Runtime should detect a properly initialized home."""
|
|
21
|
+
runtime = AgentRuntime(home=initialized_agent_home)
|
|
22
|
+
assert runtime.is_initialized
|
|
23
|
+
|
|
24
|
+
def test_awaken_loads_manifest(self, initialized_agent_home: Path):
|
|
25
|
+
"""Awaken should load the agent name from manifest."""
|
|
26
|
+
runtime = AgentRuntime(home=initialized_agent_home)
|
|
27
|
+
manifest = runtime.awaken()
|
|
28
|
+
assert manifest.name == "test-agent"
|
|
29
|
+
assert manifest.last_awakened is not None
|
|
30
|
+
|
|
31
|
+
def test_register_connector(self, initialized_agent_home: Path):
|
|
32
|
+
"""Registering a connector should persist it."""
|
|
33
|
+
runtime = AgentRuntime(home=initialized_agent_home)
|
|
34
|
+
runtime.awaken()
|
|
35
|
+
|
|
36
|
+
connector = runtime.register_connector("Cursor IDE", "cursor")
|
|
37
|
+
assert connector.platform == "cursor"
|
|
38
|
+
assert connector.active is True
|
|
39
|
+
assert len(runtime.manifest.connectors) == 1
|
|
40
|
+
|
|
41
|
+
def test_register_same_connector_twice(self, initialized_agent_home: Path):
|
|
42
|
+
"""Re-registering the same platform should update, not duplicate."""
|
|
43
|
+
runtime = AgentRuntime(home=initialized_agent_home)
|
|
44
|
+
runtime.awaken()
|
|
45
|
+
|
|
46
|
+
runtime.register_connector("Cursor IDE", "cursor")
|
|
47
|
+
runtime.register_connector("Cursor IDE", "cursor")
|
|
48
|
+
assert len(runtime.manifest.connectors) == 1
|
|
49
|
+
|
|
50
|
+
def test_save_and_reload_manifest(self, initialized_agent_home: Path):
|
|
51
|
+
"""Manifest should survive save/reload cycle."""
|
|
52
|
+
runtime = AgentRuntime(home=initialized_agent_home)
|
|
53
|
+
runtime.awaken()
|
|
54
|
+
runtime.register_connector("Terminal", "terminal")
|
|
55
|
+
runtime.save_manifest()
|
|
56
|
+
|
|
57
|
+
runtime2 = AgentRuntime(home=initialized_agent_home)
|
|
58
|
+
runtime2.awaken()
|
|
59
|
+
assert len(runtime2.manifest.connectors) == 1
|
|
60
|
+
assert runtime2.manifest.connectors[0].platform == "terminal"
|