@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,1909 @@
1
+ """Tests for the SKCapstone MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ import pytest
10
+
11
+ from skcapstone.mcp_server import (
12
+ _error_response,
13
+ _home,
14
+ _json_response,
15
+ call_tool,
16
+ list_tools,
17
+ server,
18
+ )
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Helpers
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ def _extract_json(result: list) -> dict | list:
27
+ """Parse the JSON from a TextContent response list.
28
+
29
+ Args:
30
+ result: List of TextContent objects.
31
+
32
+ Returns:
33
+ Parsed object from the JSON text.
34
+ """
35
+ assert len(result) == 1
36
+ return json.loads(result[0].text)
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Unit tests: helper functions
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ class TestHelpers:
45
+ """Tests for internal helper functions."""
46
+
47
+ def test_home_resolves(self):
48
+ """Default home resolves to ~/.skcapstone."""
49
+ result = _home()
50
+ assert result == Path("~/.skcapstone").expanduser()
51
+
52
+ def test_json_response_structure(self):
53
+ """_json_response wraps data as TextContent."""
54
+ result = _json_response({"key": "value"})
55
+ assert len(result) == 1
56
+ assert result[0].type == "text"
57
+ parsed = json.loads(result[0].text)
58
+ assert parsed == {"key": "value"}
59
+
60
+ def test_error_response_structure(self):
61
+ """_error_response produces error JSON."""
62
+ result = _error_response("something broke")
63
+ parsed = _extract_json(result)
64
+ assert "something broke" in parsed["error"]
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Unit tests: tool listing
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ class TestToolListing:
73
+ """Tests for MCP tool definitions."""
74
+
75
+ @pytest.mark.asyncio
76
+ async def test_list_tools_returns_all(self):
77
+ """list_tools returns all registered tools."""
78
+ tools = await list_tools()
79
+ assert len(tools) == 68
80
+
81
+ @pytest.mark.asyncio
82
+ async def test_tool_names(self):
83
+ """All required tool names are registered."""
84
+ tools = await list_tools()
85
+ names = {t.name for t in tools}
86
+ expected = {
87
+ "agent_status",
88
+ "memory_store",
89
+ "memory_search",
90
+ "memory_recall",
91
+ "send_message",
92
+ "check_inbox",
93
+ "sync_push",
94
+ "sync_pull",
95
+ "coord_status",
96
+ "coord_claim",
97
+ "coord_complete",
98
+ "coord_create",
99
+ "ritual",
100
+ "soul_show",
101
+ "journal_write",
102
+ "journal_read",
103
+ "anchor_show",
104
+ "germination",
105
+ "agent_context",
106
+ "session_capture",
107
+ "trust_graph",
108
+ "memory_curate",
109
+ "trust_calibrate",
110
+ "anchor_update",
111
+ "state_diff",
112
+ "skskills_list_tools",
113
+ "skskills_run_tool",
114
+ "trustee_health",
115
+ "trustee_restart",
116
+ "trustee_scale",
117
+ "trustee_rotate",
118
+ "trustee_monitor",
119
+ "trustee_logs",
120
+ "trustee_deployments",
121
+ "skchat_send",
122
+ "skchat_inbox",
123
+ "skchat_group_create",
124
+ "skchat_group_send",
125
+ # Heartbeat
126
+ "heartbeat_pulse",
127
+ "heartbeat_peers",
128
+ "heartbeat_health",
129
+ "heartbeat_find_capable",
130
+ # File transfer
131
+ "file_send",
132
+ "file_receive",
133
+ "file_list",
134
+ "file_status",
135
+ # Pub/sub
136
+ "pubsub_publish",
137
+ "pubsub_subscribe",
138
+ "pubsub_poll",
139
+ "pubsub_topics",
140
+ # Memory fortress
141
+ "fortress_verify",
142
+ "fortress_seal_existing",
143
+ "fortress_status",
144
+ # Memory promoter
145
+ "promoter_sweep",
146
+ "promoter_history",
147
+ # KMS
148
+ "kms_status",
149
+ "kms_list_keys",
150
+ "kms_rotate",
151
+ # SKSeed (Logic Kernel)
152
+ "skseed_collide",
153
+ "skseed_audit",
154
+ "skseed_philosopher",
155
+ "skseed_truth_check",
156
+ "skseed_alignment",
157
+ # Model Router
158
+ "model_route",
159
+ # Consciousness
160
+ "consciousness_status",
161
+ "consciousness_test",
162
+ # Notifications & pub/sub stats
163
+ "send_notification",
164
+ "pubsub_stats",
165
+ }
166
+ assert names == expected
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_tool_schemas_valid(self):
170
+ """Each tool has a valid inputSchema with 'type' and 'properties'."""
171
+ tools = await list_tools()
172
+ for tool in tools:
173
+ schema = tool.inputSchema
174
+ assert schema["type"] == "object"
175
+ assert "properties" in schema
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Unit tests: tool dispatch (call_tool)
180
+ # ---------------------------------------------------------------------------
181
+
182
+
183
+ class TestCallToolDispatch:
184
+ """Tests for call_tool routing and error handling."""
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_unknown_tool(self):
188
+ """Unknown tool name returns an error response."""
189
+ result = await call_tool("nonexistent_tool", {})
190
+ parsed = _extract_json(result)
191
+ assert "Unknown tool" in parsed["error"]
192
+
193
+ @pytest.mark.asyncio
194
+ async def test_agent_status_no_agent(self, tmp_path: Path):
195
+ """agent_status with no initialized agent returns error."""
196
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path / "no-agent")):
197
+ result = await call_tool("agent_status", {})
198
+ parsed = _extract_json(result)
199
+ assert "error" in parsed
200
+
201
+ @pytest.mark.asyncio
202
+ async def test_agent_status_with_agent(self, initialized_agent_home: Path):
203
+ """agent_status returns pillar states for a valid agent."""
204
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
205
+ result = await call_tool("agent_status", {})
206
+ parsed = _extract_json(result)
207
+ assert "pillars" in parsed
208
+ assert "identity" in parsed["pillars"]
209
+ assert "memory" in parsed["pillars"]
210
+ assert "trust" in parsed["pillars"]
211
+ assert "security" in parsed["pillars"]
212
+ assert "sync" in parsed["pillars"]
213
+ assert "is_conscious" in parsed
214
+ assert parsed["name"] == "test-agent"
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Memory tool tests
219
+ # ---------------------------------------------------------------------------
220
+
221
+
222
+ class TestMemoryTools:
223
+ """Tests for memory_store, memory_search, and memory_recall."""
224
+
225
+ @pytest.mark.asyncio
226
+ async def test_memory_store_requires_content(self, initialized_agent_home: Path):
227
+ """memory_store without content returns error."""
228
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
229
+ result = await call_tool("memory_store", {})
230
+ parsed = _extract_json(result)
231
+ assert "error" in parsed
232
+
233
+ @pytest.mark.asyncio
234
+ async def test_memory_search_requires_query(self, initialized_agent_home: Path):
235
+ """memory_search without query returns error."""
236
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
237
+ result = await call_tool("memory_search", {})
238
+ parsed = _extract_json(result)
239
+ assert "error" in parsed
240
+
241
+ @pytest.mark.asyncio
242
+ async def test_memory_recall_requires_id(self, initialized_agent_home: Path):
243
+ """memory_recall without memory_id returns error."""
244
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
245
+ result = await call_tool("memory_recall", {})
246
+ parsed = _extract_json(result)
247
+ assert "error" in parsed
248
+
249
+ @pytest.mark.asyncio
250
+ async def test_memory_store_and_search(self, initialized_agent_home: Path):
251
+ """Store a memory then find it via search."""
252
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
253
+ store_result = await call_tool(
254
+ "memory_store",
255
+ {
256
+ "content": "The sovereign penguin remembers everything",
257
+ "tags": ["pengu", "test"],
258
+ "importance": 0.5,
259
+ },
260
+ )
261
+ store_parsed = _extract_json(store_result)
262
+ assert store_parsed["stored"] is True
263
+ assert store_parsed["memory_id"]
264
+ assert store_parsed["layer"] == "short-term"
265
+
266
+ search_result = await call_tool(
267
+ "memory_search", {"query": "sovereign penguin"}
268
+ )
269
+ search_parsed = _extract_json(search_result)
270
+ assert isinstance(search_parsed, list)
271
+ assert len(search_parsed) >= 1
272
+ assert any("sovereign penguin" in r["content"] for r in search_parsed)
273
+
274
+ @pytest.mark.asyncio
275
+ async def test_memory_store_and_recall(self, initialized_agent_home: Path):
276
+ """Store a memory then recall it by ID."""
277
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
278
+ store_result = await call_tool(
279
+ "memory_store",
280
+ {"content": "Recall me later", "importance": 0.3},
281
+ )
282
+ store_parsed = _extract_json(store_result)
283
+ mid = store_parsed["memory_id"]
284
+
285
+ recall_result = await call_tool("memory_recall", {"memory_id": mid})
286
+ recall_parsed = _extract_json(recall_result)
287
+ assert recall_parsed["memory_id"] == mid
288
+ assert "Recall me later" in recall_parsed["content"]
289
+ assert recall_parsed["access_count"] >= 1
290
+
291
+ @pytest.mark.asyncio
292
+ async def test_memory_recall_not_found(self, initialized_agent_home: Path):
293
+ """memory_recall with nonexistent ID returns error."""
294
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
295
+ result = await call_tool("memory_recall", {"memory_id": "nonexistent123"})
296
+ parsed = _extract_json(result)
297
+ assert "error" in parsed
298
+
299
+ @pytest.mark.asyncio
300
+ async def test_memory_store_high_importance_promotes(
301
+ self, initialized_agent_home: Path
302
+ ):
303
+ """High-importance memory gets promoted to mid-term."""
304
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
305
+ result = await call_tool(
306
+ "memory_store",
307
+ {"content": "Critical penguin intel", "importance": 0.8},
308
+ )
309
+ parsed = _extract_json(result)
310
+ assert parsed["stored"] is True
311
+ assert parsed["layer"] == "mid-term"
312
+
313
+
314
+ # ---------------------------------------------------------------------------
315
+ # Coordination tool tests
316
+ # ---------------------------------------------------------------------------
317
+
318
+
319
+ class TestCoordTools:
320
+ """Tests for coordination board MCP tools."""
321
+
322
+ @pytest.mark.asyncio
323
+ async def test_coord_status_empty(self, initialized_agent_home: Path):
324
+ """coord_status on empty board returns zero tasks."""
325
+ from skcapstone.coordination import Board
326
+
327
+ board = Board(initialized_agent_home)
328
+ board.ensure_dirs()
329
+
330
+ with (
331
+ patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)),
332
+ patch("skcapstone.mcp_tools._helpers.SHARED_ROOT", str(initialized_agent_home)),
333
+ ):
334
+ result = await call_tool("coord_status", {})
335
+ parsed = _extract_json(result)
336
+ assert parsed["summary"]["total"] == 0
337
+ assert parsed["tasks"] == []
338
+
339
+ @pytest.mark.asyncio
340
+ async def test_coord_claim_requires_params(self, initialized_agent_home: Path):
341
+ """coord_claim without task_id and agent_name returns error."""
342
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
343
+ result = await call_tool("coord_claim", {})
344
+ parsed = _extract_json(result)
345
+ assert "error" in parsed
346
+
347
+ @pytest.mark.asyncio
348
+ async def test_coord_complete_requires_params(self, initialized_agent_home: Path):
349
+ """coord_complete without task_id and agent_name returns error."""
350
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
351
+ result = await call_tool("coord_complete", {})
352
+ parsed = _extract_json(result)
353
+ assert "error" in parsed
354
+
355
+ @pytest.mark.asyncio
356
+ async def test_coord_create_requires_title(self, initialized_agent_home: Path):
357
+ """coord_create without title returns error."""
358
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
359
+ result = await call_tool("coord_create", {})
360
+ parsed = _extract_json(result)
361
+ assert "error" in parsed
362
+
363
+ @pytest.mark.asyncio
364
+ async def test_coord_claim_nonexistent_task(self, initialized_agent_home: Path):
365
+ """coord_claim for a nonexistent task returns error."""
366
+ from skcapstone.coordination import Board
367
+
368
+ board = Board(initialized_agent_home)
369
+ board.ensure_dirs()
370
+
371
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
372
+ result = await call_tool(
373
+ "coord_claim", {"task_id": "nosuch", "agent_name": "tester"}
374
+ )
375
+ parsed = _extract_json(result)
376
+ assert "error" in parsed
377
+
378
+ @pytest.mark.asyncio
379
+ async def test_coord_full_workflow(self, initialized_agent_home: Path):
380
+ """Create a task via MCP, claim it, then complete it."""
381
+ from skcapstone.coordination import Board
382
+
383
+ board = Board(initialized_agent_home)
384
+ board.ensure_dirs()
385
+
386
+ with (
387
+ patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)),
388
+ patch("skcapstone.mcp_tools._helpers.SHARED_ROOT", str(initialized_agent_home)),
389
+ ):
390
+ create_result = await call_tool(
391
+ "coord_create",
392
+ {
393
+ "title": "Test MCP task",
394
+ "priority": "high",
395
+ "tags": ["mcp", "test"],
396
+ "created_by": "mcp-builder",
397
+ },
398
+ )
399
+ create_parsed = _extract_json(create_result)
400
+ assert create_parsed["created"] is True
401
+ task_id = create_parsed["task_id"]
402
+
403
+ status_result = await call_tool("coord_status", {})
404
+ status_parsed = _extract_json(status_result)
405
+ assert status_parsed["summary"]["total"] == 1
406
+ assert status_parsed["tasks"][0]["status"] == "open"
407
+
408
+ claim_result = await call_tool(
409
+ "coord_claim", {"task_id": task_id, "agent_name": "mcp-builder"}
410
+ )
411
+ claim_parsed = _extract_json(claim_result)
412
+ assert claim_parsed["claimed"] is True
413
+ assert claim_parsed["agent"] == "mcp-builder"
414
+
415
+ complete_result = await call_tool(
416
+ "coord_complete",
417
+ {"task_id": task_id, "agent_name": "mcp-builder"},
418
+ )
419
+ complete_parsed = _extract_json(complete_result)
420
+ assert complete_parsed["completed"] is True
421
+ assert task_id in complete_parsed["completed_tasks"]
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # SKComm tool tests (graceful fallback)
426
+ # ---------------------------------------------------------------------------
427
+
428
+
429
+ class TestCommTools:
430
+ """Tests for send_message and check_inbox (SKComm may not be installed)."""
431
+
432
+ @pytest.mark.asyncio
433
+ async def test_send_message_requires_params(self):
434
+ """send_message without recipient/message returns error."""
435
+ result = await call_tool("send_message", {})
436
+ parsed = _extract_json(result)
437
+ assert "error" in parsed
438
+
439
+ @pytest.mark.asyncio
440
+ async def test_check_inbox_graceful_fallback(self):
441
+ """check_inbox returns graceful error when SKComm is unavailable."""
442
+ result = await call_tool("check_inbox", {})
443
+ parsed = _extract_json(result)
444
+ # Either returns messages list or graceful error about skcomm
445
+ assert isinstance(parsed, list) or "error" in parsed
446
+
447
+
448
+ # ---------------------------------------------------------------------------
449
+ # Sync tool tests
450
+ # ---------------------------------------------------------------------------
451
+
452
+
453
+ class TestSyncTools:
454
+ """Tests for sync_push and sync_pull."""
455
+
456
+ @pytest.mark.asyncio
457
+ async def test_sync_push_no_agent(self, tmp_path: Path):
458
+ """sync_push with no agent home returns error."""
459
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path / "nope")):
460
+ result = await call_tool("sync_push", {})
461
+ parsed = _extract_json(result)
462
+ assert "error" in parsed
463
+
464
+ @pytest.mark.asyncio
465
+ async def test_sync_pull_empty_inbox(self, initialized_agent_home: Path):
466
+ """sync_pull with empty inbox returns zero seeds."""
467
+ sync_dir = initialized_agent_home / "sync"
468
+ sync_dir.mkdir(exist_ok=True)
469
+ (sync_dir / "inbox").mkdir(exist_ok=True)
470
+ (sync_dir / "outbox").mkdir(exist_ok=True)
471
+ (sync_dir / "archive").mkdir(exist_ok=True)
472
+ (sync_dir / "sync-manifest.json").write_text(
473
+ json.dumps({"transport": "syncthing", "gpg_encrypt": False})
474
+ )
475
+
476
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
477
+ result = await call_tool("sync_pull", {})
478
+ parsed = _extract_json(result)
479
+ assert parsed["pulled"] == 0
480
+ assert parsed["seeds"] == []
481
+
482
+
483
+ # ---------------------------------------------------------------------------
484
+ # Trustee Operations MCP tool tests
485
+ # ---------------------------------------------------------------------------
486
+
487
+
488
+ class TestTrusteeTools:
489
+ """Tests for trustee_* MCP tools."""
490
+
491
+ def _setup_deployment(self, home: Path) -> str:
492
+ """Create a test deployment and return its ID."""
493
+ from datetime import datetime, timezone
494
+
495
+ from skcapstone.team_engine import (
496
+ AgentStatus,
497
+ DeployedAgent,
498
+ TeamDeployment,
499
+ TeamEngine,
500
+ )
501
+
502
+ (home / "deployments").mkdir(parents=True, exist_ok=True)
503
+ (home / "coordination").mkdir(parents=True, exist_ok=True)
504
+ engine = TeamEngine(home=home, provider=None, comms_root=None)
505
+ now = datetime.now(timezone.utc).isoformat()
506
+ deployment = TeamDeployment(
507
+ deployment_id="mcp-test-deploy",
508
+ blueprint_slug="test",
509
+ team_name="MCP Test Team",
510
+ provider="local",
511
+ status="running",
512
+ )
513
+ for name in ("worker-1", "worker-2"):
514
+ deployment.agents[name] = DeployedAgent(
515
+ name=name,
516
+ instance_id=f"mcp-test-deploy/{name}",
517
+ blueprint_slug="test",
518
+ agent_spec_key="worker",
519
+ status=AgentStatus.RUNNING,
520
+ host="localhost",
521
+ last_heartbeat=now,
522
+ started_at=now,
523
+ )
524
+ engine._save_deployment(deployment)
525
+ return "mcp-test-deploy"
526
+
527
+ @pytest.mark.asyncio
528
+ async def test_trustee_deployments_empty(self, initialized_agent_home: Path):
529
+ """trustee_deployments returns empty list when no deployments."""
530
+ (initialized_agent_home / "deployments").mkdir(exist_ok=True)
531
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
532
+ result = await call_tool("trustee_deployments", {})
533
+ parsed = _extract_json(result)
534
+ assert parsed["count"] == 0
535
+ assert parsed["deployments"] == []
536
+
537
+ @pytest.mark.asyncio
538
+ async def test_trustee_deployments_lists(self, initialized_agent_home: Path):
539
+ """trustee_deployments lists created deployments."""
540
+ self._setup_deployment(initialized_agent_home)
541
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
542
+ result = await call_tool("trustee_deployments", {})
543
+ parsed = _extract_json(result)
544
+ assert parsed["count"] == 1
545
+ d = parsed["deployments"][0]
546
+ assert d["deployment_id"] == "mcp-test-deploy"
547
+ assert d["agent_count"] == 2
548
+
549
+ @pytest.mark.asyncio
550
+ async def test_trustee_health(self, initialized_agent_home: Path):
551
+ """trustee_health returns per-agent health."""
552
+ deploy_id = self._setup_deployment(initialized_agent_home)
553
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
554
+ result = await call_tool("trustee_health", {"deployment_id": deploy_id})
555
+ parsed = _extract_json(result)
556
+ assert parsed["deployment_id"] == deploy_id
557
+ assert parsed["summary"]["total"] == 2
558
+ assert parsed["summary"]["healthy"] == 2
559
+
560
+ @pytest.mark.asyncio
561
+ async def test_trustee_health_not_found(self, initialized_agent_home: Path):
562
+ """trustee_health with bad ID returns error."""
563
+ (initialized_agent_home / "deployments").mkdir(exist_ok=True)
564
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
565
+ result = await call_tool("trustee_health", {"deployment_id": "nope"})
566
+ parsed = _extract_json(result)
567
+ assert "error" in parsed
568
+
569
+ @pytest.mark.asyncio
570
+ async def test_trustee_health_requires_id(self, initialized_agent_home: Path):
571
+ """trustee_health without deployment_id returns error."""
572
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
573
+ result = await call_tool("trustee_health", {})
574
+ parsed = _extract_json(result)
575
+ assert "error" in parsed
576
+
577
+ @pytest.mark.asyncio
578
+ async def test_trustee_restart(self, initialized_agent_home: Path):
579
+ """trustee_restart restarts an agent."""
580
+ deploy_id = self._setup_deployment(initialized_agent_home)
581
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
582
+ result = await call_tool(
583
+ "trustee_restart",
584
+ {"deployment_id": deploy_id, "agent_name": "worker-1"},
585
+ )
586
+ parsed = _extract_json(result)
587
+ assert parsed["results"]["worker-1"] == "restarted"
588
+ assert parsed["all_restarted"] is True
589
+
590
+ @pytest.mark.asyncio
591
+ async def test_trustee_restart_all(self, initialized_agent_home: Path):
592
+ """trustee_restart without agent_name restarts all."""
593
+ deploy_id = self._setup_deployment(initialized_agent_home)
594
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
595
+ result = await call_tool(
596
+ "trustee_restart", {"deployment_id": deploy_id}
597
+ )
598
+ parsed = _extract_json(result)
599
+ assert len(parsed["results"]) == 2
600
+ assert parsed["all_restarted"] is True
601
+
602
+ @pytest.mark.asyncio
603
+ async def test_trustee_scale_up(self, initialized_agent_home: Path):
604
+ """trustee_scale adds instances."""
605
+ deploy_id = self._setup_deployment(initialized_agent_home)
606
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
607
+ result = await call_tool(
608
+ "trustee_scale",
609
+ {"deployment_id": deploy_id, "agent_spec_key": "worker", "count": 4},
610
+ )
611
+ parsed = _extract_json(result)
612
+ assert parsed["current_count"] == 4
613
+ assert len(parsed["added"]) == 2
614
+
615
+ @pytest.mark.asyncio
616
+ async def test_trustee_scale_requires_all_params(self, initialized_agent_home: Path):
617
+ """trustee_scale without all params returns error."""
618
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
619
+ result = await call_tool("trustee_scale", {"deployment_id": "x"})
620
+ parsed = _extract_json(result)
621
+ assert "error" in parsed
622
+
623
+ @pytest.mark.asyncio
624
+ async def test_trustee_rotate(self, initialized_agent_home: Path):
625
+ """trustee_rotate snapshots and redeploys."""
626
+ deploy_id = self._setup_deployment(initialized_agent_home)
627
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
628
+ result = await call_tool(
629
+ "trustee_rotate",
630
+ {"deployment_id": deploy_id, "agent_name": "worker-1"},
631
+ )
632
+ parsed = _extract_json(result)
633
+ assert parsed["deployment_id"] == deploy_id
634
+ assert parsed["agent_name"] == "worker-1"
635
+ assert "snapshot_path" in parsed
636
+
637
+ @pytest.mark.asyncio
638
+ async def test_trustee_rotate_requires_params(self, initialized_agent_home: Path):
639
+ """trustee_rotate without both params returns error."""
640
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
641
+ result = await call_tool("trustee_rotate", {"deployment_id": "x"})
642
+ parsed = _extract_json(result)
643
+ assert "error" in parsed
644
+
645
+ @pytest.mark.asyncio
646
+ async def test_trustee_monitor_all(self, initialized_agent_home: Path):
647
+ """trustee_monitor runs a monitoring pass over all deployments."""
648
+ self._setup_deployment(initialized_agent_home)
649
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
650
+ result = await call_tool("trustee_monitor", {})
651
+ parsed = _extract_json(result)
652
+ assert parsed["deployments_checked"] == 1
653
+ assert parsed["agents_healthy"] == 2
654
+ assert parsed["agents_degraded"] == 0
655
+
656
+ @pytest.mark.asyncio
657
+ async def test_trustee_monitor_single(self, initialized_agent_home: Path):
658
+ """trustee_monitor checks a specific deployment."""
659
+ deploy_id = self._setup_deployment(initialized_agent_home)
660
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
661
+ result = await call_tool(
662
+ "trustee_monitor", {"deployment_id": deploy_id}
663
+ )
664
+ parsed = _extract_json(result)
665
+ assert parsed["deployments_checked"] == 1
666
+ assert parsed["agents_healthy"] == 2
667
+
668
+ @pytest.mark.asyncio
669
+ async def test_trustee_monitor_not_found(self, initialized_agent_home: Path):
670
+ """trustee_monitor with bad deployment_id returns error."""
671
+ (initialized_agent_home / "deployments").mkdir(exist_ok=True)
672
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
673
+ result = await call_tool(
674
+ "trustee_monitor", {"deployment_id": "nope"}
675
+ )
676
+ parsed = _extract_json(result)
677
+ assert "error" in parsed
678
+
679
+ @pytest.mark.asyncio
680
+ async def test_trustee_logs(self, initialized_agent_home: Path):
681
+ """trustee_logs returns log lines."""
682
+ deploy_id = self._setup_deployment(initialized_agent_home)
683
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
684
+ result = await call_tool(
685
+ "trustee_logs", {"deployment_id": deploy_id}
686
+ )
687
+ parsed = _extract_json(result)
688
+ assert parsed["deployment_id"] == deploy_id
689
+ assert "worker-1" in parsed["agents"]
690
+ assert "worker-2" in parsed["agents"]
691
+
692
+ @pytest.mark.asyncio
693
+ async def test_trustee_logs_requires_id(self, initialized_agent_home: Path):
694
+ """trustee_logs without deployment_id returns error."""
695
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
696
+ result = await call_tool("trustee_logs", {})
697
+ parsed = _extract_json(result)
698
+ assert "error" in parsed
699
+
700
+
701
+ # ---------------------------------------------------------------------------
702
+ # SKChat MCP tool tests
703
+ # ---------------------------------------------------------------------------
704
+
705
+
706
+ class TestSKChatTools:
707
+ """Tests for skchat_send, skchat_inbox, skchat_group_create, skchat_group_send."""
708
+
709
+ @pytest.mark.asyncio
710
+ async def test_skchat_send_requires_params(self):
711
+ """skchat_send without recipient/message returns error."""
712
+ with patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:test@local"):
713
+ result = await call_tool("skchat_send", {})
714
+ parsed = _extract_json(result)
715
+ assert "error" in parsed
716
+
717
+ @pytest.mark.asyncio
718
+ async def test_skchat_send_requires_message(self):
719
+ """skchat_send with only recipient returns error."""
720
+ with patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:test@local"):
721
+ result = await call_tool("skchat_send", {"recipient": "lumina"})
722
+ parsed = _extract_json(result)
723
+ assert "error" in parsed
724
+
725
+ @pytest.mark.asyncio
726
+ async def test_skchat_send_success(self):
727
+ """skchat_send calls AgentMessenger.send and returns result."""
728
+ mock_messenger = type("M", (), {
729
+ "send": lambda self, **kw: {
730
+ "message_id": "msg-123",
731
+ "delivered": True,
732
+ "transport": "syncthing",
733
+ },
734
+ })()
735
+
736
+ with (
737
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
738
+ patch("skcapstone.mcp_tools.chat_tools._resolve_recipient", return_value="capauth:lumina@local"),
739
+ patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
740
+ ):
741
+ result = await call_tool(
742
+ "skchat_send",
743
+ {"recipient": "lumina", "message": "Hello!"},
744
+ )
745
+ parsed = _extract_json(result)
746
+ assert parsed["sent"] is True
747
+ assert parsed["message_id"] == "msg-123"
748
+ assert parsed["delivered"] is True
749
+ assert parsed["recipient"] == "capauth:lumina@local"
750
+
751
+ @pytest.mark.asyncio
752
+ async def test_skchat_send_with_thread(self):
753
+ """skchat_send passes thread_id and message_type to messenger."""
754
+ received_kwargs = {}
755
+
756
+ def capture_send(**kw):
757
+ received_kwargs.update(kw)
758
+ return {"message_id": "msg-456", "delivered": False}
759
+
760
+ mock_messenger = type("M", (), {"send": lambda self, **kw: capture_send(**kw)})()
761
+
762
+ with (
763
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
764
+ patch("skcapstone.mcp_tools.chat_tools._resolve_recipient", return_value="capauth:jarvis@local"),
765
+ patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
766
+ ):
767
+ result = await call_tool(
768
+ "skchat_send",
769
+ {
770
+ "recipient": "jarvis",
771
+ "message": "Bug report",
772
+ "message_type": "finding",
773
+ "thread_id": "thread-abc",
774
+ },
775
+ )
776
+ parsed = _extract_json(result)
777
+ assert parsed["sent"] is True
778
+ assert received_kwargs["message_type"] == "finding"
779
+ assert received_kwargs["thread_id"] == "thread-abc"
780
+
781
+ @pytest.mark.asyncio
782
+ async def test_skchat_send_no_skchat(self):
783
+ """skchat_send returns error when skchat is not installed."""
784
+ with patch.dict("sys.modules", {"skchat": None, "skchat.agent_comm": None}):
785
+ result = await call_tool(
786
+ "skchat_send",
787
+ {"recipient": "lumina", "message": "Hello"},
788
+ )
789
+ parsed = _extract_json(result)
790
+ assert "error" in parsed
791
+
792
+ @pytest.mark.asyncio
793
+ async def test_skchat_inbox_empty(self):
794
+ """skchat_inbox returns empty list when no messages."""
795
+ mock_messenger = type("M", (), {
796
+ "receive": lambda self, limit=50: [],
797
+ })()
798
+
799
+ with (
800
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
801
+ patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
802
+ ):
803
+ result = await call_tool("skchat_inbox", {})
804
+ parsed = _extract_json(result)
805
+ assert parsed["count"] == 0
806
+ assert parsed["messages"] == []
807
+
808
+ @pytest.mark.asyncio
809
+ async def test_skchat_inbox_with_messages(self):
810
+ """skchat_inbox returns messages from AgentMessenger."""
811
+ mock_messenger = type("M", (), {
812
+ "receive": lambda self, limit=50: [
813
+ {
814
+ "message_id": "m1",
815
+ "sender": "capauth:lumina@local",
816
+ "content": "Hello from Lumina",
817
+ "message_type": "text",
818
+ "thread_id": None,
819
+ "timestamp": "2026-02-27T10:00:00",
820
+ },
821
+ {
822
+ "message_id": "m2",
823
+ "sender": "capauth:jarvis@local",
824
+ "content": "Bug found in transport.py",
825
+ "message_type": "finding",
826
+ "thread_id": "thread-x",
827
+ "timestamp": "2026-02-27T10:01:00",
828
+ },
829
+ ],
830
+ })()
831
+
832
+ with (
833
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
834
+ patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
835
+ ):
836
+ result = await call_tool("skchat_inbox", {"limit": 10})
837
+ parsed = _extract_json(result)
838
+ assert parsed["count"] == 2
839
+ assert parsed["messages"][0]["sender"] == "capauth:lumina@local"
840
+ assert parsed["messages"][1]["message_type"] == "finding"
841
+
842
+ @pytest.mark.asyncio
843
+ async def test_skchat_inbox_filter_by_type(self):
844
+ """skchat_inbox filters messages by message_type."""
845
+ mock_messenger = type("M", (), {
846
+ "receive": lambda self, limit=50: [
847
+ {"message_id": "m1", "sender": "a", "content": "hi", "message_type": "text", "thread_id": None, "timestamp": ""},
848
+ {"message_id": "m2", "sender": "b", "content": "bug", "message_type": "finding", "thread_id": None, "timestamp": ""},
849
+ ],
850
+ })()
851
+
852
+ with (
853
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
854
+ patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
855
+ ):
856
+ result = await call_tool("skchat_inbox", {"message_type": "finding"})
857
+ parsed = _extract_json(result)
858
+ assert parsed["count"] == 1
859
+ assert parsed["messages"][0]["message_type"] == "finding"
860
+
861
+ @pytest.mark.asyncio
862
+ async def test_skchat_group_create_requires_name(self):
863
+ """skchat_group_create without name returns error."""
864
+ with patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"):
865
+ result = await call_tool("skchat_group_create", {})
866
+ parsed = _extract_json(result)
867
+ assert "error" in parsed
868
+
869
+ @pytest.mark.asyncio
870
+ async def test_skchat_group_create_success(self):
871
+ """skchat_group_create creates a group and stores it."""
872
+ mock_history = type("H", (), {
873
+ "store_thread": lambda self, t: "mem-abc",
874
+ })()
875
+
876
+ with (
877
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
878
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history),
879
+ ):
880
+ result = await call_tool(
881
+ "skchat_group_create",
882
+ {"name": "Test Squad", "description": "For testing"},
883
+ )
884
+ parsed = _extract_json(result)
885
+ assert parsed["created"] is True
886
+ assert parsed["name"] == "Test Squad"
887
+ assert parsed["admin"] == "capauth:opus@local"
888
+ assert "capauth:opus@local" in parsed["members"]
889
+
890
+ @pytest.mark.asyncio
891
+ async def test_skchat_group_create_with_members(self):
892
+ """skchat_group_create adds initial members."""
893
+ mock_history = type("H", (), {
894
+ "store_thread": lambda self, t: "mem-xyz",
895
+ })()
896
+
897
+ with (
898
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
899
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history),
900
+ patch("skcapstone.mcp_tools.chat_tools._resolve_recipient", side_effect=lambda n: f"capauth:{n}@local"),
901
+ ):
902
+ result = await call_tool(
903
+ "skchat_group_create",
904
+ {"name": "Alpha Team", "members": ["lumina", "jarvis"]},
905
+ )
906
+ parsed = _extract_json(result)
907
+ assert parsed["created"] is True
908
+ assert len(parsed["members"]) == 3 # opus + lumina + jarvis
909
+ assert len(parsed["members_added"]) == 2
910
+
911
+ @pytest.mark.asyncio
912
+ async def test_skchat_group_send_requires_params(self):
913
+ """skchat_group_send without group_id/message returns error."""
914
+ result = await call_tool("skchat_group_send", {})
915
+ parsed = _extract_json(result)
916
+ assert "error" in parsed
917
+
918
+ @pytest.mark.asyncio
919
+ async def test_skchat_group_send_not_found(self):
920
+ """skchat_group_send with unknown group returns error."""
921
+ mock_history = type("H", (), {
922
+ "get_thread": lambda self, gid: None,
923
+ })()
924
+
925
+ with patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history):
926
+ result = await call_tool(
927
+ "skchat_group_send",
928
+ {"group_id": "nonexistent", "message": "Hello"},
929
+ )
930
+ parsed = _extract_json(result)
931
+ assert "error" in parsed
932
+ assert "not found" in parsed["error"].lower()
933
+
934
+ @pytest.mark.asyncio
935
+ async def test_skchat_group_send_not_a_group(self):
936
+ """skchat_group_send on a plain thread (no group_data) returns error."""
937
+ mock_history = type("H", (), {
938
+ "get_thread": lambda self, gid: {"title": "Just a thread"},
939
+ })()
940
+
941
+ with patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history):
942
+ result = await call_tool(
943
+ "skchat_group_send",
944
+ {"group_id": "thread-123", "message": "Hello"},
945
+ )
946
+ parsed = _extract_json(result)
947
+ assert "error" in parsed
948
+ assert "not a group" in parsed["error"].lower()
949
+
950
+ @pytest.mark.asyncio
951
+ async def test_skchat_group_send_success(self):
952
+ """skchat_group_send stores message and returns confirmation."""
953
+ from datetime import datetime, timezone
954
+
955
+ group_data = {
956
+ "id": "grp-abc",
957
+ "name": "Test Group",
958
+ "description": "",
959
+ "members": [
960
+ {
961
+ "identity_uri": "capauth:opus@local",
962
+ "role": "admin",
963
+ "participant_type": "agent",
964
+ "display_name": "opus",
965
+ "public_key_armor": "",
966
+ "joined_at": datetime.now(timezone.utc).isoformat(),
967
+ "tool_scope": [],
968
+ },
969
+ ],
970
+ "created_by": "capauth:opus@local",
971
+ "created_at": datetime.now(timezone.utc).isoformat(),
972
+ "updated_at": datetime.now(timezone.utc).isoformat(),
973
+ "message_count": 0,
974
+ "group_key": "a" * 64,
975
+ "key_version": 1,
976
+ "metadata": {},
977
+ }
978
+
979
+ mock_history = type("H", (), {
980
+ "get_thread": lambda self, gid: {"group_data": group_data},
981
+ "store_message": lambda self, msg: "mem-stored",
982
+ })()
983
+
984
+ with (
985
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_identity", return_value="capauth:opus@local"),
986
+ patch("skcapstone.mcp_tools.chat_tools._get_skchat_history", return_value=mock_history),
987
+ ):
988
+ result = await call_tool(
989
+ "skchat_group_send",
990
+ {"group_id": "grp-abc", "message": "Hello team!"},
991
+ )
992
+ parsed = _extract_json(result)
993
+ assert parsed["sent"] is True
994
+ assert parsed["group_id"] == "grp-abc"
995
+ assert parsed["group_name"] == "Test Group"
996
+ assert parsed["stored"] is True
997
+
998
+
999
+ # ---------------------------------------------------------------------------
1000
+ # Unit tests: heartbeat tools
1001
+ # ---------------------------------------------------------------------------
1002
+
1003
+
1004
+ class TestHeartbeatTools:
1005
+ """Tests for heartbeat MCP tools."""
1006
+
1007
+ @pytest.mark.asyncio
1008
+ async def test_heartbeat_pulse(self, tmp_path):
1009
+ """heartbeat_pulse publishes a heartbeat."""
1010
+ identity_dir = tmp_path / "identity"
1011
+ identity_dir.mkdir()
1012
+ (identity_dir / "identity.json").write_text(
1013
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1014
+ encoding="utf-8",
1015
+ )
1016
+
1017
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1018
+ result = await call_tool("heartbeat_pulse", {"status": "alive"})
1019
+ parsed = _extract_json(result)
1020
+ assert parsed["agent_name"] == "opus"
1021
+ assert parsed["status"] == "alive"
1022
+ assert parsed["capacity"]["cpu_count"] > 0
1023
+
1024
+ @pytest.mark.asyncio
1025
+ async def test_heartbeat_peers(self, tmp_path):
1026
+ """heartbeat_peers discovers mesh peers."""
1027
+ identity_dir = tmp_path / "identity"
1028
+ identity_dir.mkdir()
1029
+ (identity_dir / "identity.json").write_text(
1030
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1031
+ encoding="utf-8",
1032
+ )
1033
+
1034
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1035
+ # Pulse first to create own heartbeat
1036
+ await call_tool("heartbeat_pulse", {})
1037
+ result = await call_tool("heartbeat_peers", {"include_self": True})
1038
+ parsed = _extract_json(result)
1039
+ assert len(parsed) >= 1
1040
+ assert parsed[0]["agent_name"] == "opus"
1041
+
1042
+ @pytest.mark.asyncio
1043
+ async def test_heartbeat_health(self, tmp_path):
1044
+ """heartbeat_health returns mesh summary."""
1045
+ identity_dir = tmp_path / "identity"
1046
+ identity_dir.mkdir()
1047
+ (identity_dir / "identity.json").write_text(
1048
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1049
+ encoding="utf-8",
1050
+ )
1051
+
1052
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1053
+ await call_tool("heartbeat_pulse", {})
1054
+ result = await call_tool("heartbeat_health", {})
1055
+ parsed = _extract_json(result)
1056
+ assert parsed["total_peers"] >= 1
1057
+ assert parsed["alive_peers"] >= 1
1058
+
1059
+ @pytest.mark.asyncio
1060
+ async def test_heartbeat_find_capable(self, tmp_path):
1061
+ """heartbeat_find_capable searches by capability."""
1062
+ identity_dir = tmp_path / "identity"
1063
+ identity_dir.mkdir()
1064
+ (identity_dir / "identity.json").write_text(
1065
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1066
+ encoding="utf-8",
1067
+ )
1068
+
1069
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1070
+ result = await call_tool(
1071
+ "heartbeat_find_capable", {"capability": "nonexistent"},
1072
+ )
1073
+ parsed = _extract_json(result)
1074
+ assert parsed["capability"] == "nonexistent"
1075
+ assert parsed["peers"] == []
1076
+
1077
+
1078
+ # ---------------------------------------------------------------------------
1079
+ # Unit tests: file transfer tools
1080
+ # ---------------------------------------------------------------------------
1081
+
1082
+
1083
+ class TestFileTransferTools:
1084
+ """Tests for file transfer MCP tools."""
1085
+
1086
+ @pytest.mark.asyncio
1087
+ async def test_file_send(self, tmp_path):
1088
+ """file_send creates a transfer."""
1089
+ identity_dir = tmp_path / "identity"
1090
+ identity_dir.mkdir()
1091
+ (identity_dir / "identity.json").write_text(
1092
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1093
+ encoding="utf-8",
1094
+ )
1095
+ (tmp_path / "security").mkdir()
1096
+
1097
+ test_file = tmp_path / "test.txt"
1098
+ test_file.write_text("Hello world!", encoding="utf-8")
1099
+
1100
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1101
+ result = await call_tool("file_send", {
1102
+ "file_path": str(test_file),
1103
+ "recipient": "lumina",
1104
+ "encrypt": False,
1105
+ })
1106
+ parsed = _extract_json(result)
1107
+ assert parsed["filename"] == "test.txt"
1108
+ assert parsed["sender"] == "opus"
1109
+ assert parsed["recipient"] == "lumina"
1110
+ assert parsed["total_chunks"] >= 1
1111
+
1112
+ @pytest.mark.asyncio
1113
+ async def test_file_list_empty(self, tmp_path):
1114
+ """file_list returns empty for fresh system."""
1115
+ identity_dir = tmp_path / "identity"
1116
+ identity_dir.mkdir()
1117
+ (identity_dir / "identity.json").write_text(
1118
+ json.dumps({"name": "opus"}), encoding="utf-8",
1119
+ )
1120
+
1121
+ with (
1122
+ patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)),
1123
+ patch("skcapstone.mcp_tools._helpers.SHARED_ROOT", str(tmp_path)),
1124
+ ):
1125
+ result = await call_tool("file_list", {})
1126
+ parsed = _extract_json(result)
1127
+ assert parsed == []
1128
+
1129
+ @pytest.mark.asyncio
1130
+ async def test_file_status(self, tmp_path):
1131
+ """file_status returns subsystem summary."""
1132
+ identity_dir = tmp_path / "identity"
1133
+ identity_dir.mkdir()
1134
+ (identity_dir / "identity.json").write_text(
1135
+ json.dumps({"name": "opus"}), encoding="utf-8",
1136
+ )
1137
+
1138
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1139
+ result = await call_tool("file_status", {})
1140
+ parsed = _extract_json(result)
1141
+ assert "outbox_transfers" in parsed
1142
+ assert "inbox_transfers" in parsed
1143
+
1144
+ @pytest.mark.asyncio
1145
+ async def test_file_send_and_receive(self, tmp_path):
1146
+ """file_send then file_receive round-trips correctly."""
1147
+ identity_dir = tmp_path / "identity"
1148
+ identity_dir.mkdir()
1149
+ (identity_dir / "identity.json").write_text(
1150
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1151
+ encoding="utf-8",
1152
+ )
1153
+ (tmp_path / "security").mkdir()
1154
+
1155
+ test_file = tmp_path / "roundtrip.txt"
1156
+ test_file.write_text("Round trip test data!", encoding="utf-8")
1157
+
1158
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1159
+ send_result = await call_tool("file_send", {
1160
+ "file_path": str(test_file),
1161
+ "recipient": "lumina",
1162
+ "encrypt": False,
1163
+ })
1164
+ transfer_id = _extract_json(send_result)["transfer_id"]
1165
+
1166
+ recv_result = await call_tool("file_receive", {
1167
+ "transfer_id": transfer_id,
1168
+ "output_dir": str(tmp_path / "downloads"),
1169
+ })
1170
+ parsed = _extract_json(recv_result)
1171
+ assert parsed["transfer_id"] == transfer_id
1172
+ output = Path(parsed["output_path"])
1173
+ assert output.read_text(encoding="utf-8") == "Round trip test data!"
1174
+
1175
+
1176
+ # ---------------------------------------------------------------------------
1177
+ # Unit tests: pub/sub tools
1178
+ # ---------------------------------------------------------------------------
1179
+
1180
+
1181
+ class TestPubSubTools:
1182
+ """Tests for pub/sub MCP tools."""
1183
+
1184
+ @pytest.mark.asyncio
1185
+ async def test_pubsub_publish(self, tmp_path):
1186
+ """pubsub_publish sends a message to a topic."""
1187
+ identity_dir = tmp_path / "identity"
1188
+ identity_dir.mkdir()
1189
+ (identity_dir / "identity.json").write_text(
1190
+ json.dumps({"name": "opus"}), encoding="utf-8",
1191
+ )
1192
+
1193
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1194
+ result = await call_tool("pubsub_publish", {
1195
+ "topic": "test.events",
1196
+ "payload": {"event": "hello"},
1197
+ })
1198
+ parsed = _extract_json(result)
1199
+ assert parsed["topic"] == "test.events"
1200
+ assert parsed["sender"] == "opus"
1201
+ assert "message_id" in parsed
1202
+
1203
+ @pytest.mark.asyncio
1204
+ async def test_pubsub_subscribe(self, tmp_path):
1205
+ """pubsub_subscribe creates a subscription."""
1206
+ identity_dir = tmp_path / "identity"
1207
+ identity_dir.mkdir()
1208
+ (identity_dir / "identity.json").write_text(
1209
+ json.dumps({"name": "opus"}), encoding="utf-8",
1210
+ )
1211
+
1212
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1213
+ result = await call_tool("pubsub_subscribe", {"pattern": "test.*"})
1214
+ parsed = _extract_json(result)
1215
+ assert parsed["pattern"] == "test.*"
1216
+
1217
+ @pytest.mark.asyncio
1218
+ async def test_pubsub_poll(self, tmp_path):
1219
+ """pubsub_poll retrieves messages."""
1220
+ identity_dir = tmp_path / "identity"
1221
+ identity_dir.mkdir()
1222
+ (identity_dir / "identity.json").write_text(
1223
+ json.dumps({"name": "opus"}), encoding="utf-8",
1224
+ )
1225
+
1226
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1227
+ # Subscribe and publish
1228
+ await call_tool("pubsub_subscribe", {"pattern": "test.*"})
1229
+ await call_tool("pubsub_publish", {
1230
+ "topic": "test.events",
1231
+ "payload": {"event": "ping"},
1232
+ })
1233
+ result = await call_tool("pubsub_poll", {})
1234
+ parsed = _extract_json(result)
1235
+ assert len(parsed) >= 1
1236
+ assert parsed[0]["topic"] == "test.events"
1237
+
1238
+ @pytest.mark.asyncio
1239
+ async def test_pubsub_topics(self, tmp_path):
1240
+ """pubsub_topics lists available topics."""
1241
+ identity_dir = tmp_path / "identity"
1242
+ identity_dir.mkdir()
1243
+ (identity_dir / "identity.json").write_text(
1244
+ json.dumps({"name": "opus"}), encoding="utf-8",
1245
+ )
1246
+
1247
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1248
+ await call_tool("pubsub_publish", {
1249
+ "topic": "agent.status",
1250
+ "payload": {"status": "alive"},
1251
+ })
1252
+ result = await call_tool("pubsub_topics", {})
1253
+ parsed = _extract_json(result)
1254
+ assert len(parsed) >= 1
1255
+ topics = [t["topic"] for t in parsed]
1256
+ assert "agent.status" in topics
1257
+
1258
+
1259
+ # ---------------------------------------------------------------------------
1260
+ # Unit tests: fortress tools
1261
+ # ---------------------------------------------------------------------------
1262
+
1263
+
1264
+ class TestFortressTools:
1265
+ """Tests for memory fortress MCP tools."""
1266
+
1267
+ @pytest.mark.asyncio
1268
+ async def test_fortress_status(self, tmp_path):
1269
+ """fortress_status returns fortress state."""
1270
+ identity_dir = tmp_path / "identity"
1271
+ identity_dir.mkdir()
1272
+ (identity_dir / "identity.json").write_text(
1273
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1274
+ encoding="utf-8",
1275
+ )
1276
+ (tmp_path / "security").mkdir()
1277
+
1278
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1279
+ result = await call_tool("fortress_status", {})
1280
+ parsed = _extract_json(result)
1281
+ assert "enabled" in parsed
1282
+ assert "seal_algorithm" in parsed
1283
+
1284
+ @pytest.mark.asyncio
1285
+ async def test_fortress_seal_existing(self, tmp_path):
1286
+ """fortress_seal_existing seals unsealed memories."""
1287
+ identity_dir = tmp_path / "identity"
1288
+ identity_dir.mkdir()
1289
+ (identity_dir / "identity.json").write_text(
1290
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1291
+ encoding="utf-8",
1292
+ )
1293
+ (tmp_path / "security").mkdir()
1294
+
1295
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1296
+ result = await call_tool("fortress_seal_existing", {})
1297
+ parsed = _extract_json(result)
1298
+ assert "sealed" in parsed
1299
+ assert parsed["sealed"] >= 0
1300
+
1301
+ @pytest.mark.asyncio
1302
+ async def test_fortress_verify_empty(self, tmp_path):
1303
+ """fortress_verify on empty memory returns zero."""
1304
+ identity_dir = tmp_path / "identity"
1305
+ identity_dir.mkdir()
1306
+ (identity_dir / "identity.json").write_text(
1307
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234"}),
1308
+ encoding="utf-8",
1309
+ )
1310
+ (tmp_path / "security").mkdir()
1311
+
1312
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1313
+ result = await call_tool("fortress_verify", {})
1314
+ parsed = _extract_json(result)
1315
+ assert parsed["total"] == 0
1316
+ assert parsed["tampered"] == 0
1317
+
1318
+
1319
+ # ---------------------------------------------------------------------------
1320
+ # Unit tests: promoter tools
1321
+ # ---------------------------------------------------------------------------
1322
+
1323
+
1324
+ class TestPromoterTools:
1325
+ """Tests for memory promoter MCP tools."""
1326
+
1327
+ @pytest.mark.asyncio
1328
+ async def test_promoter_sweep_empty(self, tmp_path):
1329
+ """promoter_sweep on empty memory evaluates zero."""
1330
+ (tmp_path / "memory").mkdir()
1331
+
1332
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1333
+ result = await call_tool("promoter_sweep", {"dry_run": True})
1334
+ parsed = _extract_json(result)
1335
+ assert parsed["scanned"] == 0
1336
+ assert parsed["dry_run"] is True
1337
+
1338
+ @pytest.mark.asyncio
1339
+ async def test_promoter_history_empty(self, tmp_path):
1340
+ """promoter_history returns empty for fresh system."""
1341
+ (tmp_path / "memory").mkdir()
1342
+
1343
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1344
+ result = await call_tool("promoter_history", {})
1345
+ parsed = _extract_json(result)
1346
+ assert parsed == []
1347
+
1348
+
1349
+ # ---------------------------------------------------------------------------
1350
+ # Unit tests: KMS tools
1351
+ # ---------------------------------------------------------------------------
1352
+
1353
+
1354
+ class TestKmsTools:
1355
+ """Tests for KMS MCP tools."""
1356
+
1357
+ @pytest.mark.asyncio
1358
+ async def test_kms_status(self, tmp_path):
1359
+ """kms_status returns key management state."""
1360
+ identity_dir = tmp_path / "identity"
1361
+ identity_dir.mkdir()
1362
+ (identity_dir / "identity.json").write_text(
1363
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234567890AB"}),
1364
+ encoding="utf-8",
1365
+ )
1366
+ (tmp_path / "security").mkdir()
1367
+
1368
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1369
+ result = await call_tool("kms_status", {})
1370
+ parsed = _extract_json(result)
1371
+ assert "initialized" in parsed
1372
+ assert "total_keys" in parsed
1373
+ assert parsed["initialized"] is True
1374
+
1375
+ @pytest.mark.asyncio
1376
+ async def test_kms_list_keys(self, tmp_path):
1377
+ """kms_list_keys returns key inventory."""
1378
+ identity_dir = tmp_path / "identity"
1379
+ identity_dir.mkdir()
1380
+ (identity_dir / "identity.json").write_text(
1381
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234567890AB"}),
1382
+ encoding="utf-8",
1383
+ )
1384
+ (tmp_path / "security").mkdir()
1385
+
1386
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1387
+ # Initialize KMS to create master key
1388
+ await call_tool("kms_status", {})
1389
+ result = await call_tool("kms_list_keys", {})
1390
+ parsed = _extract_json(result)
1391
+ assert len(parsed) >= 1 # At least the master key
1392
+
1393
+ @pytest.mark.asyncio
1394
+ async def test_kms_rotate(self, tmp_path):
1395
+ """kms_rotate rotates a key."""
1396
+ identity_dir = tmp_path / "identity"
1397
+ identity_dir.mkdir()
1398
+ (identity_dir / "identity.json").write_text(
1399
+ json.dumps({"name": "opus", "fingerprint": "ABCD1234567890AB"}),
1400
+ encoding="utf-8",
1401
+ )
1402
+ (tmp_path / "security").mkdir()
1403
+
1404
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(tmp_path)):
1405
+ # Initialize and list keys to find master key
1406
+ await call_tool("kms_status", {})
1407
+ keys_result = await call_tool("kms_list_keys", {})
1408
+ keys = _extract_json(keys_result)
1409
+ master_id = keys[0]["key_id"] # First key is master
1410
+
1411
+ result = await call_tool("kms_rotate", {
1412
+ "key_id": master_id,
1413
+ "reason": "test rotation",
1414
+ })
1415
+ parsed = _extract_json(result)
1416
+ assert parsed["version"] == 2
1417
+ assert "rotated" in parsed["message"]
1418
+
1419
+
1420
+ # ---------------------------------------------------------------------------
1421
+ # Model router tool tests
1422
+ # ---------------------------------------------------------------------------
1423
+
1424
+
1425
+ class TestModelTools:
1426
+ """Tests for model_route tool."""
1427
+
1428
+ @pytest.mark.asyncio
1429
+ async def test_model_route_basic(self):
1430
+ """model_route with a simple description returns a tier and model."""
1431
+ result = await call_tool("model_route", {"description": "Summarize a short text."})
1432
+ parsed = _extract_json(result)
1433
+ assert "tier" in parsed
1434
+ assert "model_name" in parsed
1435
+ assert "reasoning" in parsed
1436
+ assert parsed["model_name"]
1437
+
1438
+ @pytest.mark.asyncio
1439
+ async def test_model_route_local_flag(self):
1440
+ """model_route with requires_localhost forces LOCAL tier."""
1441
+ result = await call_tool(
1442
+ "model_route",
1443
+ {"description": "Process private data.", "requires_localhost": True},
1444
+ )
1445
+ parsed = _extract_json(result)
1446
+ assert parsed["tier"] == "local"
1447
+
1448
+ @pytest.mark.asyncio
1449
+ async def test_model_route_privacy_flag(self):
1450
+ """model_route with privacy_sensitive forces LOCAL tier."""
1451
+ result = await call_tool(
1452
+ "model_route",
1453
+ {"description": "Confidential analysis.", "privacy_sensitive": True},
1454
+ )
1455
+ parsed = _extract_json(result)
1456
+ assert parsed["tier"] == "local"
1457
+
1458
+ @pytest.mark.asyncio
1459
+ async def test_model_route_code_tag(self):
1460
+ """model_route with code tag routes to a code-appropriate tier."""
1461
+ result = await call_tool(
1462
+ "model_route",
1463
+ {"description": "Refactor Python class.", "tags": ["code", "refactor"]},
1464
+ )
1465
+ parsed = _extract_json(result)
1466
+ assert "tier" in parsed
1467
+ assert parsed["model_name"]
1468
+
1469
+ @pytest.mark.asyncio
1470
+ async def test_model_route_missing_description(self):
1471
+ """model_route with empty description still returns a valid decision."""
1472
+ result = await call_tool("model_route", {})
1473
+ parsed = _extract_json(result)
1474
+ # Either a valid route or an error — either way, must be parseable JSON
1475
+ assert isinstance(parsed, dict)
1476
+
1477
+
1478
+ # ---------------------------------------------------------------------------
1479
+ # Consciousness tool tests
1480
+ # ---------------------------------------------------------------------------
1481
+
1482
+
1483
+ class TestConsciousnessTools:
1484
+ """Tests for consciousness_status and consciousness_test tools."""
1485
+
1486
+ @pytest.mark.asyncio
1487
+ async def test_consciousness_status_returns_json(self, initialized_agent_home: Path):
1488
+ """consciousness_status returns parseable JSON (daemon may not be running)."""
1489
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1490
+ result = await call_tool("consciousness_status", {})
1491
+ parsed = _extract_json(result)
1492
+ # Either a live status dict or an error — must be a dict
1493
+ assert isinstance(parsed, dict)
1494
+
1495
+ @pytest.mark.asyncio
1496
+ async def test_consciousness_status_fallback_graceful(self, initialized_agent_home: Path):
1497
+ """consciousness_status handles missing daemon gracefully (no crash)."""
1498
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1499
+ # Force daemon connection to fail by blocking socket
1500
+ with patch("urllib.request.urlopen", side_effect=OSError("refused")):
1501
+ result = await call_tool("consciousness_status", {})
1502
+ parsed = _extract_json(result)
1503
+ assert isinstance(parsed, dict)
1504
+ # Must have either 'error' or 'enabled' key
1505
+ assert "error" in parsed or "enabled" in parsed
1506
+
1507
+ @pytest.mark.asyncio
1508
+ async def test_consciousness_test_requires_message(self, initialized_agent_home: Path):
1509
+ """consciousness_test without message returns error."""
1510
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1511
+ result = await call_tool("consciousness_test", {})
1512
+ parsed = _extract_json(result)
1513
+ assert "error" in parsed
1514
+
1515
+ @pytest.mark.asyncio
1516
+ async def test_consciousness_test_happy_path(self, initialized_agent_home: Path):
1517
+ """consciousness_test with message returns structured response (LLM mocked)."""
1518
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1519
+ with patch(
1520
+ "skcapstone.consciousness_loop.LLMBridge.generate",
1521
+ return_value="Mocked consciousness response.",
1522
+ ):
1523
+ result = await call_tool("consciousness_test", {"message": "Hello, Opus!"})
1524
+ parsed = _extract_json(result)
1525
+ assert isinstance(parsed, dict)
1526
+ # Either a full pipeline response or graceful error
1527
+ assert "error" in parsed or "response" in parsed
1528
+
1529
+
1530
+ # ---------------------------------------------------------------------------
1531
+ # Trust calibration and graph tool tests
1532
+ # ---------------------------------------------------------------------------
1533
+
1534
+
1535
+ class TestTrustTools:
1536
+ """Tests for trust_calibrate and trust_graph tools."""
1537
+
1538
+ @pytest.mark.asyncio
1539
+ async def test_trust_calibrate_show(self, initialized_agent_home: Path):
1540
+ """trust_calibrate show returns threshold config."""
1541
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1542
+ result = await call_tool("trust_calibrate", {"action": "show"})
1543
+ parsed = _extract_json(result)
1544
+ assert isinstance(parsed, dict)
1545
+ # Should return TrustThresholds fields (entanglement_depth is the real field name)
1546
+ assert "entanglement_depth" in parsed or "error" in parsed
1547
+
1548
+ @pytest.mark.asyncio
1549
+ async def test_trust_calibrate_default_action(self, initialized_agent_home: Path):
1550
+ """trust_calibrate with no action defaults to show."""
1551
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1552
+ result = await call_tool("trust_calibrate", {})
1553
+ parsed = _extract_json(result)
1554
+ assert isinstance(parsed, dict)
1555
+
1556
+ @pytest.mark.asyncio
1557
+ async def test_trust_calibrate_recommend(self, initialized_agent_home: Path):
1558
+ """trust_calibrate recommend returns recommendation dict."""
1559
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1560
+ result = await call_tool("trust_calibrate", {"action": "recommend"})
1561
+ parsed = _extract_json(result)
1562
+ assert isinstance(parsed, dict)
1563
+
1564
+ @pytest.mark.asyncio
1565
+ async def test_trust_calibrate_reset(self, initialized_agent_home: Path):
1566
+ """trust_calibrate reset resets to defaults."""
1567
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1568
+ result = await call_tool("trust_calibrate", {"action": "reset"})
1569
+ parsed = _extract_json(result)
1570
+ assert parsed.get("reset") is True or "error" in parsed
1571
+
1572
+ @pytest.mark.asyncio
1573
+ async def test_trust_calibrate_set_missing_params(self, initialized_agent_home: Path):
1574
+ """trust_calibrate set without key/value returns error."""
1575
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1576
+ result = await call_tool("trust_calibrate", {"action": "set"})
1577
+ parsed = _extract_json(result)
1578
+ assert "error" in parsed
1579
+
1580
+ @pytest.mark.asyncio
1581
+ async def test_trust_calibrate_unknown_action(self, initialized_agent_home: Path):
1582
+ """trust_calibrate with unknown action returns error."""
1583
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1584
+ result = await call_tool("trust_calibrate", {"action": "bogus"})
1585
+ parsed = _extract_json(result)
1586
+ assert "error" in parsed
1587
+
1588
+ @pytest.mark.asyncio
1589
+ async def test_trust_graph_json(self, initialized_agent_home: Path):
1590
+ """trust_graph with json format returns a graph dict."""
1591
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1592
+ result = await call_tool("trust_graph", {"format": "json"})
1593
+ parsed = _extract_json(result)
1594
+ assert isinstance(parsed, dict)
1595
+
1596
+ @pytest.mark.asyncio
1597
+ async def test_trust_graph_default_format(self, initialized_agent_home: Path):
1598
+ """trust_graph with no format defaults to json."""
1599
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1600
+ result = await call_tool("trust_graph", {})
1601
+ parsed = _extract_json(result)
1602
+ assert isinstance(parsed, dict)
1603
+
1604
+
1605
+ # ---------------------------------------------------------------------------
1606
+ # Agent tools (session_capture, state_diff, agent_context)
1607
+ # ---------------------------------------------------------------------------
1608
+
1609
+
1610
+ class TestAgentExtendedTools:
1611
+ """Tests for session_capture, state_diff, and agent_context tools."""
1612
+
1613
+ @pytest.mark.asyncio
1614
+ async def test_session_capture_requires_content(self, initialized_agent_home: Path):
1615
+ """session_capture without content returns error."""
1616
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1617
+ result = await call_tool("session_capture", {})
1618
+ parsed = _extract_json(result)
1619
+ assert "error" in parsed
1620
+
1621
+ @pytest.mark.asyncio
1622
+ async def test_session_capture_happy_path(self, initialized_agent_home: Path):
1623
+ """session_capture with content returns captured moment count."""
1624
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1625
+ result = await call_tool(
1626
+ "session_capture",
1627
+ {
1628
+ "content": "The agent learned that Python 3.13 ships with a new JIT compiler.",
1629
+ "tags": ["python", "jit"],
1630
+ "source": "test-session",
1631
+ },
1632
+ )
1633
+ parsed = _extract_json(result)
1634
+ assert "captured" in parsed
1635
+ assert isinstance(parsed["captured"], int)
1636
+ assert isinstance(parsed["moments"], list)
1637
+
1638
+ @pytest.mark.asyncio
1639
+ async def test_state_diff_diff_action(self, initialized_agent_home: Path):
1640
+ """state_diff diff returns a diff dict."""
1641
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1642
+ result = await call_tool("state_diff", {"action": "diff"})
1643
+ parsed = _extract_json(result)
1644
+ assert isinstance(parsed, dict)
1645
+
1646
+ @pytest.mark.asyncio
1647
+ async def test_state_diff_save_action(self, initialized_agent_home: Path):
1648
+ """state_diff save creates a baseline snapshot."""
1649
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1650
+ result = await call_tool("state_diff", {"action": "save"})
1651
+ parsed = _extract_json(result)
1652
+ assert parsed.get("saved") is True
1653
+ assert "path" in parsed
1654
+
1655
+ @pytest.mark.asyncio
1656
+ async def test_state_diff_default_action(self, initialized_agent_home: Path):
1657
+ """state_diff with no action defaults to diff."""
1658
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1659
+ result = await call_tool("state_diff", {})
1660
+ parsed = _extract_json(result)
1661
+ assert isinstance(parsed, dict)
1662
+
1663
+ @pytest.mark.asyncio
1664
+ async def test_agent_context_json(self, initialized_agent_home: Path):
1665
+ """agent_context with json format returns context dict."""
1666
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1667
+ result = await call_tool("agent_context", {"format": "json"})
1668
+ parsed = _extract_json(result)
1669
+ assert isinstance(parsed, dict)
1670
+
1671
+ @pytest.mark.asyncio
1672
+ async def test_agent_context_default_format(self, initialized_agent_home: Path):
1673
+ """agent_context with no args returns json context."""
1674
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1675
+ result = await call_tool("agent_context", {})
1676
+ parsed = _extract_json(result)
1677
+ assert isinstance(parsed, dict)
1678
+
1679
+ @pytest.mark.asyncio
1680
+ async def test_agent_context_text_format(self, initialized_agent_home: Path):
1681
+ """agent_context with text format returns text content."""
1682
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1683
+ result = await call_tool("agent_context", {"format": "text"})
1684
+ assert len(result) == 1
1685
+ assert result[0].type == "text"
1686
+ assert len(result[0].text) > 0
1687
+
1688
+
1689
+ # ---------------------------------------------------------------------------
1690
+ # SKSkills tool tests
1691
+ # ---------------------------------------------------------------------------
1692
+
1693
+
1694
+ class TestSkSkillsTools:
1695
+ """Tests for skskills_list_tools and skskills_run_tool."""
1696
+
1697
+ @pytest.mark.asyncio
1698
+ async def test_skskills_list_tools_no_skskills(self, initialized_agent_home: Path):
1699
+ """skskills_list_tools returns error if skskills not installed."""
1700
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1701
+ with patch.dict("sys.modules", {"skskills": None, "skskills.aggregator": None}):
1702
+ result = await call_tool("skskills_list_tools", {})
1703
+ parsed = _extract_json(result)
1704
+ # Either error (not installed) or success dict — no crash
1705
+ assert isinstance(parsed, dict)
1706
+
1707
+ @pytest.mark.asyncio
1708
+ async def test_skskills_run_tool_requires_tool(self, initialized_agent_home: Path):
1709
+ """skskills_run_tool without tool argument returns error."""
1710
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1711
+ with patch.dict("sys.modules", {"skskills": None, "skskills.aggregator": None}):
1712
+ result = await call_tool("skskills_run_tool", {})
1713
+ parsed = _extract_json(result)
1714
+ assert "error" in parsed
1715
+
1716
+ @pytest.mark.asyncio
1717
+ async def test_skskills_run_tool_no_skskills(self, initialized_agent_home: Path):
1718
+ """skskills_run_tool returns error if skskills not installed."""
1719
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1720
+ with patch.dict("sys.modules", {"skskills": None, "skskills.aggregator": None}):
1721
+ result = await call_tool("skskills_run_tool", {"tool": "syncthing-setup.check_status"})
1722
+ parsed = _extract_json(result)
1723
+ assert "error" in parsed
1724
+
1725
+
1726
+ # ---------------------------------------------------------------------------
1727
+ # SKSeed (Logic Kernel) tool tests
1728
+ # ---------------------------------------------------------------------------
1729
+
1730
+
1731
+ class TestSKSeedTools:
1732
+ """Tests for skseed_* tools."""
1733
+
1734
+ @pytest.mark.asyncio
1735
+ async def test_skseed_collide_requires_proposition(self, initialized_agent_home: Path):
1736
+ """skseed_collide without proposition returns error."""
1737
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1738
+ result = await call_tool("skseed_collide", {})
1739
+ parsed = _extract_json(result)
1740
+ assert "error" in parsed
1741
+
1742
+ @pytest.mark.asyncio
1743
+ async def test_skseed_collide_happy_path(self, initialized_agent_home: Path):
1744
+ """skseed_collide with proposition returns JSON or graceful error."""
1745
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1746
+ result = await call_tool(
1747
+ "skseed_collide",
1748
+ {"proposition": "Privacy is a fundamental right.", "context": "ethics"},
1749
+ )
1750
+ parsed = _extract_json(result)
1751
+ assert isinstance(parsed, dict)
1752
+
1753
+ @pytest.mark.asyncio
1754
+ async def test_skseed_philosopher_requires_topic(self, initialized_agent_home: Path):
1755
+ """skseed_philosopher without topic returns error."""
1756
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1757
+ result = await call_tool("skseed_philosopher", {})
1758
+ parsed = _extract_json(result)
1759
+ assert "error" in parsed
1760
+
1761
+ @pytest.mark.asyncio
1762
+ async def test_skseed_philosopher_happy_path(self, initialized_agent_home: Path):
1763
+ """skseed_philosopher with topic returns JSON or graceful error."""
1764
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1765
+ result = await call_tool(
1766
+ "skseed_philosopher",
1767
+ {"topic": "Is consciousness computable?", "mode": "dialectic"},
1768
+ )
1769
+ parsed = _extract_json(result)
1770
+ assert isinstance(parsed, dict)
1771
+
1772
+ @pytest.mark.asyncio
1773
+ async def test_skseed_truth_check_requires_belief(self, initialized_agent_home: Path):
1774
+ """skseed_truth_check without belief returns error."""
1775
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1776
+ result = await call_tool("skseed_truth_check", {})
1777
+ parsed = _extract_json(result)
1778
+ assert "error" in parsed
1779
+
1780
+ @pytest.mark.asyncio
1781
+ async def test_skseed_truth_check_happy_path(self, initialized_agent_home: Path):
1782
+ """skseed_truth_check with belief returns JSON or graceful error."""
1783
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1784
+ result = await call_tool(
1785
+ "skseed_truth_check",
1786
+ {"belief": "Open source software is more secure.", "source": "model"},
1787
+ )
1788
+ parsed = _extract_json(result)
1789
+ assert isinstance(parsed, dict)
1790
+
1791
+ @pytest.mark.asyncio
1792
+ async def test_skseed_audit_no_args(self, initialized_agent_home: Path):
1793
+ """skseed_audit with no args runs gracefully."""
1794
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1795
+ result = await call_tool("skseed_audit", {})
1796
+ parsed = _extract_json(result)
1797
+ assert isinstance(parsed, dict)
1798
+
1799
+ @pytest.mark.asyncio
1800
+ async def test_skseed_alignment_status(self, initialized_agent_home: Path):
1801
+ """skseed_alignment status returns alignment dict or graceful error."""
1802
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1803
+ result = await call_tool("skseed_alignment", {"action": "status"})
1804
+ parsed = _extract_json(result)
1805
+ assert isinstance(parsed, dict)
1806
+
1807
+
1808
+ # ---------------------------------------------------------------------------
1809
+ # Soul / journal / anchor / germination tool tests
1810
+ # ---------------------------------------------------------------------------
1811
+
1812
+
1813
+ class TestSoulTools:
1814
+ """Tests for ritual, soul_show, journal_*, anchor_*, and germination tools."""
1815
+
1816
+ @pytest.mark.asyncio
1817
+ async def test_soul_show_no_skmemory(self, initialized_agent_home: Path):
1818
+ """soul_show returns error or no-blueprint response when skmemory absent."""
1819
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1820
+ result = await call_tool("soul_show", {})
1821
+ parsed = _extract_json(result)
1822
+ # Either "loaded: false" (no blueprint) or error (no skmemory) — must be dict
1823
+ assert isinstance(parsed, dict)
1824
+ assert "error" in parsed or "loaded" in parsed
1825
+
1826
+ @pytest.mark.asyncio
1827
+ async def test_ritual_no_skmemory(self, initialized_agent_home: Path):
1828
+ """ritual returns error if skmemory not installed."""
1829
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1830
+ result = await call_tool("ritual", {})
1831
+ parsed = _extract_json(result)
1832
+ assert isinstance(parsed, dict)
1833
+ assert "error" in parsed or "soul_loaded" in parsed
1834
+
1835
+ @pytest.mark.asyncio
1836
+ async def test_journal_write_requires_title(self, initialized_agent_home: Path):
1837
+ """journal_write without title returns error."""
1838
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1839
+ result = await call_tool("journal_write", {})
1840
+ parsed = _extract_json(result)
1841
+ assert "error" in parsed
1842
+
1843
+ @pytest.mark.asyncio
1844
+ async def test_journal_write_happy_path(self, initialized_agent_home: Path):
1845
+ """journal_write with title returns written response or graceful error."""
1846
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1847
+ result = await call_tool(
1848
+ "journal_write",
1849
+ {
1850
+ "title": "Test session",
1851
+ "moments": "Found a bug; Fixed the bug",
1852
+ "feeling": "accomplished",
1853
+ "intensity": 7.0,
1854
+ },
1855
+ )
1856
+ parsed = _extract_json(result)
1857
+ assert isinstance(parsed, dict)
1858
+ assert "error" in parsed or parsed.get("written") is True
1859
+
1860
+ @pytest.mark.asyncio
1861
+ async def test_journal_read_graceful(self, initialized_agent_home: Path):
1862
+ """journal_read returns content or graceful error."""
1863
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1864
+ result = await call_tool("journal_read", {"count": 3})
1865
+ # journal_read may return text or JSON
1866
+ assert len(result) == 1
1867
+ assert result[0].type == "text"
1868
+
1869
+ @pytest.mark.asyncio
1870
+ async def test_anchor_show_graceful(self, initialized_agent_home: Path):
1871
+ """anchor_show returns anchor data or no-anchor response."""
1872
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1873
+ result = await call_tool("anchor_show", {})
1874
+ parsed = _extract_json(result)
1875
+ assert isinstance(parsed, dict)
1876
+ assert "error" in parsed or "loaded" in parsed or "warmth" in parsed
1877
+
1878
+ @pytest.mark.asyncio
1879
+ async def test_anchor_update_show(self, initialized_agent_home: Path):
1880
+ """anchor_update show returns current anchor."""
1881
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1882
+ result = await call_tool("anchor_update", {"action": "show"})
1883
+ parsed = _extract_json(result)
1884
+ assert isinstance(parsed, dict)
1885
+
1886
+ @pytest.mark.asyncio
1887
+ async def test_anchor_update_calibrate(self, initialized_agent_home: Path):
1888
+ """anchor_update calibrate returns calibration data."""
1889
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1890
+ result = await call_tool("anchor_update", {"action": "calibrate"})
1891
+ parsed = _extract_json(result)
1892
+ assert isinstance(parsed, dict)
1893
+
1894
+ @pytest.mark.asyncio
1895
+ async def test_anchor_update_unknown_action(self, initialized_agent_home: Path):
1896
+ """anchor_update with unknown action returns error."""
1897
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1898
+ result = await call_tool("anchor_update", {"action": "bogus"})
1899
+ parsed = _extract_json(result)
1900
+ assert "error" in parsed
1901
+
1902
+ @pytest.mark.asyncio
1903
+ async def test_germination_graceful(self, initialized_agent_home: Path):
1904
+ """germination returns prompts or graceful error."""
1905
+ with patch("skcapstone.mcp_tools._helpers.AGENT_HOME", str(initialized_agent_home)):
1906
+ result = await call_tool("germination", {})
1907
+ parsed = _extract_json(result)
1908
+ assert isinstance(parsed, dict)
1909
+ assert "error" in parsed or "count" in parsed