@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,333 @@
1
+ """Tests for LLM token usage tracking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import threading
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from skcapstone.usage import (
12
+ DailyUsageReport,
13
+ ModelUsageSummary,
14
+ UsageTracker,
15
+ _cost_per_million,
16
+ _today_str,
17
+ )
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Fixtures
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ @pytest.fixture
26
+ def home(tmp_path: Path) -> Path:
27
+ """Minimal agent home directory."""
28
+ return tmp_path
29
+
30
+
31
+ @pytest.fixture
32
+ def tracker(home: Path) -> UsageTracker:
33
+ """UsageTracker backed by a temp directory."""
34
+ return UsageTracker(home)
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Cost table
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ class TestCostTable:
43
+ """Unit tests for _cost_per_million()."""
44
+
45
+ def test_claude_sonnet_priced(self) -> None:
46
+ """Claude Sonnet returns non-zero pricing."""
47
+ inp, out = _cost_per_million("claude-sonnet-4-6")
48
+ assert inp > 0
49
+ assert out > inp # output always more expensive
50
+
51
+ def test_ollama_free(self) -> None:
52
+ """Ollama / local models have zero cost."""
53
+ inp, out = _cost_per_million("ollama:llama3.1")
54
+ assert inp == 0.0
55
+ assert out == 0.0
56
+
57
+ def test_passthrough_free(self) -> None:
58
+ """Passthrough backend is always free."""
59
+ inp, out = _cost_per_million("passthrough")
60
+ assert inp == 0.0
61
+ assert out == 0.0
62
+
63
+ def test_unknown_model_has_nonzero_fallback(self) -> None:
64
+ """Unknown models get a conservative non-zero price."""
65
+ inp, out = _cost_per_million("some-unknown-model-xyz")
66
+ assert inp > 0
67
+ assert out > 0
68
+
69
+ def test_gpt4o_priced(self) -> None:
70
+ """GPT-4o returns positive pricing."""
71
+ inp, out = _cost_per_million("gpt-4o")
72
+ assert inp > 0
73
+ assert out > inp
74
+
75
+ def test_claude_opus_more_expensive_than_haiku(self) -> None:
76
+ """Opus costs more per token than Haiku."""
77
+ opus_inp, opus_out = _cost_per_million("claude-opus-4-6")
78
+ haiku_inp, haiku_out = _cost_per_million("claude-haiku-4-5")
79
+ assert opus_inp > haiku_inp
80
+ assert opus_out > haiku_out
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # UsageTracker.record_usage
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ class TestRecordUsage:
89
+ """Tests for the write path."""
90
+
91
+ def test_creates_usage_file(self, tracker: UsageTracker, home: Path) -> None:
92
+ """record_usage creates tokens-{date}.json."""
93
+ date_str = "2026-03-02"
94
+ tracker.record_usage("ollama:llama3.1", 100, 50, date_str=date_str)
95
+ path = home / "usage" / f"tokens-{date_str}.json"
96
+ assert path.exists()
97
+
98
+ def test_accumulates_calls(self, tracker: UsageTracker) -> None:
99
+ """Multiple record_usage calls accumulate counters."""
100
+ date_str = "2026-03-02"
101
+ for _ in range(5):
102
+ tracker.record_usage("ollama:llama3.1", 100, 50, date_str=date_str)
103
+ report = tracker.get_daily(date_str)
104
+ summary = report.models["ollama:llama3.1"]
105
+ assert summary.calls == 5
106
+ assert summary.input_tokens == 500
107
+ assert summary.output_tokens == 250
108
+
109
+ def test_multiple_models_tracked_separately(self, tracker: UsageTracker) -> None:
110
+ """Different models accumulate independently."""
111
+ date_str = "2026-03-02"
112
+ tracker.record_usage("ollama:llama3.1", 100, 50, date_str=date_str)
113
+ tracker.record_usage("claude-sonnet-4-6", 200, 80, date_str=date_str)
114
+ report = tracker.get_daily(date_str)
115
+ assert "ollama:llama3.1" in report.models
116
+ assert "claude-sonnet-4-6" in report.models
117
+ assert report.models["ollama:llama3.1"].calls == 1
118
+ assert report.models["claude-sonnet-4-6"].calls == 1
119
+
120
+ def test_cost_zero_for_local_model(self, tracker: UsageTracker) -> None:
121
+ """Local ollama models accumulate zero estimated cost."""
122
+ date_str = "2026-03-02"
123
+ tracker.record_usage("ollama:llama3.1", 10_000, 5_000, date_str=date_str)
124
+ report = tracker.get_daily(date_str)
125
+ assert report.models["ollama:llama3.1"].estimated_cost_usd == 0.0
126
+
127
+ def test_cost_nonzero_for_paid_model(self, tracker: UsageTracker) -> None:
128
+ """Paid models accumulate a positive estimated cost."""
129
+ date_str = "2026-03-02"
130
+ tracker.record_usage("claude-sonnet-4-6", 1_000_000, 100_000, date_str=date_str)
131
+ report = tracker.get_daily(date_str)
132
+ assert report.models["claude-sonnet-4-6"].estimated_cost_usd > 0
133
+
134
+ def test_json_file_readable(self, tracker: UsageTracker, home: Path) -> None:
135
+ """Persisted file is valid JSON."""
136
+ date_str = "2026-03-02"
137
+ tracker.record_usage("ollama:llama3.1", 100, 50, date_str=date_str)
138
+ path = home / "usage" / f"tokens-{date_str}.json"
139
+ data = json.loads(path.read_text(encoding="utf-8"))
140
+ assert "models" in data
141
+ assert "ollama:llama3.1" in data["models"]
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # UsageTracker.get_daily
146
+ # ---------------------------------------------------------------------------
147
+
148
+
149
+ class TestGetDaily:
150
+ """Tests for the daily read path."""
151
+
152
+ def test_empty_day_returns_report(self, tracker: UsageTracker) -> None:
153
+ """get_daily for a day with no data returns an empty report."""
154
+ report = tracker.get_daily("2099-01-01")
155
+ assert isinstance(report, DailyUsageReport)
156
+ assert report.date == "2099-01-01"
157
+ assert report.models == {}
158
+
159
+ def test_total_tokens_property(self, tracker: UsageTracker) -> None:
160
+ """total_tokens sums input + output across all models."""
161
+ date_str = "2026-03-02"
162
+ tracker.record_usage("ollama:llama3.1", 100, 40, date_str=date_str)
163
+ tracker.record_usage("claude-sonnet-4-6", 200, 60, date_str=date_str)
164
+ report = tracker.get_daily(date_str)
165
+ assert report.total_input_tokens == 300
166
+ assert report.total_output_tokens == 100
167
+ assert report.total_tokens == 400
168
+
169
+ def test_corrupt_file_returns_empty(self, home: Path) -> None:
170
+ """A corrupt JSON file returns an empty report instead of crashing."""
171
+ date_str = "2026-03-02"
172
+ usage_dir = home / "usage"
173
+ usage_dir.mkdir(parents=True)
174
+ (usage_dir / f"tokens-{date_str}.json").write_text("not json {{{", encoding="utf-8")
175
+ tracker = UsageTracker(home)
176
+ report = tracker.get_daily(date_str)
177
+ assert isinstance(report, DailyUsageReport)
178
+ assert report.models == {}
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # UsageTracker.get_weekly / get_monthly
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ class TestRangeQueries:
187
+ """Tests for weekly and monthly range queries."""
188
+
189
+ def test_weekly_returns_7_days(self, tracker: UsageTracker) -> None:
190
+ """get_weekly returns exactly 7 DailyUsageReport objects."""
191
+ reports = tracker.get_weekly()
192
+ assert len(reports) == 7
193
+
194
+ def test_monthly_returns_30_days(self, tracker: UsageTracker) -> None:
195
+ """get_monthly returns exactly 30 DailyUsageReport objects."""
196
+ reports = tracker.get_monthly()
197
+ assert len(reports) == 30
198
+
199
+ def test_weekly_includes_data(self, tracker: UsageTracker) -> None:
200
+ """get_weekly includes a day that has data recorded."""
201
+ from datetime import date, timedelta
202
+ today = date.today().strftime("%Y-%m-%d")
203
+ tracker.record_usage("ollama:llama3.1", 100, 50, date_str=today)
204
+ reports = tracker.get_weekly()
205
+ total = sum(r.total_calls for r in reports)
206
+ assert total == 1
207
+
208
+ def test_aggregate_sums_correctly(self, tracker: UsageTracker) -> None:
209
+ """aggregate() totals across multiple days."""
210
+ tracker.record_usage("ollama:llama3.1", 100, 50, date_str="2026-03-01")
211
+ tracker.record_usage("ollama:llama3.1", 200, 80, date_str="2026-03-02")
212
+ reports = [tracker.get_daily("2026-03-01"), tracker.get_daily("2026-03-02")]
213
+ agg = tracker.aggregate(reports)
214
+ m = agg.models["ollama:llama3.1"]
215
+ assert m.calls == 2
216
+ assert m.input_tokens == 300
217
+ assert m.output_tokens == 130
218
+
219
+ def test_aggregate_empty_list(self, tracker: UsageTracker) -> None:
220
+ """aggregate([]) returns a safe empty report."""
221
+ agg = tracker.aggregate([])
222
+ assert agg.date == "empty"
223
+ assert agg.models == {}
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Thread safety
228
+ # ---------------------------------------------------------------------------
229
+
230
+
231
+ class TestThreadSafety:
232
+ """Concurrent record_usage calls must not corrupt data."""
233
+
234
+ def test_concurrent_writes_same_model(self, tracker: UsageTracker) -> None:
235
+ """100 concurrent record_usage calls produce correct totals."""
236
+ date_str = "2026-03-02"
237
+ n = 100
238
+ barrier = threading.Barrier(n)
239
+
240
+ def _record():
241
+ barrier.wait()
242
+ tracker.record_usage("ollama:llama3.1", 10, 5, date_str=date_str)
243
+
244
+ threads = [threading.Thread(target=_record) for _ in range(n)]
245
+ for t in threads:
246
+ t.start()
247
+ for t in threads:
248
+ t.join()
249
+
250
+ report = tracker.get_daily(date_str)
251
+ m = report.models["ollama:llama3.1"]
252
+ assert m.calls == n
253
+ assert m.input_tokens == n * 10
254
+ assert m.output_tokens == n * 5
255
+
256
+ def test_concurrent_writes_different_models(self, tracker: UsageTracker) -> None:
257
+ """Concurrent writes to different models don't lose data."""
258
+ date_str = "2026-03-02"
259
+ n = 50
260
+ barrier = threading.Barrier(n * 2)
261
+
262
+ def _record_a():
263
+ barrier.wait()
264
+ tracker.record_usage("model-a", 10, 5, date_str=date_str)
265
+
266
+ def _record_b():
267
+ barrier.wait()
268
+ tracker.record_usage("model-b", 20, 10, date_str=date_str)
269
+
270
+ threads = (
271
+ [threading.Thread(target=_record_a) for _ in range(n)]
272
+ + [threading.Thread(target=_record_b) for _ in range(n)]
273
+ )
274
+ for t in threads:
275
+ t.start()
276
+ for t in threads:
277
+ t.join()
278
+
279
+ report = tracker.get_daily(date_str)
280
+ assert report.models["model-a"].calls == n
281
+ assert report.models["model-b"].calls == n
282
+
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # ModelUsageSummary
286
+ # ---------------------------------------------------------------------------
287
+
288
+
289
+ class TestModelUsageSummary:
290
+ """Unit tests for the ModelUsageSummary model."""
291
+
292
+ def test_total_tokens(self) -> None:
293
+ """total_tokens sums input and output."""
294
+ m = ModelUsageSummary(
295
+ model="test", calls=1, input_tokens=100, output_tokens=50
296
+ )
297
+ assert m.total_tokens == 150
298
+
299
+ def test_defaults(self) -> None:
300
+ """All counts default to zero."""
301
+ m = ModelUsageSummary(model="test")
302
+ assert m.calls == 0
303
+ assert m.input_tokens == 0
304
+ assert m.output_tokens == 0
305
+ assert m.estimated_cost_usd == 0.0
306
+ assert m.total_tokens == 0
307
+
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # DailyUsageReport
311
+ # ---------------------------------------------------------------------------
312
+
313
+
314
+ class TestDailyUsageReport:
315
+ """Unit tests for the DailyUsageReport model."""
316
+
317
+ def test_empty_report_totals(self) -> None:
318
+ """Empty report has all-zero aggregates."""
319
+ r = DailyUsageReport(date="2026-03-02")
320
+ assert r.total_calls == 0
321
+ assert r.total_tokens == 0
322
+ assert r.total_cost_usd == 0.0
323
+
324
+ def test_total_cost_aggregates(self) -> None:
325
+ """total_cost_usd sums across all models."""
326
+ r = DailyUsageReport(
327
+ date="2026-03-02",
328
+ models={
329
+ "m1": ModelUsageSummary(model="m1", estimated_cost_usd=0.10),
330
+ "m2": ModelUsageSummary(model="m2", estimated_cost_usd=0.25),
331
+ },
332
+ )
333
+ assert abs(r.total_cost_usd - 0.35) < 1e-9
@@ -0,0 +1,355 @@
1
+ """Tests for skcapstone version command and doctor --verbose.
2
+
3
+ Covers:
4
+ - gather_version_info() returns expected keys
5
+ - _check_optional_dep() returns version or None
6
+ - _probe_ollama() running / not-running paths
7
+ - _get_daemon_pid() running / not-running paths
8
+ - version CLI: normal output and --json-out
9
+ - doctor CLI: --verbose mode shows all checks
10
+ - doctor CLI: --verbose with --json-out includes all checks
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from pathlib import Path
17
+ from unittest.mock import MagicMock, patch
18
+
19
+ import pytest
20
+ from click.testing import CliRunner
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Shared fixture
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ @pytest.fixture
29
+ def agent_home(tmp_path: Path) -> Path:
30
+ """Fully initialised agent home with minimal required files."""
31
+ home = tmp_path / ".skcapstone"
32
+ for d in [
33
+ "identity", "memory", "trust", "security", "sync", "config",
34
+ "memory/short-term", "memory/mid-term", "memory/long-term",
35
+ ]:
36
+ (home / d).mkdir(parents=True, exist_ok=True)
37
+
38
+ (home / "manifest.json").write_text(json.dumps({
39
+ "name": "TestAgent", "version": "0.1.0",
40
+ }))
41
+ (home / "identity" / "identity.json").write_text(json.dumps({
42
+ "name": "TestAgent",
43
+ "fingerprint": "DEADBEEF12345678",
44
+ "capauth_managed": True,
45
+ }))
46
+ return home
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Unit tests for version_cmd helpers
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ class TestCheckOptionalDep:
55
+ """Tests for _check_optional_dep()."""
56
+
57
+ def test_installed_package_returns_version(self):
58
+ """Returns version string when package is importable."""
59
+ from skcapstone.cli.version_cmd import _check_optional_dep
60
+
61
+ # 'sys' is always importable; give it a fake __version__ to confirm
62
+ with patch("importlib.import_module") as mock_import:
63
+ mock_mod = MagicMock()
64
+ mock_mod.__version__ = "9.9.9"
65
+ mock_import.return_value = mock_mod
66
+ result = _check_optional_dep("fakepkg")
67
+
68
+ assert result == "9.9.9"
69
+
70
+ def test_missing_package_returns_none(self):
71
+ """Returns None when package raises ImportError."""
72
+ from skcapstone.cli.version_cmd import _check_optional_dep
73
+
74
+ with patch("importlib.import_module", side_effect=ImportError("no module")):
75
+ result = _check_optional_dep("nonexistent_pkg_xyz")
76
+
77
+ assert result is None
78
+
79
+ def test_package_without_version_attr_returns_installed(self):
80
+ """Returns 'installed' fallback when __version__ is absent."""
81
+ from skcapstone.cli.version_cmd import _check_optional_dep
82
+
83
+ with patch("importlib.import_module") as mock_import:
84
+ mock_mod = MagicMock(spec=[]) # no attributes
85
+ mock_import.return_value = mock_mod
86
+ result = _check_optional_dep("nover_pkg")
87
+
88
+ assert result == "installed"
89
+
90
+
91
+ class TestProbeOllama:
92
+ """Tests for _probe_ollama()."""
93
+
94
+ def test_running_returns_models(self):
95
+ """Running Ollama: running=True, models list populated."""
96
+ from skcapstone.cli.version_cmd import _probe_ollama
97
+
98
+ payload = json.dumps({
99
+ "models": [{"name": "llama3:latest"}, {"name": "phi3:mini"}]
100
+ }).encode()
101
+ mock_resp = MagicMock()
102
+ mock_resp.__enter__ = lambda s: s
103
+ mock_resp.__exit__ = MagicMock(return_value=False)
104
+ mock_resp.read.return_value = payload
105
+
106
+ with patch("urllib.request.urlopen", return_value=mock_resp):
107
+ result = _probe_ollama()
108
+
109
+ assert result["running"] is True
110
+ assert "llama3:latest" in result["models"]
111
+ assert "phi3:mini" in result["models"]
112
+
113
+ def test_not_running_on_connection_error(self):
114
+ """Connection refused: running=False, models=[]."""
115
+ from skcapstone.cli.version_cmd import _probe_ollama
116
+
117
+ with patch("urllib.request.urlopen", side_effect=OSError("connection refused")):
118
+ result = _probe_ollama()
119
+
120
+ assert result["running"] is False
121
+ assert result["models"] == []
122
+
123
+ def test_host_included_in_result(self, monkeypatch):
124
+ """Custom OLLAMA_HOST appears in the returned dict."""
125
+ from skcapstone.cli.version_cmd import _probe_ollama
126
+
127
+ monkeypatch.setenv("OLLAMA_HOST", "http://my-server:11434")
128
+ with patch("urllib.request.urlopen", side_effect=OSError):
129
+ result = _probe_ollama()
130
+
131
+ assert result["host"] == "http://my-server:11434"
132
+
133
+
134
+ class TestGetDaemonPid:
135
+ """Tests for _get_daemon_pid()."""
136
+
137
+ def test_returns_pid_when_running(self, agent_home: Path):
138
+ """Returns integer PID when daemon is alive."""
139
+ from skcapstone.cli.version_cmd import _get_daemon_pid
140
+
141
+ with patch("skcapstone.daemon.read_pid", return_value=99999):
142
+ result = _get_daemon_pid(agent_home)
143
+
144
+ assert result == 99999
145
+
146
+ def test_returns_none_when_stopped(self, agent_home: Path):
147
+ """Returns None when no PID file exists."""
148
+ from skcapstone.cli.version_cmd import _get_daemon_pid
149
+
150
+ with patch("skcapstone.daemon.read_pid", return_value=None):
151
+ result = _get_daemon_pid(agent_home)
152
+
153
+ assert result is None
154
+
155
+ def test_returns_none_on_exception(self, agent_home: Path):
156
+ """Swallows import or runtime errors, returns None."""
157
+ from skcapstone.cli.version_cmd import _get_daemon_pid
158
+
159
+ with patch("skcapstone.daemon.read_pid", side_effect=RuntimeError("oops")):
160
+ result = _get_daemon_pid(agent_home)
161
+
162
+ assert result is None
163
+
164
+
165
+ class TestGatherVersionInfo:
166
+ """Tests for gather_version_info()."""
167
+
168
+ def test_contains_all_expected_keys(self, agent_home: Path):
169
+ """Dict has all required top-level keys."""
170
+ from skcapstone.cli.version_cmd import gather_version_info
171
+
172
+ with patch("urllib.request.urlopen", side_effect=OSError), \
173
+ patch("skcapstone.daemon.read_pid", return_value=None):
174
+ info = gather_version_info(agent_home)
175
+
176
+ assert "package_version" in info
177
+ assert "python_version" in info
178
+ assert "platform" in info
179
+ assert "optional_deps" in info
180
+ assert "ollama" in info
181
+ assert "daemon_pid" in info
182
+
183
+ def test_optional_deps_has_four_packages(self, agent_home: Path):
184
+ """optional_deps covers watchdog, skcomm, skchat, skseed."""
185
+ from skcapstone.cli.version_cmd import gather_version_info
186
+
187
+ with patch("urllib.request.urlopen", side_effect=OSError), \
188
+ patch("skcapstone.daemon.read_pid", return_value=None):
189
+ info = gather_version_info(agent_home)
190
+
191
+ deps = info["optional_deps"]
192
+ assert set(deps.keys()) == {"watchdog", "skcomm", "skchat", "skseed"}
193
+
194
+ def test_package_version_matches_module(self, agent_home: Path):
195
+ """package_version matches skcapstone.__version__."""
196
+ from skcapstone import __version__
197
+ from skcapstone.cli.version_cmd import gather_version_info
198
+
199
+ with patch("urllib.request.urlopen", side_effect=OSError), \
200
+ patch("skcapstone.daemon.read_pid", return_value=None):
201
+ info = gather_version_info(agent_home)
202
+
203
+ assert info["package_version"] == __version__
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # CLI integration tests — version command
208
+ # ---------------------------------------------------------------------------
209
+
210
+
211
+ class TestVersionCommand:
212
+ """Integration tests for `skcapstone version`."""
213
+
214
+ def _run(self, args: list[str], agent_home: Path):
215
+ from skcapstone.cli import main
216
+
217
+ runner = CliRunner(mix_stderr=False)
218
+ with patch("urllib.request.urlopen", side_effect=OSError), \
219
+ patch("skcapstone.daemon.read_pid", return_value=None):
220
+ return runner.invoke(
221
+ main,
222
+ ["version", "--home", str(agent_home)] + args,
223
+ catch_exceptions=False,
224
+ )
225
+
226
+ def test_default_output_contains_version(self, agent_home: Path):
227
+ """Normal output includes the skcapstone package version."""
228
+ from skcapstone import __version__
229
+
230
+ result = self._run([], agent_home)
231
+ assert result.exit_code == 0, result.output
232
+ assert __version__ in result.output
233
+
234
+ def test_default_output_lists_optional_deps(self, agent_home: Path):
235
+ """Normal output lists all four optional dep names."""
236
+ result = self._run([], agent_home)
237
+ assert result.exit_code == 0
238
+ for pkg in ("watchdog", "skcomm", "skchat", "skseed"):
239
+ assert pkg in result.output
240
+
241
+ def test_json_output_is_valid_and_complete(self, agent_home: Path):
242
+ """--json-out emits valid JSON with all required keys."""
243
+ result = self._run(["--json-out"], agent_home)
244
+ assert result.exit_code == 0
245
+ data = json.loads(result.output)
246
+ assert "package_version" in data
247
+ assert "python_version" in data
248
+ assert "optional_deps" in data
249
+ assert "ollama" in data
250
+ assert "daemon_pid" in data
251
+
252
+ def test_daemon_running_shown_in_output(self, agent_home: Path):
253
+ """Shows running + PID when daemon is alive."""
254
+ from skcapstone.cli import main
255
+
256
+ runner = CliRunner(mix_stderr=False)
257
+ with patch("urllib.request.urlopen", side_effect=OSError), \
258
+ patch("skcapstone.daemon.read_pid", return_value=42001):
259
+ result = runner.invoke(
260
+ main,
261
+ ["version", "--home", str(agent_home)],
262
+ catch_exceptions=False,
263
+ )
264
+ assert result.exit_code == 0
265
+ assert "42001" in result.output
266
+
267
+ def test_ollama_running_shows_model_count(self, agent_home: Path):
268
+ """When Ollama is up, output includes model count."""
269
+ from skcapstone.cli import main
270
+
271
+ payload = json.dumps({
272
+ "models": [{"name": "llama3:latest"}, {"name": "mistral:7b"}]
273
+ }).encode()
274
+ mock_resp = MagicMock()
275
+ mock_resp.__enter__ = lambda s: s
276
+ mock_resp.__exit__ = MagicMock(return_value=False)
277
+ mock_resp.read.return_value = payload
278
+
279
+ runner = CliRunner(mix_stderr=False)
280
+ with patch("urllib.request.urlopen", return_value=mock_resp), \
281
+ patch("skcapstone.daemon.read_pid", return_value=None):
282
+ result = runner.invoke(
283
+ main,
284
+ ["version", "--home", str(agent_home)],
285
+ catch_exceptions=False,
286
+ )
287
+ assert result.exit_code == 0
288
+ assert "2 model" in result.output
289
+
290
+
291
+ # ---------------------------------------------------------------------------
292
+ # CLI integration tests — doctor --verbose
293
+ # ---------------------------------------------------------------------------
294
+
295
+
296
+ class TestDoctorVerbose:
297
+ """Integration tests for `skcapstone doctor --verbose`."""
298
+
299
+ def _run_doctor(self, args: list[str], agent_home: Path):
300
+ from skcapstone.cli import main
301
+
302
+ runner = CliRunner(mix_stderr=False)
303
+ return runner.invoke(
304
+ main,
305
+ ["doctor", "--home", str(agent_home)] + args,
306
+ catch_exceptions=False,
307
+ )
308
+
309
+ def test_verbose_shows_passing_checks(self, agent_home: Path):
310
+ """--verbose prints checks that passed, not just failures."""
311
+ result = self._run_doctor(["--verbose"], agent_home)
312
+ assert result.exit_code == 0
313
+ # At minimum the home:exists check should be present in output
314
+ assert "Agent home directory" in result.output
315
+
316
+ def test_verbose_output_includes_check_names(self, agent_home: Path):
317
+ """--verbose output contains internal check names in parentheses."""
318
+ result = self._run_doctor(["--verbose"], agent_home)
319
+ assert result.exit_code == 0
320
+ # Internal names like (home:exists) appear in verbose mode
321
+ assert "home:exists" in result.output
322
+
323
+ def test_verbose_shows_summary_line(self, agent_home: Path):
324
+ """--verbose ends with a 'Summary:' line containing pass/fail counts."""
325
+ result = self._run_doctor(["--verbose"], agent_home)
326
+ assert result.exit_code == 0
327
+ assert "Summary:" in result.output
328
+ assert "passed" in result.output
329
+
330
+ def test_non_verbose_collapses_all_pass_categories(self, agent_home: Path):
331
+ """Without --verbose, fully-passing categories are on one line."""
332
+ result = self._run_doctor([], agent_home)
333
+ assert result.exit_code == 0
334
+ # Agent Home directory exists, so it should be collapsed
335
+ assert "passed" in result.output
336
+
337
+ def test_verbose_json_includes_all_checks(self, agent_home: Path):
338
+ """--verbose --json-out still emits the full checks list."""
339
+ result = self._run_doctor(["--verbose", "--json-out"], agent_home)
340
+ assert result.exit_code == 0
341
+ data = json.loads(result.output)
342
+ assert "checks" in data
343
+ assert len(data["checks"]) > 0
344
+ # Every check has a name
345
+ for c in data["checks"]:
346
+ assert "name" in c
347
+
348
+ def test_verbose_help_text_mentions_verbose(self, agent_home: Path):
349
+ """--help output documents the --verbose flag."""
350
+ from skcapstone.cli import main
351
+
352
+ runner = CliRunner(mix_stderr=False)
353
+ result = runner.invoke(main, ["doctor", "--help"])
354
+ assert result.exit_code == 0
355
+ assert "--verbose" in result.output