@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.
Files changed (50) hide show
  1. package/.cursorrules +33 -0
  2. package/.github/workflows/ci.yml +23 -0
  3. package/.github/workflows/publish.yml +52 -0
  4. package/AGENTS.md +74 -0
  5. package/CLAUDE.md +56 -0
  6. package/LICENSE +674 -0
  7. package/README.md +242 -0
  8. package/SKILL.md +36 -0
  9. package/bin/cli.js +18 -0
  10. package/docs/ARCHITECTURE.md +510 -0
  11. package/docs/SECURITY_DESIGN.md +315 -0
  12. package/docs/SOVEREIGN_SINGULARITY.md +371 -0
  13. package/docs/TOKEN_SYSTEM.md +201 -0
  14. package/index.d.ts +9 -0
  15. package/index.js +32 -0
  16. package/package.json +32 -0
  17. package/pyproject.toml +84 -0
  18. package/src/skcapstone/__init__.py +13 -0
  19. package/src/skcapstone/cli.py +1441 -0
  20. package/src/skcapstone/connectors/__init__.py +6 -0
  21. package/src/skcapstone/coordination.py +590 -0
  22. package/src/skcapstone/discovery.py +275 -0
  23. package/src/skcapstone/memory_engine.py +457 -0
  24. package/src/skcapstone/models.py +223 -0
  25. package/src/skcapstone/pillars/__init__.py +8 -0
  26. package/src/skcapstone/pillars/identity.py +91 -0
  27. package/src/skcapstone/pillars/memory.py +61 -0
  28. package/src/skcapstone/pillars/security.py +83 -0
  29. package/src/skcapstone/pillars/sync.py +486 -0
  30. package/src/skcapstone/pillars/trust.py +335 -0
  31. package/src/skcapstone/runtime.py +190 -0
  32. package/src/skcapstone/skills/__init__.py +1 -0
  33. package/src/skcapstone/skills/syncthing_setup.py +297 -0
  34. package/src/skcapstone/sync/__init__.py +14 -0
  35. package/src/skcapstone/sync/backends.py +330 -0
  36. package/src/skcapstone/sync/engine.py +301 -0
  37. package/src/skcapstone/sync/models.py +97 -0
  38. package/src/skcapstone/sync/vault.py +284 -0
  39. package/src/skcapstone/tokens.py +439 -0
  40. package/tests/__init__.py +0 -0
  41. package/tests/conftest.py +42 -0
  42. package/tests/test_coordination.py +299 -0
  43. package/tests/test_discovery.py +57 -0
  44. package/tests/test_memory_engine.py +391 -0
  45. package/tests/test_models.py +63 -0
  46. package/tests/test_pillars.py +87 -0
  47. package/tests/test_runtime.py +60 -0
  48. package/tests/test_sync.py +507 -0
  49. package/tests/test_syncthing_setup.py +76 -0
  50. 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"