@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,522 @@
1
+ """Tests for AWS EC2 and GCP Compute cloud provider adapters.
2
+
3
+ All cloud API calls are mocked — no real infrastructure required.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from typing import Any, Dict
10
+ from unittest.mock import MagicMock, patch, PropertyMock
11
+
12
+ import pytest
13
+
14
+ from skcapstone.blueprints.schema import AgentRole, AgentSpec, ModelTier, ResourceSpec
15
+ from skcapstone.providers.cloud import (
16
+ AWSAdapter,
17
+ CloudProvider,
18
+ GCPAdapter,
19
+ HetznerAdapter,
20
+ _CLOUD_ADAPTERS,
21
+ _build_cloud_init,
22
+ _memory_to_ec2_instance_type,
23
+ _memory_to_gcp_machine_type,
24
+ _memory_to_hetzner_type,
25
+ )
26
+ from skcapstone.team_engine import AgentStatus
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Helpers
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ def _make_spec(
35
+ role: str = "worker",
36
+ model: str = "fast",
37
+ memory: str = "4g",
38
+ cores: int = 2,
39
+ skills: list | None = None,
40
+ ) -> AgentSpec:
41
+ """Build a minimal AgentSpec for testing."""
42
+ return AgentSpec(
43
+ role=AgentRole(role),
44
+ model=ModelTier(model),
45
+ resources=ResourceSpec(memory=memory, cores=cores),
46
+ skills=skills or [],
47
+ env={},
48
+ )
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Instance type mapping
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ class TestMemoryToEC2InstanceType:
57
+ """Tests for _memory_to_ec2_instance_type."""
58
+
59
+ def test_micro_for_1g(self):
60
+ assert _memory_to_ec2_instance_type("1g", 1) == "t3.micro"
61
+
62
+ def test_small_for_2g(self):
63
+ assert _memory_to_ec2_instance_type("2g", 2) == "t3.small"
64
+
65
+ def test_medium_for_4g(self):
66
+ assert _memory_to_ec2_instance_type("4g", 2) == "t3.medium"
67
+
68
+ def test_large_for_8g(self):
69
+ assert _memory_to_ec2_instance_type("8g", 2) == "t3.large"
70
+
71
+ def test_xlarge_for_16g(self):
72
+ assert _memory_to_ec2_instance_type("16g", 4) == "t3.xlarge"
73
+
74
+ def test_2xlarge_for_large_memory(self):
75
+ assert _memory_to_ec2_instance_type("64g", 16) == "t3.2xlarge"
76
+
77
+ def test_megabytes_converted(self):
78
+ assert _memory_to_ec2_instance_type("512m", 1) == "t3.micro"
79
+
80
+
81
+ class TestMemoryToGCPMachineType:
82
+ """Tests for _memory_to_gcp_machine_type."""
83
+
84
+ def test_micro_for_1g(self):
85
+ assert _memory_to_gcp_machine_type("1g", 1) == "e2-micro"
86
+
87
+ def test_small_for_2g(self):
88
+ assert _memory_to_gcp_machine_type("2g", 1) == "e2-small"
89
+
90
+ def test_medium_for_4g(self):
91
+ assert _memory_to_gcp_machine_type("4g", 2) == "e2-medium"
92
+
93
+ def test_standard_2_for_8g(self):
94
+ assert _memory_to_gcp_machine_type("8g", 2) == "e2-standard-2"
95
+
96
+ def test_standard_4_for_16g(self):
97
+ assert _memory_to_gcp_machine_type("16g", 4) == "e2-standard-4"
98
+
99
+ def test_standard_8_for_large(self):
100
+ assert _memory_to_gcp_machine_type("64g", 16) == "e2-standard-8"
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # CloudProvider adapter dispatch
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ class TestCloudProviderDispatch:
109
+ """Tests for CloudProvider._get_adapter routing."""
110
+
111
+ def test_hetzner_returns_hetzner_adapter(self):
112
+ with patch.object(HetznerAdapter, "__init__", return_value=None):
113
+ provider = CloudProvider(cloud="hetzner")
114
+ assert isinstance(provider._adapter, HetznerAdapter)
115
+
116
+ def test_aws_returns_aws_adapter(self):
117
+ provider = CloudProvider(cloud="aws")
118
+ assert isinstance(provider._adapter, AWSAdapter)
119
+
120
+ def test_gcp_returns_gcp_adapter(self):
121
+ provider = CloudProvider(cloud="gcp")
122
+ assert isinstance(provider._adapter, GCPAdapter)
123
+
124
+ def test_unknown_cloud_raises(self):
125
+ with pytest.raises(RuntimeError, match="Unknown cloud"):
126
+ CloudProvider(cloud="digitalocean")
127
+
128
+ def test_registered_adapters_include_aws_and_gcp(self):
129
+ assert "aws" in _CLOUD_ADAPTERS
130
+ assert "gcp" in _CLOUD_ADAPTERS
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # AWS EC2 Adapter
135
+ # ---------------------------------------------------------------------------
136
+
137
+
138
+ class TestAWSAdapterProvision:
139
+ """Tests for AWSAdapter.provision()."""
140
+
141
+ @pytest.fixture()
142
+ def adapter(self):
143
+ return AWSAdapter(region="us-east-1", ami_id="ami-test123")
144
+
145
+ @pytest.fixture()
146
+ def mock_ec2(self):
147
+ mock = MagicMock()
148
+ mock.run_instances.return_value = {
149
+ "Instances": [{
150
+ "InstanceId": "i-abc123",
151
+ "PublicIpAddress": "1.2.3.4",
152
+ }]
153
+ }
154
+ return mock
155
+
156
+ def test_provision_returns_instance_id(self, adapter, mock_ec2):
157
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
158
+ result = adapter.provision("agent-1", _make_spec(), "team-a")
159
+ assert result["instance_id"] == "i-abc123"
160
+ assert result["host"] == "1.2.3.4"
161
+
162
+ def test_provision_tags_instance(self, adapter, mock_ec2):
163
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
164
+ adapter.provision("agent-1", _make_spec(role="coder"), "team-a")
165
+ call_kwargs = mock_ec2.run_instances.call_args[1]
166
+ tags = call_kwargs["TagSpecifications"][0]["Tags"]
167
+ tag_dict = {t["Key"]: t["Value"] for t in tags}
168
+ assert tag_dict["Name"] == "agent-1"
169
+ assert tag_dict["Role"] == "coder"
170
+ assert tag_dict["ManagedBy"] == "skcapstone"
171
+
172
+ def test_provision_uses_cloud_init(self, adapter, mock_ec2):
173
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
174
+ adapter.provision("agent-1", _make_spec(), "team-a")
175
+ call_kwargs = mock_ec2.run_instances.call_args[1]
176
+ assert "skcapstone" in call_kwargs["UserData"]
177
+
178
+ def test_provision_uses_correct_instance_type(self, adapter, mock_ec2):
179
+ spec = _make_spec(memory="8g", cores=2)
180
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
181
+ adapter.provision("agent-1", spec, "team-a")
182
+ call_kwargs = mock_ec2.run_instances.call_args[1]
183
+ assert call_kwargs["InstanceType"] == "t3.large"
184
+
185
+ def test_provision_waits_for_ip_if_missing(self, adapter):
186
+ mock_ec2 = MagicMock()
187
+ mock_ec2.run_instances.return_value = {
188
+ "Instances": [{"InstanceId": "i-noip"}]
189
+ }
190
+ mock_waiter = MagicMock()
191
+ mock_ec2.get_waiter.return_value = mock_waiter
192
+ mock_ec2.describe_instances.return_value = {
193
+ "Reservations": [{
194
+ "Instances": [{"PublicIpAddress": "5.6.7.8"}]
195
+ }]
196
+ }
197
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
198
+ result = adapter.provision("agent-wait", _make_spec(), "team-a")
199
+ assert result["host"] == "5.6.7.8"
200
+ mock_waiter.wait.assert_called_once()
201
+
202
+
203
+ class TestAWSAdapterLifecycle:
204
+ """Tests for AWSAdapter start/stop/destroy/health_check."""
205
+
206
+ @pytest.fixture()
207
+ def adapter(self):
208
+ return AWSAdapter(region="us-east-1")
209
+
210
+ @pytest.fixture()
211
+ def provision_result(self):
212
+ return {"instance_id": "i-test123", "host": "1.2.3.4", "region": "us-east-1"}
213
+
214
+ def test_configure_returns_true(self, adapter):
215
+ assert adapter.configure("a", _make_spec(), {}) is True
216
+
217
+ def test_start_calls_start_instances(self, adapter, provision_result):
218
+ mock_ec2 = MagicMock()
219
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
220
+ result = adapter.start("agent-1", provision_result)
221
+ assert result is True
222
+ mock_ec2.start_instances.assert_called_once_with(InstanceIds=["i-test123"])
223
+
224
+ def test_start_returns_false_without_instance_id(self, adapter):
225
+ assert adapter.start("a", {}) is False
226
+
227
+ def test_stop_calls_stop_instances(self, adapter, provision_result):
228
+ mock_ec2 = MagicMock()
229
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
230
+ result = adapter.stop("agent-1", provision_result)
231
+ assert result is True
232
+ mock_ec2.stop_instances.assert_called_once_with(InstanceIds=["i-test123"])
233
+
234
+ def test_stop_returns_false_without_instance_id(self, adapter):
235
+ assert adapter.stop("a", {}) is False
236
+
237
+ def test_destroy_calls_terminate_instances(self, adapter, provision_result):
238
+ mock_ec2 = MagicMock()
239
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
240
+ result = adapter.destroy("agent-1", provision_result)
241
+ assert result is True
242
+ mock_ec2.terminate_instances.assert_called_once_with(InstanceIds=["i-test123"])
243
+
244
+ def test_destroy_returns_false_without_instance_id(self, adapter):
245
+ assert adapter.destroy("a", {}) is False
246
+
247
+ def test_destroy_returns_false_on_error(self, adapter, provision_result):
248
+ mock_ec2 = MagicMock()
249
+ mock_ec2.terminate_instances.side_effect = Exception("access denied")
250
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
251
+ result = adapter.destroy("agent-1", provision_result)
252
+ assert result is False
253
+
254
+ def test_health_running(self, adapter, provision_result):
255
+ mock_ec2 = MagicMock()
256
+ mock_ec2.describe_instances.return_value = {
257
+ "Reservations": [{"Instances": [{"State": {"Name": "running"}}]}]
258
+ }
259
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
260
+ status = adapter.health_check("agent-1", provision_result)
261
+ assert status == AgentStatus.RUNNING
262
+
263
+ def test_health_stopped(self, adapter, provision_result):
264
+ mock_ec2 = MagicMock()
265
+ mock_ec2.describe_instances.return_value = {
266
+ "Reservations": [{"Instances": [{"State": {"Name": "stopped"}}]}]
267
+ }
268
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
269
+ status = adapter.health_check("agent-1", provision_result)
270
+ assert status == AgentStatus.STOPPED
271
+
272
+ def test_health_terminated(self, adapter, provision_result):
273
+ mock_ec2 = MagicMock()
274
+ mock_ec2.describe_instances.return_value = {
275
+ "Reservations": [{"Instances": [{"State": {"Name": "terminated"}}]}]
276
+ }
277
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
278
+ status = adapter.health_check("agent-1", provision_result)
279
+ assert status == AgentStatus.STOPPED
280
+
281
+ def test_health_pending_is_degraded(self, adapter, provision_result):
282
+ mock_ec2 = MagicMock()
283
+ mock_ec2.describe_instances.return_value = {
284
+ "Reservations": [{"Instances": [{"State": {"Name": "pending"}}]}]
285
+ }
286
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
287
+ status = adapter.health_check("agent-1", provision_result)
288
+ assert status == AgentStatus.DEGRADED
289
+
290
+ def test_health_no_reservations_returns_stopped(self, adapter, provision_result):
291
+ mock_ec2 = MagicMock()
292
+ mock_ec2.describe_instances.return_value = {"Reservations": []}
293
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
294
+ status = adapter.health_check("agent-1", provision_result)
295
+ assert status == AgentStatus.STOPPED
296
+
297
+ def test_health_api_error_returns_failed(self, adapter, provision_result):
298
+ mock_ec2 = MagicMock()
299
+ mock_ec2.describe_instances.side_effect = Exception("timeout")
300
+ with patch.object(adapter, "_ec2_client", return_value=mock_ec2):
301
+ status = adapter.health_check("agent-1", provision_result)
302
+ assert status == AgentStatus.FAILED
303
+
304
+ def test_health_no_instance_id_returns_stopped(self, adapter):
305
+ assert adapter.health_check("a", {}) == AgentStatus.STOPPED
306
+
307
+
308
+ class TestAWSAdapterAMI:
309
+ """Tests for AWS AMI resolution."""
310
+
311
+ def test_explicit_ami_used(self):
312
+ adapter = AWSAdapter(ami_id="ami-custom")
313
+ assert adapter._resolve_ami() == "ami-custom"
314
+
315
+ def test_default_ami_for_known_region(self):
316
+ adapter = AWSAdapter(region="us-east-1")
317
+ ami = adapter._resolve_ami()
318
+ assert ami.startswith("ami-")
319
+
320
+ def test_unknown_region_raises(self):
321
+ adapter = AWSAdapter(region="ap-southeast-99")
322
+ with pytest.raises(RuntimeError, match="No default AMI"):
323
+ adapter._resolve_ami()
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # GCP Compute Adapter
328
+ # ---------------------------------------------------------------------------
329
+
330
+
331
+ class TestGCPAdapterProvision:
332
+ """Tests for GCPAdapter.provision()."""
333
+
334
+ @pytest.fixture()
335
+ def adapter(self):
336
+ return GCPAdapter(project="test-project", zone="us-central1-a")
337
+
338
+ @pytest.fixture()
339
+ def _mock_compute_v1(self):
340
+ """Mock the google.cloud.compute_v1 module so provision() can import it."""
341
+ mock_mod = MagicMock()
342
+ with patch.dict("sys.modules", {"google": MagicMock(), "google.cloud": MagicMock(), "google.cloud.compute_v1": mock_mod}):
343
+ yield mock_mod
344
+
345
+ def test_provision_calls_insert(self, adapter, _mock_compute_v1):
346
+ mock_client = MagicMock()
347
+ mock_op = MagicMock()
348
+ mock_client.insert.return_value = mock_op
349
+
350
+ # Mock get for fetching IP
351
+ mock_inst = MagicMock()
352
+ mock_iface = MagicMock()
353
+ mock_ac = MagicMock()
354
+ mock_ac.nat_i_p = "10.0.0.1"
355
+ mock_iface.access_configs = [mock_ac]
356
+ mock_inst.network_interfaces = [mock_iface]
357
+ mock_client.get.return_value = mock_inst
358
+
359
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
360
+ with patch.object(adapter, "_get_source_image", return_value="projects/debian-cloud/global/images/debian-12"):
361
+ result = adapter.provision("agent-gcp", _make_spec(), "team-g")
362
+
363
+ assert result["instance_name"] == "agent-gcp"
364
+ assert result["host"] == "10.0.0.1"
365
+ assert result["project"] == "test-project"
366
+ mock_client.insert.assert_called_once()
367
+
368
+ def test_provision_no_project_raises(self, _mock_compute_v1):
369
+ adapter = GCPAdapter(project="")
370
+ adapter._project = ""
371
+ with pytest.raises(RuntimeError, match="project not configured"):
372
+ adapter.provision("agent", _make_spec(), "team")
373
+
374
+ def test_provision_normalizes_instance_name(self, adapter, _mock_compute_v1):
375
+ mock_client = MagicMock()
376
+ mock_op = MagicMock()
377
+ mock_client.insert.return_value = mock_op
378
+ mock_client.get.side_effect = Exception("skip")
379
+
380
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
381
+ with patch.object(adapter, "_get_source_image", return_value="image"):
382
+ result = adapter.provision("Agent_Name_1", _make_spec(), "team")
383
+
384
+ assert result["instance_name"] == "agent-name-1"
385
+
386
+
387
+ class TestGCPAdapterLifecycle:
388
+ """Tests for GCPAdapter start/stop/destroy/health_check."""
389
+
390
+ @pytest.fixture()
391
+ def adapter(self):
392
+ return GCPAdapter(project="test-project", zone="us-central1-a")
393
+
394
+ @pytest.fixture()
395
+ def provision_result(self):
396
+ return {
397
+ "instance_name": "agent-gcp",
398
+ "host": "10.0.0.1",
399
+ "zone": "us-central1-a",
400
+ "project": "test-project",
401
+ }
402
+
403
+ def test_configure_returns_true(self, adapter):
404
+ assert adapter.configure("a", _make_spec(), {}) is True
405
+
406
+ def test_start_calls_client_start(self, adapter, provision_result):
407
+ mock_client = MagicMock()
408
+ mock_op = MagicMock()
409
+ mock_client.start.return_value = mock_op
410
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
411
+ result = adapter.start("agent-gcp", provision_result)
412
+ assert result is True
413
+ mock_client.start.assert_called_once_with(
414
+ project="test-project",
415
+ zone="us-central1-a",
416
+ instance="agent-gcp",
417
+ )
418
+
419
+ def test_start_returns_false_without_instance_name(self, adapter):
420
+ assert adapter.start("a", {}) is False
421
+
422
+ def test_stop_calls_client_stop(self, adapter, provision_result):
423
+ mock_client = MagicMock()
424
+ mock_op = MagicMock()
425
+ mock_client.stop.return_value = mock_op
426
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
427
+ result = adapter.stop("agent-gcp", provision_result)
428
+ assert result is True
429
+ mock_client.stop.assert_called_once()
430
+
431
+ def test_stop_returns_false_without_instance_name(self, adapter):
432
+ assert adapter.stop("a", {}) is False
433
+
434
+ def test_destroy_calls_client_delete(self, adapter, provision_result):
435
+ mock_client = MagicMock()
436
+ mock_op = MagicMock()
437
+ mock_client.delete.return_value = mock_op
438
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
439
+ result = adapter.destroy("agent-gcp", provision_result)
440
+ assert result is True
441
+ mock_client.delete.assert_called_once()
442
+
443
+ def test_destroy_returns_false_without_instance_name(self, adapter):
444
+ assert adapter.destroy("a", {}) is False
445
+
446
+ def test_destroy_returns_false_on_error(self, adapter, provision_result):
447
+ mock_client = MagicMock()
448
+ mock_client.delete.side_effect = Exception("permission denied")
449
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
450
+ result = adapter.destroy("agent-gcp", provision_result)
451
+ assert result is False
452
+
453
+ def test_health_running(self, adapter, provision_result):
454
+ mock_client = MagicMock()
455
+ mock_inst = MagicMock()
456
+ mock_inst.status = "RUNNING"
457
+ mock_client.get.return_value = mock_inst
458
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
459
+ status = adapter.health_check("agent-gcp", provision_result)
460
+ assert status == AgentStatus.RUNNING
461
+
462
+ def test_health_terminated(self, adapter, provision_result):
463
+ mock_client = MagicMock()
464
+ mock_inst = MagicMock()
465
+ mock_inst.status = "TERMINATED"
466
+ mock_client.get.return_value = mock_inst
467
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
468
+ status = adapter.health_check("agent-gcp", provision_result)
469
+ assert status == AgentStatus.STOPPED
470
+
471
+ def test_health_staging_is_degraded(self, adapter, provision_result):
472
+ mock_client = MagicMock()
473
+ mock_inst = MagicMock()
474
+ mock_inst.status = "STAGING"
475
+ mock_client.get.return_value = mock_inst
476
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
477
+ status = adapter.health_check("agent-gcp", provision_result)
478
+ assert status == AgentStatus.DEGRADED
479
+
480
+ def test_health_api_error_returns_failed(self, adapter, provision_result):
481
+ mock_client = MagicMock()
482
+ mock_client.get.side_effect = Exception("timeout")
483
+ with patch.object(adapter, "_compute_client", return_value=mock_client):
484
+ status = adapter.health_check("agent-gcp", provision_result)
485
+ assert status == AgentStatus.FAILED
486
+
487
+ def test_health_no_instance_name_returns_stopped(self, adapter):
488
+ assert adapter.health_check("a", {}) == AgentStatus.STOPPED
489
+
490
+
491
+ # ---------------------------------------------------------------------------
492
+ # Cloud-init template
493
+ # ---------------------------------------------------------------------------
494
+
495
+
496
+ class TestBuildCloudInit:
497
+ """Tests for _build_cloud_init shared template."""
498
+
499
+ def test_contains_agent_name(self):
500
+ spec = _make_spec()
501
+ ci = _build_cloud_init("my-agent", spec)
502
+ assert "my-agent" in ci
503
+
504
+ def test_contains_skcapstone_install(self):
505
+ spec = _make_spec()
506
+ ci = _build_cloud_init("agent", spec)
507
+ assert "pip3 install skcapstone" in ci
508
+
509
+ def test_contains_tailscale(self):
510
+ spec = _make_spec()
511
+ ci = _build_cloud_init("agent", spec)
512
+ assert "tailscale" in ci
513
+
514
+ def test_contains_role(self):
515
+ spec = _make_spec(role="coder")
516
+ ci = _build_cloud_init("agent", spec)
517
+ assert "coder" in ci
518
+
519
+ def test_is_cloud_config(self):
520
+ spec = _make_spec()
521
+ ci = _build_cloud_init("agent", spec)
522
+ assert ci.startswith("#cloud-config")
@@ -0,0 +1,158 @@
1
+ """Tests for shell tab completion module.
2
+
3
+ Covers:
4
+ - Script generation for bash, zsh, fish
5
+ - Shell detection from environment
6
+ - Install to correct paths
7
+ - Uninstall removes files
8
+ - Invalid shell raises ValueError
9
+ - CLI commands (install, show, uninstall)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from pathlib import Path
16
+ from unittest.mock import patch
17
+
18
+ import pytest
19
+ from click.testing import CliRunner
20
+
21
+ from skcapstone.completions import (
22
+ SUPPORTED_SHELLS,
23
+ detect_shell,
24
+ generate_script,
25
+ install_completions,
26
+ uninstall_completions,
27
+ )
28
+
29
+
30
+ class TestGenerateScript:
31
+ """Test completion script generation."""
32
+
33
+ def test_bash_script(self):
34
+ """Bash script contains the correct env var."""
35
+ script = generate_script("bash")
36
+ assert "_SKCAPSTONE_COMPLETE=bash_source" in script
37
+ assert "skcapstone" in script
38
+
39
+ def test_zsh_script(self):
40
+ """Zsh script contains the correct env var."""
41
+ script = generate_script("zsh")
42
+ assert "_SKCAPSTONE_COMPLETE=zsh_source" in script
43
+
44
+ def test_fish_script(self):
45
+ """Fish script contains the correct env var."""
46
+ script = generate_script("fish")
47
+ assert "_SKCAPSTONE_COMPLETE=fish_source" in script
48
+
49
+ def test_unsupported_shell(self):
50
+ """Unsupported shell raises ValueError."""
51
+ with pytest.raises(ValueError, match="Unsupported"):
52
+ generate_script("powershell")
53
+
54
+ def test_all_shells_generate(self):
55
+ """Every supported shell produces a non-empty script."""
56
+ for shell in SUPPORTED_SHELLS:
57
+ script = generate_script(shell)
58
+ assert len(script) > 10
59
+
60
+
61
+ class TestDetectShell:
62
+ """Test shell auto-detection."""
63
+
64
+ def test_detect_bash(self):
65
+ """Detects bash from SHELL env."""
66
+ with patch.dict(os.environ, {"SHELL": "/bin/bash"}):
67
+ assert detect_shell() == "bash"
68
+
69
+ def test_detect_zsh(self):
70
+ """Detects zsh from SHELL env."""
71
+ with patch.dict(os.environ, {"SHELL": "/usr/bin/zsh"}):
72
+ assert detect_shell() == "zsh"
73
+
74
+ def test_detect_fish(self):
75
+ """Detects fish from SHELL env."""
76
+ with patch.dict(os.environ, {"SHELL": "/usr/bin/fish"}):
77
+ assert detect_shell() == "fish"
78
+
79
+ def test_unknown_shell(self):
80
+ """Returns None for unknown shells."""
81
+ with patch.dict(os.environ, {"SHELL": "/bin/csh"}):
82
+ assert detect_shell() is None
83
+
84
+
85
+ class TestInstall:
86
+ """Test completion installation."""
87
+
88
+ def test_install_bash(self, tmp_path):
89
+ """Install creates bash completion file."""
90
+ with patch("skcapstone.completions.INSTALL_PATHS",
91
+ {"bash": tmp_path / "skcapstone.bash-completion"}), \
92
+ patch("skcapstone.completions.RC_MARKERS", {}):
93
+ result = install_completions(shell="bash")
94
+
95
+ assert result["success"]
96
+ assert result["shell"] == "bash"
97
+ assert (tmp_path / "skcapstone.bash-completion").exists()
98
+
99
+ def test_install_auto_detect_fails(self):
100
+ """Install fails gracefully when shell can't be detected."""
101
+ with patch.dict(os.environ, {"SHELL": "/bin/unknown"}):
102
+ result = install_completions(shell=None)
103
+ assert not result["success"]
104
+ assert "error" in result
105
+
106
+
107
+ class TestUninstall:
108
+ """Test completion removal."""
109
+
110
+ def test_uninstall_removes_file(self, tmp_path):
111
+ """Uninstall removes the completion script."""
112
+ script = tmp_path / "skcapstone.bash-completion"
113
+ script.write_text("# completion")
114
+
115
+ with patch("skcapstone.completions.INSTALL_PATHS",
116
+ {"bash": script}):
117
+ result = uninstall_completions(shell="bash")
118
+
119
+ assert not script.exists()
120
+ assert len(result["removed"]) == 1
121
+
122
+ def test_uninstall_no_files(self, tmp_path):
123
+ """Uninstall on empty is a no-op."""
124
+ with patch("skcapstone.completions.INSTALL_PATHS",
125
+ {"bash": tmp_path / "nonexistent"}):
126
+ result = uninstall_completions(shell="bash")
127
+
128
+ assert result["removed"] == []
129
+
130
+
131
+ class TestCLI:
132
+ """Test CLI commands."""
133
+
134
+ def test_completions_help(self):
135
+ """completions --help works."""
136
+ from skcapstone.cli import main
137
+ runner = CliRunner()
138
+ result = runner.invoke(main, ["completions", "--help"])
139
+ assert result.exit_code == 0
140
+ assert "install" in result.output
141
+ assert "show" in result.output
142
+ assert "uninstall" in result.output
143
+
144
+ def test_show_bash(self):
145
+ """completions show --shell bash prints script."""
146
+ from skcapstone.cli import main
147
+ runner = CliRunner()
148
+ result = runner.invoke(main, ["completions", "show", "--shell", "bash"])
149
+ assert result.exit_code == 0
150
+ assert "_SKCAPSTONE_COMPLETE" in result.output
151
+
152
+ def test_show_zsh(self):
153
+ """completions show --shell zsh prints script."""
154
+ from skcapstone.cli import main
155
+ runner = CliRunner()
156
+ result = runner.invoke(main, ["completions", "show", "--shell", "zsh"])
157
+ assert result.exit_code == 0
158
+ assert "zsh_source" in result.output