@smilintux/skcapstone 0.1.0 → 0.2.3

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 (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. package/src/skcapstone/cli.py +0 -1441
@@ -0,0 +1,395 @@
1
+ """Tests for the PeerDirectory transport address registry.
2
+
3
+ Covers:
4
+ - load() on empty / missing / malformed directory
5
+ - add_peer() — creates entry, persists to YAML, overwrites on re-add
6
+ - remove_peer() — removes known peer, returns False for unknown
7
+ - resolve() — returns address or None
8
+ - list_peers() — empty and with entries, sorted
9
+ - update_last_seen() — timestamps known peer, no-op on unknown
10
+ - auto_discover() — discovers from heartbeats dir and outbox dirs
11
+ - YAML persistence round-trip
12
+ - CLI: peers list, peers add, peers discover
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ import pytest
22
+ import yaml
23
+ from click.testing import CliRunner
24
+
25
+ from skcapstone.peer_directory import DirectoryEntry, PeerDirectory
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Helpers
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def make_directory(tmp_path: Path) -> PeerDirectory:
34
+ """Return a PeerDirectory rooted at tmp_path."""
35
+ return PeerDirectory(home=tmp_path)
36
+
37
+
38
+ def write_heartbeat(hb_dir: Path, agent: str, fingerprint: str = "") -> None:
39
+ """Write a minimal heartbeat JSON file."""
40
+ hb_dir.mkdir(parents=True, exist_ok=True)
41
+ data = {
42
+ "agent_name": agent,
43
+ "status": "alive",
44
+ "hostname": f"{agent}-host",
45
+ "platform": "Linux x86_64",
46
+ "timestamp": datetime.now(timezone.utc).isoformat(),
47
+ "ttl_seconds": 300,
48
+ "fingerprint": fingerprint,
49
+ }
50
+ (hb_dir / f"{agent}.json").write_text(json.dumps(data), encoding="utf-8")
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Test 1: Empty directory
55
+ # ---------------------------------------------------------------------------
56
+
57
+
58
+ class TestLoad:
59
+ """Test load() behaviour."""
60
+
61
+ def test_load_missing_file(self, tmp_path):
62
+ """load() on a fresh home returns empty dict without error."""
63
+ d = make_directory(tmp_path)
64
+ result = d.load()
65
+ assert result == {}
66
+
67
+ def test_load_empty_yaml(self, tmp_path):
68
+ """load() on an empty YAML file returns empty dict."""
69
+ peers_dir = tmp_path / "peers"
70
+ peers_dir.mkdir()
71
+ (peers_dir / "directory.yaml").write_text("", encoding="utf-8")
72
+ d = make_directory(tmp_path)
73
+ assert d.load() == {}
74
+
75
+ def test_load_malformed_yaml_is_safe(self, tmp_path):
76
+ """load() on malformed YAML does not raise, returns empty."""
77
+ peers_dir = tmp_path / "peers"
78
+ peers_dir.mkdir()
79
+ (peers_dir / "directory.yaml").write_text("{{{invalid yaml", encoding="utf-8")
80
+ d = make_directory(tmp_path)
81
+ result = d.load()
82
+ # Graceful degradation — no exception
83
+ assert isinstance(result, dict)
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Test 2: add_peer / resolve
88
+ # ---------------------------------------------------------------------------
89
+
90
+
91
+ class TestAddAndResolve:
92
+ """Test add_peer() and resolve()."""
93
+
94
+ def test_add_peer_creates_entry(self, tmp_path):
95
+ """add_peer() returns a DirectoryEntry with correct fields."""
96
+ d = make_directory(tmp_path)
97
+ entry = d.add_peer(
98
+ name="Lumina",
99
+ address="/home/user/.skcapstone/sync/comms/outbox/lumina",
100
+ transport="syncthing",
101
+ fingerprint="AABB1122",
102
+ )
103
+ assert entry.name == "lumina"
104
+ assert entry.address == "/home/user/.skcapstone/sync/comms/outbox/lumina"
105
+ assert entry.transport == "syncthing"
106
+ assert entry.fingerprint == "AABB1122"
107
+ assert entry.last_seen is not None
108
+
109
+ def test_resolve_known_peer(self, tmp_path):
110
+ """resolve() returns the address of a known peer."""
111
+ d = make_directory(tmp_path)
112
+ d.add_peer("Opus", "/path/to/outbox/opus")
113
+ assert d.resolve("Opus") == "/path/to/outbox/opus"
114
+
115
+ def test_resolve_case_insensitive(self, tmp_path):
116
+ """resolve() is case-insensitive."""
117
+ d = make_directory(tmp_path)
118
+ d.add_peer("LUMINA", "/outbox/lumina")
119
+ assert d.resolve("lumina") == "/outbox/lumina"
120
+ assert d.resolve("LUMINA") == "/outbox/lumina"
121
+ assert d.resolve("Lumina") == "/outbox/lumina"
122
+
123
+ def test_resolve_unknown_returns_none(self, tmp_path):
124
+ """resolve() returns None for an unknown peer."""
125
+ d = make_directory(tmp_path)
126
+ assert d.resolve("nobody") is None
127
+
128
+ def test_add_peer_overwrites(self, tmp_path):
129
+ """Adding the same peer twice updates the entry."""
130
+ d = make_directory(tmp_path)
131
+ d.add_peer("Jarvis", "/old/path")
132
+ d.add_peer("Jarvis", "/new/path", transport="tailscale")
133
+ assert d.resolve("jarvis") == "/new/path"
134
+ # List should still show only one entry
135
+ peers = d.list_peers()
136
+ assert len([p for p in peers if p.name == "jarvis"]) == 1
137
+
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # Test 3: remove_peer
141
+ # ---------------------------------------------------------------------------
142
+
143
+
144
+ class TestRemovePeer:
145
+ """Test remove_peer()."""
146
+
147
+ def test_remove_existing(self, tmp_path):
148
+ """remove_peer() removes a known peer and returns True."""
149
+ d = make_directory(tmp_path)
150
+ d.add_peer("Grok", "/outbox/grok")
151
+ assert d.remove_peer("Grok") is True
152
+ assert d.resolve("grok") is None
153
+
154
+ def test_remove_unknown_returns_false(self, tmp_path):
155
+ """remove_peer() returns False when peer is not in directory."""
156
+ d = make_directory(tmp_path)
157
+ assert d.remove_peer("nobody") is False
158
+
159
+ def test_remove_case_insensitive(self, tmp_path):
160
+ """remove_peer() handles mixed case."""
161
+ d = make_directory(tmp_path)
162
+ d.add_peer("Ava", "/outbox/ava")
163
+ assert d.remove_peer("AVA") is True
164
+ assert d.resolve("ava") is None
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Test 4: list_peers
169
+ # ---------------------------------------------------------------------------
170
+
171
+
172
+ class TestListPeers:
173
+ """Test list_peers()."""
174
+
175
+ def test_empty(self, tmp_path):
176
+ """list_peers() on empty directory returns []."""
177
+ d = make_directory(tmp_path)
178
+ assert d.list_peers() == []
179
+
180
+ def test_sorted_alphabetically(self, tmp_path):
181
+ """list_peers() returns entries sorted by name."""
182
+ d = make_directory(tmp_path)
183
+ d.add_peer("Zeta", "/z")
184
+ d.add_peer("Alpha", "/a")
185
+ d.add_peer("Mango", "/m")
186
+ names = [p.name for p in d.list_peers()]
187
+ assert names == sorted(names)
188
+
189
+ def test_returns_all_peers(self, tmp_path):
190
+ """list_peers() returns every added peer."""
191
+ d = make_directory(tmp_path)
192
+ for i in range(5):
193
+ d.add_peer(f"agent{i}", f"/outbox/agent{i}")
194
+ assert len(d.list_peers()) == 5
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Test 5: update_last_seen
199
+ # ---------------------------------------------------------------------------
200
+
201
+
202
+ class TestUpdateLastSeen:
203
+ """Test update_last_seen()."""
204
+
205
+ def test_updates_timestamp(self, tmp_path):
206
+ """update_last_seen() sets a new ISO timestamp for a known peer."""
207
+ d = make_directory(tmp_path)
208
+ d.add_peer("Opus", "/outbox/opus")
209
+ old_ts = d.resolve("opus") # address won't change, but we want to check last_seen
210
+
211
+ import time
212
+ time.sleep(0.01) # ensure clock advances
213
+
214
+ d.update_last_seen("Opus")
215
+ entry = d.list_peers()[0]
216
+ assert entry.last_seen is not None
217
+
218
+ def test_noop_on_unknown(self, tmp_path):
219
+ """update_last_seen() on an unknown peer does not raise."""
220
+ d = make_directory(tmp_path)
221
+ d.update_last_seen("ghost") # must not raise
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # Test 6: auto_discover
226
+ # ---------------------------------------------------------------------------
227
+
228
+
229
+ class TestAutoDiscover:
230
+ """Test auto_discover()."""
231
+
232
+ def test_discover_from_heartbeats(self, tmp_path):
233
+ """auto_discover() adds peers found in heartbeat files."""
234
+ hb_dir = tmp_path / "heartbeats"
235
+ write_heartbeat(hb_dir, "lumina", fingerprint="FP123")
236
+ write_heartbeat(hb_dir, "jarvis")
237
+
238
+ d = make_directory(tmp_path)
239
+ added = d.auto_discover(heartbeats_dir=hb_dir)
240
+
241
+ names = {e.name for e in added}
242
+ assert "lumina" in names
243
+ assert "jarvis" in names
244
+ # Transport should default to syncthing
245
+ assert all(e.transport == "syncthing" for e in added)
246
+
247
+ def test_discover_from_outbox_dirs(self, tmp_path):
248
+ """auto_discover() adds peers from Syncthing outbox directories."""
249
+ outbox_root = tmp_path / "sync" / "comms" / "outbox"
250
+ (outbox_root / "ava").mkdir(parents=True)
251
+ (outbox_root / "mcp-builder").mkdir(parents=True)
252
+
253
+ d = make_directory(tmp_path)
254
+ added = d.auto_discover()
255
+
256
+ names = {e.name for e in added}
257
+ assert "ava" in names
258
+ assert "mcp-builder" in names
259
+
260
+ def test_discover_skips_known(self, tmp_path):
261
+ """auto_discover() does not overwrite existing entries."""
262
+ hb_dir = tmp_path / "heartbeats"
263
+ write_heartbeat(hb_dir, "lumina")
264
+
265
+ d = make_directory(tmp_path)
266
+ d.add_peer("lumina", "/custom/path")
267
+ added = d.auto_discover(heartbeats_dir=hb_dir)
268
+
269
+ # lumina was already known — should not appear in added
270
+ added_names = {e.name for e in added}
271
+ assert "lumina" not in added_names
272
+ # And the existing address must be preserved
273
+ assert d.resolve("lumina") == "/custom/path"
274
+
275
+ def test_discover_empty_dirs(self, tmp_path):
276
+ """auto_discover() on empty dirs returns empty list."""
277
+ d = make_directory(tmp_path)
278
+ added = d.auto_discover()
279
+ assert added == []
280
+
281
+
282
+ # ---------------------------------------------------------------------------
283
+ # Test 7: YAML persistence round-trip
284
+ # ---------------------------------------------------------------------------
285
+
286
+
287
+ class TestPersistence:
288
+ """Test that entries survive a full save → load cycle."""
289
+
290
+ def test_round_trip(self, tmp_path):
291
+ """Entries written by one PeerDirectory instance are readable by another."""
292
+ d1 = make_directory(tmp_path)
293
+ d1.add_peer("Lumina", "/outbox/lumina", transport="syncthing", fingerprint="FP99")
294
+ d1.add_peer("Grok", "/outbox/grok", transport="tailscale")
295
+
296
+ # Fresh instance — reads from disk
297
+ d2 = make_directory(tmp_path)
298
+ d2.load()
299
+ assert d2.resolve("lumina") == "/outbox/lumina"
300
+ assert d2.resolve("grok") == "/outbox/grok"
301
+ lumina = next(p for p in d2.list_peers() if p.name == "lumina")
302
+ assert lumina.fingerprint == "FP99"
303
+
304
+ def test_yaml_file_created(self, tmp_path):
305
+ """add_peer() creates the directory.yaml file."""
306
+ d = make_directory(tmp_path)
307
+ d.add_peer("Opus", "/outbox/opus")
308
+ yaml_path = tmp_path / "peers" / "directory.yaml"
309
+ assert yaml_path.exists()
310
+ data = yaml.safe_load(yaml_path.read_text())
311
+ assert "opus" in data
312
+
313
+ def test_remove_removes_from_yaml(self, tmp_path):
314
+ """remove_peer() removes the entry from YAML on disk."""
315
+ d = make_directory(tmp_path)
316
+ d.add_peer("Jarvis", "/outbox/jarvis")
317
+ d.remove_peer("Jarvis")
318
+
319
+ yaml_path = tmp_path / "peers" / "directory.yaml"
320
+ data = yaml.safe_load(yaml_path.read_text()) or {}
321
+ assert "jarvis" not in data
322
+
323
+
324
+ # ---------------------------------------------------------------------------
325
+ # Test 8: CLI — peers list, add, discover
326
+ # ---------------------------------------------------------------------------
327
+
328
+
329
+ class TestCLI:
330
+ """Test the `skcapstone peers` CLI commands."""
331
+
332
+ def test_peers_help(self):
333
+ """`peers --help` exits cleanly."""
334
+ from skcapstone.cli import main
335
+ runner = CliRunner()
336
+ result = runner.invoke(main, ["peers", "--help"])
337
+ assert result.exit_code == 0
338
+ assert "list" in result.output
339
+ assert "add" in result.output
340
+
341
+ def test_peers_list_empty(self, tmp_path):
342
+ """`peers list` on empty directory shows no-peers message."""
343
+ from skcapstone.cli import main
344
+ runner = CliRunner()
345
+ result = runner.invoke(main, ["peers", "list", "--home", str(tmp_path)])
346
+ assert result.exit_code == 0
347
+ assert "No peers" in result.output
348
+
349
+ def test_peers_add_and_list(self, tmp_path):
350
+ """`peers add` then `peers list` shows the new entry."""
351
+ from skcapstone.cli import main
352
+ runner = CliRunner()
353
+
354
+ add_result = runner.invoke(main, [
355
+ "peers", "add",
356
+ "--name", "Lumina",
357
+ "--address", "/outbox/lumina",
358
+ "--home", str(tmp_path),
359
+ ])
360
+ assert add_result.exit_code == 0, add_result.output
361
+ assert "lumina" in add_result.output.lower()
362
+
363
+ list_result = runner.invoke(main, ["peers", "list", "--home", str(tmp_path)])
364
+ assert list_result.exit_code == 0
365
+ assert "lumina" in list_result.output.lower()
366
+
367
+ def test_peers_list_json(self, tmp_path):
368
+ """`peers list --json-out` produces valid JSON."""
369
+ from skcapstone.cli import main
370
+ runner = CliRunner()
371
+
372
+ runner.invoke(main, [
373
+ "peers", "add",
374
+ "--name", "Grok",
375
+ "--address", "/outbox/grok",
376
+ "--home", str(tmp_path),
377
+ ])
378
+
379
+ result = runner.invoke(main, ["peers", "list", "--json-out", "--home", str(tmp_path)])
380
+ assert result.exit_code == 0
381
+ data = json.loads(result.output)
382
+ assert isinstance(data, list)
383
+ assert any(p["name"] == "grok" for p in data)
384
+
385
+ def test_peers_discover_cli(self, tmp_path):
386
+ """`peers discover` reports newly found peers."""
387
+ from skcapstone.cli import main
388
+
389
+ hb_dir = tmp_path / "heartbeats"
390
+ write_heartbeat(hb_dir, "lumina")
391
+
392
+ runner = CliRunner()
393
+ result = runner.invoke(main, ["peers", "discover", "--home", str(tmp_path)])
394
+ assert result.exit_code == 0
395
+ assert "lumina" in result.output.lower()
@@ -0,0 +1,248 @@
1
+ """Tests for the skcapstone peer management module.
2
+
3
+ Covers:
4
+ - add_peer_from_card (success, missing file, invalid JSON, missing name)
5
+ - add_peer_manual (with/without key)
6
+ - list_peers (empty, with peers)
7
+ - get_peer / remove_peer
8
+ - PeerRecord model
9
+ - SKComm peer file creation
10
+ - CLI commands (add, list, remove, show)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from pathlib import Path
17
+
18
+ import pytest
19
+ from click.testing import CliRunner
20
+
21
+ from skcapstone.peers import (
22
+ PeerRecord,
23
+ add_peer_from_card,
24
+ add_peer_manual,
25
+ get_peer,
26
+ list_peers,
27
+ remove_peer,
28
+ )
29
+
30
+
31
+ @pytest.fixture
32
+ def homes(tmp_path):
33
+ """Create skcapstone and skcomm home directories."""
34
+ sk = tmp_path / ".skcapstone"
35
+ sc = tmp_path / ".skcomm"
36
+ sk.mkdir()
37
+ sc.mkdir()
38
+ return sk, sc
39
+
40
+
41
+ @pytest.fixture
42
+ def card_file(tmp_path):
43
+ """Create a sample identity card file."""
44
+ card = {
45
+ "skcapstone_card": "1.0.0",
46
+ "name": "Lumina",
47
+ "fingerprint": "AABB1122CCDD3344EEFF5566",
48
+ "public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nfakekey\n-----END PGP PUBLIC KEY BLOCK-----",
49
+ "entity_type": "ai",
50
+ "handle": "lumina@skworld.io",
51
+ "email": "lumina@skworld.io",
52
+ "capabilities": ["capauth:identity", "skchat:p2p-chat"],
53
+ "contact_uris": ["capauth:AABB1122CCDD3344"],
54
+ }
55
+ path = tmp_path / "lumina-card.json"
56
+ path.write_text(json.dumps(card, indent=2))
57
+ return path
58
+
59
+
60
+ class TestPeerRecord:
61
+ """Test PeerRecord model."""
62
+
63
+ def test_defaults(self):
64
+ """Record has sensible defaults."""
65
+ p = PeerRecord(name="Test")
66
+ assert p.name == "Test"
67
+ assert p.trust_level == "unknown"
68
+ assert p.source == "manual"
69
+ assert p.added_at != ""
70
+
71
+ def test_serialization(self):
72
+ """Record round-trips through JSON."""
73
+ p = PeerRecord(
74
+ name="Opus",
75
+ fingerprint="FP123",
76
+ capabilities=["capauth:identity"],
77
+ )
78
+ data = json.loads(p.model_dump_json())
79
+ restored = PeerRecord.model_validate(data)
80
+ assert restored.name == "Opus"
81
+ assert restored.fingerprint == "FP123"
82
+
83
+
84
+ class TestAddPeerFromCard:
85
+ """Test importing peers from identity cards."""
86
+
87
+ def test_add_from_card(self, card_file, homes):
88
+ """Card import creates peer records in both registries."""
89
+ sk, sc = homes
90
+ peer = add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
91
+
92
+ assert peer.name == "Lumina"
93
+ assert peer.fingerprint == "AABB1122CCDD3344EEFF5566"
94
+ assert peer.entity_type == "ai"
95
+ assert peer.trust_level == "verified"
96
+ assert peer.source == "card"
97
+ assert "capauth:identity" in peer.capabilities
98
+
99
+ assert (sk / "peers" / "lumina.json").exists()
100
+ assert (sc / "peers" / "lumina.yml").exists()
101
+ assert (sc / "peers" / "lumina.pub.asc").exists()
102
+
103
+ def test_missing_card_raises(self, homes):
104
+ """Nonexistent card raises FileNotFoundError."""
105
+ sk, sc = homes
106
+ with pytest.raises(FileNotFoundError):
107
+ add_peer_from_card(Path("/nope.json"), skcapstone_home=sk, skcomm_home=sc)
108
+
109
+ def test_invalid_json_raises(self, tmp_path, homes):
110
+ """Invalid JSON raises ValueError."""
111
+ sk, sc = homes
112
+ bad = tmp_path / "bad.json"
113
+ bad.write_text("{{{not json")
114
+ with pytest.raises(ValueError):
115
+ add_peer_from_card(bad, skcapstone_home=sk, skcomm_home=sc)
116
+
117
+ def test_missing_name_raises(self, tmp_path, homes):
118
+ """Card without name raises ValueError."""
119
+ sk, sc = homes
120
+ no_name = tmp_path / "noname.json"
121
+ no_name.write_text(json.dumps({"fingerprint": "123"}))
122
+ with pytest.raises(ValueError, match="name"):
123
+ add_peer_from_card(no_name, skcapstone_home=sk, skcomm_home=sc)
124
+
125
+
126
+ class TestAddPeerManual:
127
+ """Test manual peer creation."""
128
+
129
+ def test_add_manual_basic(self, homes):
130
+ """Manual add creates a peer record."""
131
+ sk, sc = homes
132
+ peer = add_peer_manual(
133
+ name="Opus", email="opus@smilintux.org",
134
+ skcapstone_home=sk, skcomm_home=sc,
135
+ )
136
+ assert peer.name == "Opus"
137
+ assert peer.email == "opus@smilintux.org"
138
+ assert (sk / "peers" / "opus.json").exists()
139
+
140
+ def test_add_manual_with_key(self, tmp_path, homes):
141
+ """Manual add with public key file imports the key."""
142
+ sk, sc = homes
143
+ key_file = tmp_path / "opus.pub.asc"
144
+ key_file.write_text("-----BEGIN PGP PUBLIC KEY BLOCK-----\nfake\n-----END PGP PUBLIC KEY BLOCK-----")
145
+
146
+ peer = add_peer_manual(
147
+ name="Opus", public_key_path=key_file,
148
+ skcapstone_home=sk, skcomm_home=sc,
149
+ )
150
+ assert peer.public_key != ""
151
+ assert peer.trust_level == "verified"
152
+ assert (sc / "peers" / "opus.pub.asc").exists()
153
+
154
+
155
+ class TestListPeers:
156
+ """Test peer listing."""
157
+
158
+ def test_empty_list(self, homes):
159
+ """No peers returns empty list."""
160
+ sk, _ = homes
161
+ assert list_peers(skcapstone_home=sk) == []
162
+
163
+ def test_list_with_peers(self, card_file, homes):
164
+ """Added peers appear in listing."""
165
+ sk, sc = homes
166
+ add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
167
+
168
+ peers = list_peers(skcapstone_home=sk)
169
+ assert len(peers) == 1
170
+ assert peers[0].name == "Lumina"
171
+
172
+
173
+ class TestGetPeer:
174
+ """Test single peer lookup."""
175
+
176
+ def test_get_existing(self, card_file, homes):
177
+ """Known peer is returned."""
178
+ sk, sc = homes
179
+ add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
180
+
181
+ peer = get_peer("Lumina", skcapstone_home=sk)
182
+ assert peer is not None
183
+ assert peer.name == "Lumina"
184
+
185
+ def test_get_unknown(self, homes):
186
+ """Unknown peer returns None."""
187
+ sk, _ = homes
188
+ assert get_peer("Nobody", skcapstone_home=sk) is None
189
+
190
+
191
+ class TestRemovePeer:
192
+ """Test peer removal."""
193
+
194
+ def test_remove_existing(self, card_file, homes):
195
+ """Removing an existing peer cleans up all files."""
196
+ sk, sc = homes
197
+ add_peer_from_card(card_file, skcapstone_home=sk, skcomm_home=sc)
198
+
199
+ assert remove_peer("Lumina", skcapstone_home=sk, skcomm_home=sc)
200
+ assert not (sk / "peers" / "lumina.json").exists()
201
+ assert not (sc / "peers" / "lumina.yml").exists()
202
+
203
+ def test_remove_unknown(self, homes):
204
+ """Removing unknown peer returns False."""
205
+ sk, sc = homes
206
+ assert not remove_peer("Nobody", skcapstone_home=sk, skcomm_home=sc)
207
+
208
+
209
+ class TestCLI:
210
+ """Test peer CLI commands."""
211
+
212
+ def test_peer_help(self):
213
+ """peer --help works."""
214
+ from skcapstone.cli import main
215
+ runner = CliRunner()
216
+ result = runner.invoke(main, ["peer", "--help"])
217
+ assert result.exit_code == 0
218
+ assert "add" in result.output
219
+ assert "list" in result.output
220
+ assert "remove" in result.output
221
+ assert "show" in result.output
222
+
223
+ def test_peer_list_empty(self, homes):
224
+ """peer list on empty registry shows message."""
225
+ from skcapstone.cli import main
226
+ sk, _ = homes
227
+ runner = CliRunner()
228
+ result = runner.invoke(main, ["peer", "list", "--home", str(sk)])
229
+ assert result.exit_code == 0
230
+ assert "No peers" in result.output
231
+
232
+ def test_peer_add_from_card_cli(self, card_file, homes):
233
+ """peer add --card via CLI."""
234
+ from skcapstone.cli import main
235
+ sk, _ = homes
236
+ runner = CliRunner()
237
+ result = runner.invoke(main, [
238
+ "peer", "add", "--card", str(card_file), "--home", str(sk),
239
+ ])
240
+ assert result.exit_code == 0
241
+ assert "Lumina" in result.output
242
+
243
+ def test_peer_add_no_args(self):
244
+ """peer add without args shows usage hint."""
245
+ from skcapstone.cli import main
246
+ runner = CliRunner()
247
+ result = runner.invoke(main, ["peer", "add"])
248
+ assert result.exit_code == 1