@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,254 @@
1
+ """Tests for sovereign agent backup and restore."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import tarfile
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from skcapstone.backup import (
12
+ BackupManifest,
13
+ create_backup,
14
+ list_backups,
15
+ restore_backup,
16
+ )
17
+
18
+
19
+ def _setup_agent_home(tmp_path: Path) -> Path:
20
+ """Create a fake agent home directory with test data.
21
+
22
+ Args:
23
+ tmp_path: Temporary directory from pytest.
24
+
25
+ Returns:
26
+ Path: The fake agent home.
27
+ """
28
+ home = tmp_path / ".skcapstone"
29
+
30
+ (home / "config").mkdir(parents=True)
31
+ (home / "config" / "config.yaml").write_text('agent_name: "TestAgent"\n')
32
+
33
+ (home / "identity").mkdir()
34
+ (home / "identity" / "profile.json").write_text('{"name": "TestAgent"}')
35
+ (home / "identity" / "public.asc").write_text("-----BEGIN PGP PUBLIC KEY-----\ntest\n-----END PGP PUBLIC KEY-----\n")
36
+
37
+ (home / "memory").mkdir()
38
+ (home / "memory" / "mem1.json").write_text('{"id": "mem1", "title": "test memory"}')
39
+ (home / "memory" / "mem2.json").write_text('{"id": "mem2", "title": "another memory"}')
40
+
41
+ (home / "trust").mkdir()
42
+ (home / "trust" / "FEB_test.feb").write_text('{"emotional_payload": {}}')
43
+
44
+ (home / "soul").mkdir()
45
+ (home / "soul" / "lumina.yaml").write_text('name: lumina\npersonality: warm\n')
46
+
47
+ (home / "conversations").mkdir()
48
+ (home / "conversations" / "conv1.json").write_text('{"id": "conv1", "messages": []}')
49
+
50
+ # Ephemeral dirs — must NOT be backed up
51
+ (home / "sync").mkdir()
52
+ (home / "sync" / "seed.json").write_text('{"ephemeral": true}')
53
+
54
+ (home / "heartbeats").mkdir()
55
+ (home / "heartbeats" / "opus.json").write_text('{"alive": true}')
56
+
57
+ (home / "coordination" / "tasks").mkdir(parents=True)
58
+ (home / "coordination" / "tasks" / "task1.json").write_text('{"status": "done"}')
59
+
60
+ (home / "manifest.json").write_text('{"name": "TestAgent", "version": "0.1.0"}')
61
+ (home / "agent-card.json").write_text('{"name": "TestAgent", "fingerprint": "abc123"}')
62
+
63
+ return home
64
+
65
+
66
+ class TestCreateBackup:
67
+ """Tests for backup creation."""
68
+
69
+ def test_create_basic_backup(self, tmp_path: Path) -> None:
70
+ """Happy path: backup creates a valid tar.gz archive."""
71
+ home = _setup_agent_home(tmp_path)
72
+ out_dir = tmp_path / "backups"
73
+
74
+ result = create_backup(home=home, output_dir=out_dir, agent_name="TestAgent")
75
+
76
+ assert result["file_count"] > 0
77
+ assert result["archive_size"] > 0
78
+ assert Path(result["filepath"]).exists()
79
+ assert result["filepath"].endswith(".tar.gz")
80
+
81
+ def test_backup_contains_expected_files(self, tmp_path: Path) -> None:
82
+ """Archive contains identity, memory, config, soul, conversations, and manifest."""
83
+ home = _setup_agent_home(tmp_path)
84
+ result = create_backup(home=home, output_dir=tmp_path / "out")
85
+
86
+ with tarfile.open(result["filepath"], "r:gz") as tar:
87
+ names = tar.getnames()
88
+
89
+ assert any("config/config.yaml" in n for n in names)
90
+ assert any("identity/profile.json" in n for n in names)
91
+ assert any("memory/mem1.json" in n for n in names)
92
+ assert any("manifest.json" in n for n in names)
93
+ assert any("soul/lumina.yaml" in n for n in names)
94
+ assert any("conversations/conv1.json" in n for n in names)
95
+
96
+ def test_backup_excludes_ephemeral_dirs(self, tmp_path: Path) -> None:
97
+ """Ephemeral dirs (sync/, heartbeats/, coordination/tasks/) are not backed up."""
98
+ home = _setup_agent_home(tmp_path)
99
+ result = create_backup(home=home, output_dir=tmp_path / "out")
100
+
101
+ with tarfile.open(result["filepath"], "r:gz") as tar:
102
+ names = tar.getnames()
103
+
104
+ assert not any("/sync/" in n or n.endswith("/sync") for n in names)
105
+ assert not any("/heartbeats/" in n or n.endswith("/heartbeats") for n in names)
106
+ assert not any("coordination/tasks" in n for n in names)
107
+
108
+ def test_backup_manifest_has_checksums(self, tmp_path: Path) -> None:
109
+ """Manifest contains SHA-256 checksums for all files."""
110
+ home = _setup_agent_home(tmp_path)
111
+ result = create_backup(home=home, output_dir=tmp_path / "out")
112
+
113
+ manifest = result["manifest"]
114
+ assert len(manifest["files"]) > 0
115
+ for filepath, checksum in manifest["files"].items():
116
+ assert len(checksum) == 64
117
+
118
+ def test_backup_excludes_pycache(self, tmp_path: Path) -> None:
119
+ """Backup skips __pycache__ directories."""
120
+ home = _setup_agent_home(tmp_path)
121
+ cache_dir = home / "memory" / "__pycache__"
122
+ cache_dir.mkdir()
123
+ (cache_dir / "module.cpython-312.pyc").write_text("bytecode")
124
+
125
+ result = create_backup(home=home, output_dir=tmp_path / "out")
126
+
127
+ with tarfile.open(result["filepath"], "r:gz") as tar:
128
+ names = tar.getnames()
129
+
130
+ assert not any("__pycache__" in n for n in names)
131
+ assert not any(".pyc" in n for n in names)
132
+
133
+ def test_backup_missing_home_raises(self, tmp_path: Path) -> None:
134
+ """Backup raises FileNotFoundError for missing home."""
135
+ with pytest.raises(FileNotFoundError):
136
+ create_backup(home=tmp_path / "nonexistent")
137
+
138
+
139
+ class TestRestoreBackup:
140
+ """Tests for backup restoration."""
141
+
142
+ def test_restore_roundtrip(self, tmp_path: Path) -> None:
143
+ """Backup then restore produces identical files."""
144
+ home = _setup_agent_home(tmp_path)
145
+ result = create_backup(home=home, output_dir=tmp_path / "out")
146
+
147
+ restore_target = tmp_path / "restored"
148
+ restore_result = restore_backup(
149
+ archive_path=result["filepath"],
150
+ target_home=restore_target,
151
+ )
152
+
153
+ assert restore_result["file_count"] > 0
154
+ assert restore_result["verified"] is True
155
+ assert len(restore_result["errors"]) == 0
156
+
157
+ assert (restore_target / "config" / "config.yaml").exists()
158
+ assert (restore_target / "identity" / "profile.json").exists()
159
+ assert (restore_target / "memory" / "mem1.json").exists()
160
+ assert (restore_target / "soul" / "lumina.yaml").exists()
161
+ assert (restore_target / "conversations" / "conv1.json").exists()
162
+
163
+ def test_restore_detects_tampered_file(self, tmp_path: Path) -> None:
164
+ """Verification catches files that don't match manifest checksums."""
165
+ home = _setup_agent_home(tmp_path)
166
+ result = create_backup(home=home, output_dir=tmp_path / "out")
167
+
168
+ restore_target = tmp_path / "tampered"
169
+ restore_backup(
170
+ archive_path=result["filepath"],
171
+ target_home=restore_target,
172
+ verify=False,
173
+ )
174
+
175
+ # Tamper after extraction, then verify by comparing to manifest
176
+ (restore_target / "memory" / "mem1.json").write_text("TAMPERED!")
177
+
178
+ from skcapstone.backup import BackupManifest, _sha256_file
179
+
180
+ manifest = BackupManifest(**result["manifest"])
181
+ errors = []
182
+ for rel_path, expected in manifest.files.items():
183
+ f = restore_target / rel_path
184
+ if f.exists() and _sha256_file(f) != expected:
185
+ errors.append(rel_path)
186
+
187
+ assert len(errors) > 0
188
+ assert any("mem1" in e for e in errors)
189
+
190
+ def test_restore_missing_archive_raises(self, tmp_path: Path) -> None:
191
+ """Restore raises FileNotFoundError for missing archive."""
192
+ with pytest.raises(FileNotFoundError):
193
+ restore_backup(archive_path="/nonexistent.tar.gz")
194
+
195
+ def test_restore_no_verify(self, tmp_path: Path) -> None:
196
+ """Restore with verify=False skips checksum checks."""
197
+ home = _setup_agent_home(tmp_path)
198
+ result = create_backup(home=home, output_dir=tmp_path / "out")
199
+
200
+ restore_target = tmp_path / "noverify"
201
+ restore_result = restore_backup(
202
+ archive_path=result["filepath"],
203
+ target_home=restore_target,
204
+ verify=False,
205
+ )
206
+ assert restore_result["file_count"] > 0
207
+
208
+
209
+ class TestListBackups:
210
+ """Tests for backup listing."""
211
+
212
+ def test_list_empty_dir(self, tmp_path: Path) -> None:
213
+ """Empty directory returns empty list."""
214
+ assert list_backups(tmp_path) == []
215
+
216
+ def test_list_nonexistent_dir(self, tmp_path: Path) -> None:
217
+ """Missing directory returns empty list."""
218
+ assert list_backups(tmp_path / "nope") == []
219
+
220
+ def test_list_with_backups(self, tmp_path: Path) -> None:
221
+ """Lists backup archives sorted newest first."""
222
+ home = _setup_agent_home(tmp_path)
223
+ out = tmp_path / "backups"
224
+
225
+ create_backup(home=home, output_dir=out)
226
+ create_backup(home=home, output_dir=out)
227
+
228
+ backups = list_backups(out)
229
+ assert len(backups) == 2
230
+ assert all(b["filename"].endswith(".tar.gz") for b in backups)
231
+ assert all(b["size"] > 0 for b in backups)
232
+
233
+
234
+ class TestBackupManifest:
235
+ """Tests for the manifest model."""
236
+
237
+ def test_manifest_defaults(self) -> None:
238
+ """Manifest has sensible defaults."""
239
+ m = BackupManifest()
240
+ assert m.version == "0.9.0"
241
+ assert m.files == {}
242
+ assert m.total_size == 0
243
+
244
+ def test_manifest_serialization(self) -> None:
245
+ """Manifest roundtrips through JSON."""
246
+ m = BackupManifest(
247
+ backup_id="test-123",
248
+ agent_name="Jarvis",
249
+ files={"config.yaml": "abc123"},
250
+ )
251
+ data = json.loads(m.model_dump_json())
252
+ loaded = BackupManifest(**data)
253
+ assert loaded.backup_id == "test-123"
254
+ assert loaded.files["config.yaml"] == "abc123"
@@ -0,0 +1,366 @@
1
+ """Tests for ``skcapstone benchmark`` — LLM backend latency benchmarking.
2
+
3
+ Covers:
4
+ - BenchmarkRunner.detect_backends() availability detection
5
+ - BenchmarkRunner.run_backend() per-backend call and error handling
6
+ - BenchmarkRunner.run_all() result aggregation (skip_unavailable logic)
7
+ - CLI rendering: table output and JSON output modes
8
+ - Passthrough always succeeds with zero external dependencies
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from unittest.mock import MagicMock, patch
16
+
17
+ import pytest
18
+ from click.testing import CliRunner
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Helpers
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ def _make_runner(prompt: str = "Hello", timeout: float = 5.0):
27
+ from skcapstone.cli.benchmark import BenchmarkRunner
28
+
29
+ return BenchmarkRunner(prompt=prompt, timeout=timeout)
30
+
31
+
32
+ def _ok(backend: str, ms: float = 42.0, model: str = "test-model") -> dict:
33
+ return {"backend": backend, "status": "ok", "ms": ms, "model": model, "error": None}
34
+
35
+
36
+ def _unavail(backend: str) -> dict:
37
+ return {"backend": backend, "status": "unavailable", "ms": None, "model": None, "error": None}
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # detect_backends
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ class TestDetectBackends:
46
+ """BenchmarkRunner.detect_backends() tests."""
47
+
48
+ def test_passthrough_always_available(self):
49
+ """passthrough must always be True regardless of env."""
50
+ runner = _make_runner()
51
+ with patch.object(runner, "_probe_ollama", return_value=False):
52
+ detected = runner.detect_backends()
53
+ assert detected["passthrough"] is True
54
+
55
+ def test_ollama_available_when_probe_succeeds(self):
56
+ """ollama is True when _probe_ollama returns True."""
57
+ runner = _make_runner()
58
+ with patch.object(runner, "_probe_ollama", return_value=True):
59
+ detected = runner.detect_backends()
60
+ assert detected["ollama"] is True
61
+
62
+ def test_ollama_unavailable_when_probe_fails(self):
63
+ """ollama is False when _probe_ollama returns False."""
64
+ runner = _make_runner()
65
+ with patch.object(runner, "_probe_ollama", return_value=False):
66
+ detected = runner.detect_backends()
67
+ assert detected["ollama"] is False
68
+
69
+ def test_cloud_backend_requires_env_var(self):
70
+ """Cloud backends are True only when the right env var is set."""
71
+ runner = _make_runner()
72
+ env_cases = {
73
+ "anthropic": "ANTHROPIC_API_KEY",
74
+ "openai": "OPENAI_API_KEY",
75
+ "grok": "XAI_API_KEY",
76
+ "kimi": "MOONSHOT_API_KEY",
77
+ "nvidia": "NVIDIA_API_KEY",
78
+ }
79
+ with patch.object(runner, "_probe_ollama", return_value=False):
80
+ # No keys set — all cloud backends False
81
+ clean_env = {k: "" for k in env_cases.values()}
82
+ with patch.dict(os.environ, clean_env, clear=False):
83
+ # Temporarily remove the keys
84
+ for var in env_cases.values():
85
+ os.environ.pop(var, None)
86
+ detected = runner.detect_backends()
87
+ for name in env_cases:
88
+ assert detected[name] is False, f"{name} should be unavailable without key"
89
+
90
+ def test_anthropic_available_with_env_var(self):
91
+ """anthropic becomes available when ANTHROPIC_API_KEY is set."""
92
+ runner = _make_runner()
93
+ with patch.object(runner, "_probe_ollama", return_value=False), \
94
+ patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test-key"}):
95
+ detected = runner.detect_backends()
96
+ assert detected["anthropic"] is True
97
+
98
+ def test_all_backends_listed(self):
99
+ """detect_backends() covers all known backends."""
100
+ from skcapstone.cli.benchmark import BACKENDS
101
+
102
+ runner = _make_runner()
103
+ with patch.object(runner, "_probe_ollama", return_value=False):
104
+ detected = runner.detect_backends()
105
+ for name in BACKENDS:
106
+ assert name in detected, f"Backend '{name}' missing from detect_backends() result"
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # run_backend — passthrough
111
+ # ---------------------------------------------------------------------------
112
+
113
+
114
+ class TestRunBackendPassthrough:
115
+ """Passthrough backend — always ok, no external calls."""
116
+
117
+ def test_passthrough_returns_ok(self):
118
+ """_bench_passthrough returns status=ok with a float ms value."""
119
+ runner = _make_runner()
120
+ result = runner._bench_passthrough()
121
+ assert result["status"] == "ok"
122
+ assert isinstance(result["ms"], float)
123
+ assert result["model"] == "mock"
124
+ assert result["error"] is None
125
+
126
+ def test_passthrough_ms_is_non_negative(self):
127
+ """Passthrough latency must be >= 0."""
128
+ runner = _make_runner()
129
+ result = runner._bench_passthrough()
130
+ assert result["ms"] >= 0.0
131
+
132
+ def test_run_backend_passthrough_via_dispatch(self):
133
+ """run_backend('passthrough') dispatches to _bench_passthrough."""
134
+ runner = _make_runner()
135
+ result = runner.run_backend("passthrough")
136
+ assert result["backend"] == "passthrough"
137
+ assert result["status"] == "ok"
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # run_backend — error handling
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ class TestRunBackendErrors:
146
+ """run_backend() error-handling for failed or unsupported backends."""
147
+
148
+ def test_run_backend_catches_exception(self):
149
+ """Exceptions from _bench_* are caught and returned as status=error."""
150
+ runner = _make_runner()
151
+ with patch.object(runner, "_bench_ollama", side_effect=RuntimeError("connection refused")):
152
+ result = runner.run_backend("ollama")
153
+ assert result["status"] == "error"
154
+ assert result["ms"] is None
155
+ assert "connection refused" in result["error"]
156
+
157
+ def test_run_backend_unsupported_name(self):
158
+ """Unknown backend name returns status=unsupported."""
159
+ runner = _make_runner()
160
+ result = runner.run_backend("nonexistent_backend_xyz")
161
+ assert result["status"] == "unsupported"
162
+ assert result["ms"] is None
163
+
164
+ def test_run_backend_error_truncates_long_message(self):
165
+ """Long exception messages are truncated to 120 chars."""
166
+ runner = _make_runner()
167
+ long_msg = "x" * 200
168
+ with patch.object(runner, "_bench_ollama", side_effect=RuntimeError(long_msg)):
169
+ result = runner.run_backend("ollama")
170
+ assert len(result["error"]) <= 120
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # run_all — aggregation
175
+ # ---------------------------------------------------------------------------
176
+
177
+
178
+ class TestRunAll:
179
+ """BenchmarkRunner.run_all() aggregation logic."""
180
+
181
+ def test_run_all_skips_unavailable_by_default(self):
182
+ """Unavailable backends appear with status=unavailable, not called."""
183
+ runner = _make_runner()
184
+ # All unavailable except passthrough
185
+ unavail = {name: False for name in runner.detect_backends()}
186
+ unavail["passthrough"] = True
187
+
188
+ with patch.object(runner, "detect_backends", return_value=unavail), \
189
+ patch.object(runner, "_bench_passthrough", return_value=_ok("passthrough")):
190
+ results = runner.run_all()
191
+
192
+ by_name = {r["backend"]: r for r in results}
193
+ assert by_name["passthrough"]["status"] == "ok"
194
+ for name, avail in unavail.items():
195
+ if not avail:
196
+ assert by_name[name]["status"] == "unavailable"
197
+
198
+ def test_run_all_returns_one_result_per_backend(self):
199
+ """run_all() always returns exactly len(BACKENDS) results."""
200
+ from skcapstone.cli.benchmark import BACKENDS
201
+
202
+ runner = _make_runner()
203
+ with patch.object(runner, "detect_backends", return_value={n: False for n in BACKENDS}):
204
+ # passthrough would still be run — override detect_backends to all-False
205
+ # except passthrough
206
+ avail = {n: False for n in BACKENDS}
207
+ avail["passthrough"] = True
208
+ with patch.object(runner, "detect_backends", return_value=avail), \
209
+ patch.object(runner, "_bench_passthrough", return_value=_ok("passthrough")):
210
+ results = runner.run_all()
211
+
212
+ assert len(results) == len(BACKENDS)
213
+
214
+ def test_run_all_collects_errors(self):
215
+ """Failing backends are included with status=error, not raised."""
216
+ runner = _make_runner()
217
+ avail = {n: False for n in runner.detect_backends()}
218
+ avail["ollama"] = True
219
+
220
+ with patch.object(runner, "detect_backends", return_value=avail), \
221
+ patch.object(runner, "_bench_ollama", side_effect=OSError("network down")):
222
+ results = runner.run_all()
223
+
224
+ ollama_result = next(r for r in results if r["backend"] == "ollama")
225
+ assert ollama_result["status"] == "error"
226
+ assert "network down" in ollama_result["error"]
227
+
228
+ def test_run_all_includes_ok_results(self):
229
+ """Successful backends appear with status=ok and a float ms."""
230
+ runner = _make_runner()
231
+ avail = {n: False for n in runner.detect_backends()}
232
+ avail["passthrough"] = True
233
+
234
+ with patch.object(runner, "detect_backends", return_value=avail), \
235
+ patch.object(runner, "_bench_passthrough", return_value=_ok("passthrough", ms=7.5)):
236
+ results = runner.run_all()
237
+
238
+ pt = next(r for r in results if r["backend"] == "passthrough")
239
+ assert pt["status"] == "ok"
240
+ assert pt["ms"] == 7.5
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Ollama benchmark (mocked HTTP)
245
+ # ---------------------------------------------------------------------------
246
+
247
+
248
+ class TestBenchOllama:
249
+ """_bench_ollama with mocked urllib."""
250
+
251
+ def test_bench_ollama_ok(self):
252
+ """Successful Ollama call returns status=ok with ms and model."""
253
+ runner = _make_runner()
254
+ fake_response = json.dumps({"model": "llama3.2", "response": "Hi!"}).encode()
255
+
256
+ mock_resp = MagicMock()
257
+ mock_resp.__enter__ = lambda s: s
258
+ mock_resp.__exit__ = MagicMock(return_value=False)
259
+ mock_resp.read.return_value = fake_response
260
+
261
+ with patch("urllib.request.urlopen", return_value=mock_resp):
262
+ result = runner._bench_ollama()
263
+
264
+ assert result["status"] == "ok"
265
+ assert result["model"] == "llama3.2"
266
+ assert isinstance(result["ms"], float)
267
+ assert result["ms"] >= 0
268
+
269
+ def test_bench_ollama_network_error_propagates(self):
270
+ """Network errors from urlopen are re-raised (caught by run_backend)."""
271
+ runner = _make_runner()
272
+
273
+ with patch("urllib.request.urlopen", side_effect=OSError("refused")):
274
+ with pytest.raises(OSError):
275
+ runner._bench_ollama()
276
+
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # CLI command tests
280
+ # ---------------------------------------------------------------------------
281
+
282
+
283
+ class TestBenchmarkCLI:
284
+ """CLI integration tests using CliRunner."""
285
+
286
+ def _invoke(self, *args):
287
+ from skcapstone.cli import main
288
+ runner = CliRunner()
289
+ return runner.invoke(main, ["benchmark", *args])
290
+
291
+ def test_help(self):
292
+ """benchmark --help exits 0 and shows key options."""
293
+ result = self._invoke("--help")
294
+ assert result.exit_code == 0
295
+ assert "--prompt" in result.output
296
+ assert "--timeout" in result.output
297
+ assert "--json-out" in result.output
298
+
299
+ def test_json_output(self):
300
+ """--json-out emits valid JSON list."""
301
+ from skcapstone.cli.benchmark import BenchmarkRunner
302
+
303
+ fake_results = [_ok("passthrough"), _unavail("ollama")]
304
+ with patch.object(BenchmarkRunner, "run_all", return_value=fake_results):
305
+ result = self._invoke("--json-out")
306
+
307
+ assert result.exit_code == 0
308
+ data = json.loads(result.output)
309
+ assert isinstance(data, list)
310
+ assert data[0]["backend"] == "passthrough"
311
+ assert data[0]["status"] == "ok"
312
+
313
+ def test_table_output_contains_backend_names(self):
314
+ """Default table output contains backend names."""
315
+ from skcapstone.cli.benchmark import BenchmarkRunner
316
+
317
+ fake_results = [
318
+ _ok("passthrough", ms=1.2),
319
+ _unavail("ollama"),
320
+ ]
321
+ with patch.object(BenchmarkRunner, "run_all", return_value=fake_results):
322
+ result = self._invoke()
323
+
324
+ assert result.exit_code == 0
325
+ assert "passthrough" in result.output
326
+ assert "ollama" in result.output
327
+
328
+ def test_custom_prompt_passed_to_runner(self):
329
+ """--prompt value is forwarded to BenchmarkRunner."""
330
+ from skcapstone.cli.benchmark import BenchmarkRunner
331
+
332
+ with patch.object(BenchmarkRunner, "__init__", return_value=None) as mock_init, \
333
+ patch.object(BenchmarkRunner, "run_all", return_value=[]):
334
+ self._invoke("--prompt", "Ping", "--json-out")
335
+
336
+ call_kwargs = mock_init.call_args
337
+ assert call_kwargs is not None
338
+ # prompt is passed as keyword or positional arg
339
+ all_args = list(call_kwargs.args) + list(call_kwargs.kwargs.values())
340
+ assert "Ping" in all_args
341
+
342
+ def test_fastest_summary_shown_in_table(self):
343
+ """When at least one backend succeeds, fastest backend is shown."""
344
+ from skcapstone.cli.benchmark import BenchmarkRunner
345
+
346
+ fake_results = [
347
+ _ok("passthrough", ms=3.0),
348
+ _ok("ollama", ms=200.0),
349
+ ]
350
+ with patch.object(BenchmarkRunner, "run_all", return_value=fake_results):
351
+ result = self._invoke()
352
+
353
+ assert result.exit_code == 0
354
+ assert "Fastest" in result.output
355
+ assert "passthrough" in result.output
356
+
357
+ def test_no_backends_available_message(self):
358
+ """When all backends are unavailable, a friendly message is shown."""
359
+ from skcapstone.cli.benchmark import BenchmarkRunner, BACKENDS
360
+
361
+ all_unavail = [_unavail(name) for name in BACKENDS]
362
+ with patch.object(BenchmarkRunner, "run_all", return_value=all_unavail):
363
+ result = self._invoke()
364
+
365
+ assert result.exit_code == 0
366
+ assert "No backends available" in result.output or "unavailable" in result.output