@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,560 @@
1
+ """
2
+ Unit tests for skcapstone sync backends.
3
+
4
+ Covers SyncthingBackend, GitBackend, and LocalBackend with mocked
5
+ external dependencies (subprocess, shutil.which, OS errors).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ from unittest.mock import MagicMock, call, patch
14
+
15
+ import pytest
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Shared fixture
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ @pytest.fixture
24
+ def agent_home(tmp_path: Path) -> Path:
25
+ home = tmp_path / ".skcapstone"
26
+ home.mkdir()
27
+ for d in ("identity", "memory", "trust", "config", "skills"):
28
+ (home / d).mkdir()
29
+ (home / "manifest.json").write_text(
30
+ json.dumps({"name": "TestAgent", "version": "0.1.0", "connectors": []})
31
+ )
32
+ return home
33
+
34
+
35
+ @pytest.fixture
36
+ def vault_files(tmp_path: Path):
37
+ """Create a fake vault + manifest pair."""
38
+ vault = tmp_path / "vault-host-20260228T000000Z.tar.gz"
39
+ vault.write_bytes(b"fake tar data")
40
+ manifest = tmp_path / "vault-host-20260228T000000Z.tar.gz.manifest.json"
41
+ manifest.write_text(json.dumps({"agent_name": "TestAgent", "pillars_included": []}))
42
+ return vault, manifest
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # SyncthingBackend
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ class TestSyncthingBackend:
51
+ def _make_backend(self, agent_home):
52
+ from skcapstone.sync.backends import SyncthingBackend
53
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
54
+
55
+ config = SyncBackendConfig(backend_type=SyncBackendType.SYNCTHING)
56
+ return SyncthingBackend(config, agent_home)
57
+
58
+ def test_creates_directories(self, agent_home: Path):
59
+ """SyncthingBackend constructor creates outbox/inbox/archive dirs."""
60
+ backend = self._make_backend(agent_home)
61
+ assert backend.outbox.is_dir()
62
+ assert backend.inbox.is_dir()
63
+ assert backend.archive.is_dir()
64
+
65
+ def test_push_creates_state_file(self, agent_home: Path, vault_files):
66
+ """Push should update sync-state.json with last_push timestamp."""
67
+ vault, manifest = vault_files
68
+ backend = self._make_backend(agent_home)
69
+ result = backend.push(vault, manifest)
70
+
71
+ assert result is True
72
+ state_file = agent_home / "sync" / "sync-state.json"
73
+ assert state_file.exists()
74
+ state = json.loads(state_file.read_text())
75
+ assert "last_push" in state
76
+ assert "seed_count" in state
77
+
78
+ def test_push_updates_seed_count(self, agent_home: Path, tmp_path: Path):
79
+ """seed_count in state should count .tar.gz files in outbox."""
80
+ from skcapstone.sync.backends import SyncthingBackend
81
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
82
+
83
+ config = SyncBackendConfig(backend_type=SyncBackendType.SYNCTHING)
84
+ backend = SyncthingBackend(config, agent_home)
85
+
86
+ # Push two vaults
87
+ for i in range(2):
88
+ v = tmp_path / f"vault-test-{i}.tar.gz"
89
+ v.write_bytes(b"data")
90
+ m = tmp_path / f"vault-test-{i}.tar.gz.manifest.json"
91
+ m.write_text("{}")
92
+ backend.push(v, m)
93
+
94
+ state = json.loads((agent_home / "sync" / "sync-state.json").read_text())
95
+ # glob "*.tar.gz*" matches both .tar.gz and .tar.gz.manifest.json files
96
+ assert state["seed_count"] >= 2
97
+
98
+ def test_push_handles_oserror(self, agent_home: Path, tmp_path: Path):
99
+ """Push should return False on OSError (e.g., permissions)."""
100
+ from skcapstone.sync.backends import SyncthingBackend
101
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
102
+
103
+ config = SyncBackendConfig(backend_type=SyncBackendType.SYNCTHING)
104
+ backend = SyncthingBackend(config, agent_home)
105
+
106
+ nonexistent = tmp_path / "missing.tar.gz"
107
+ manifest = tmp_path / "missing.manifest.json"
108
+ # Don't create them — shutil.copy2 will raise OSError
109
+ result = backend.push(nonexistent, manifest)
110
+ assert result is False
111
+
112
+ def test_push_state_file_invalid_json_recovers(self, agent_home: Path, vault_files):
113
+ """Push should recover gracefully when existing state file is corrupt JSON."""
114
+ from skcapstone.sync.backends import SyncthingBackend
115
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
116
+
117
+ config = SyncBackendConfig(backend_type=SyncBackendType.SYNCTHING)
118
+ backend = SyncthingBackend(config, agent_home)
119
+
120
+ # Write invalid JSON to state file
121
+ state_file = agent_home / "sync" / "sync-state.json"
122
+ state_file.write_text("NOT JSON {{{")
123
+
124
+ vault, manifest = vault_files
125
+ result = backend.push(vault, manifest)
126
+ assert result is True
127
+ # State file should now be valid
128
+ state = json.loads(state_file.read_text())
129
+ assert "last_push" in state
130
+
131
+ def test_pull_returns_latest_vault(self, agent_home: Path, tmp_path: Path):
132
+ """Pull returns the most recently modified vault from inbox."""
133
+ import time
134
+
135
+ from skcapstone.sync.backends import SyncthingBackend
136
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
137
+
138
+ config = SyncBackendConfig(backend_type=SyncBackendType.SYNCTHING)
139
+ backend = SyncthingBackend(config, agent_home)
140
+
141
+ older = backend.inbox / "vault-peer-20260101T000000Z.tar.gz"
142
+ older.write_bytes(b"old")
143
+ time.sleep(0.05)
144
+ newer = backend.inbox / "vault-peer-20260228T000000Z.tar.gz"
145
+ newer.write_bytes(b"new")
146
+
147
+ result = backend.pull(tmp_path)
148
+ assert result is not None
149
+ assert result.read_bytes() == b"new"
150
+
151
+ def test_pull_moves_pulled_vault_to_archive(self, agent_home: Path, tmp_path: Path):
152
+ """After pull, the source file should be moved to archive."""
153
+ from skcapstone.sync.backends import SyncthingBackend
154
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
155
+
156
+ config = SyncBackendConfig(backend_type=SyncBackendType.SYNCTHING)
157
+ backend = SyncthingBackend(config, agent_home)
158
+
159
+ inbox_file = backend.inbox / "vault-peer-20260228T000000Z.tar.gz"
160
+ inbox_file.write_bytes(b"data")
161
+
162
+ backend.pull(tmp_path)
163
+ assert not inbox_file.exists()
164
+ assert (backend.archive / "vault-peer-20260228T000000Z.tar.gz").exists()
165
+
166
+ def test_pull_empty_inbox_returns_none(self, agent_home: Path, tmp_path: Path):
167
+ """Pull returns None when inbox has no vault files."""
168
+ backend = self._make_backend(agent_home)
169
+ # Add a non-vault file to ensure glob filters correctly
170
+ (backend.inbox / "README.txt").write_text("ignore me")
171
+ assert backend.pull(tmp_path) is None
172
+
173
+ def test_available_when_syncthing_in_path(self, agent_home: Path):
174
+ """available() returns True when syncthing binary is found."""
175
+ backend = self._make_backend(agent_home)
176
+ with patch("shutil.which", return_value="/usr/bin/syncthing"):
177
+ assert backend.available() is True
178
+
179
+ def test_available_when_syncthing_missing(self, agent_home: Path):
180
+ """available() returns False when syncthing binary is not found."""
181
+ backend = self._make_backend(agent_home)
182
+ with patch("shutil.which", return_value=None):
183
+ assert backend.available() is False
184
+
185
+ def test_name_property(self, agent_home: Path):
186
+ backend = self._make_backend(agent_home)
187
+ assert backend.name == "syncthing"
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # GitBackend
192
+ # ---------------------------------------------------------------------------
193
+
194
+
195
+ class TestGitBackend:
196
+ def _make_github_backend(self, agent_home: Path, repo_url: str = "https://github.com/org/repo"):
197
+ from skcapstone.sync.backends import GitBackend
198
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
199
+
200
+ config = SyncBackendConfig(
201
+ backend_type=SyncBackendType.GITHUB,
202
+ repo_url=repo_url,
203
+ branch="main",
204
+ )
205
+ return GitBackend(config, agent_home)
206
+
207
+ def _make_forgejo_backend(self, agent_home: Path):
208
+ from skcapstone.sync.backends import GitBackend
209
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
210
+
211
+ config = SyncBackendConfig(
212
+ backend_type=SyncBackendType.FORGEJO,
213
+ repo_url="https://forgejo.example/org/repo",
214
+ branch="skworld",
215
+ )
216
+ return GitBackend(config, agent_home)
217
+
218
+ def test_name_github(self, agent_home: Path):
219
+ backend = self._make_github_backend(agent_home)
220
+ assert backend.name == "github"
221
+
222
+ def test_name_forgejo(self, agent_home: Path):
223
+ backend = self._make_forgejo_backend(agent_home)
224
+ assert backend.name == "forgejo"
225
+
226
+ def test_available_requires_git_and_repo_url(self, agent_home: Path):
227
+ backend = self._make_github_backend(agent_home)
228
+ with patch("shutil.which", return_value="/usr/bin/git"):
229
+ assert backend.available() is True
230
+
231
+ def test_available_missing_git_binary(self, agent_home: Path):
232
+ backend = self._make_github_backend(agent_home)
233
+ with patch("shutil.which", return_value=None):
234
+ assert backend.available() is False
235
+
236
+ def test_available_no_repo_url(self, agent_home: Path):
237
+ from skcapstone.sync.backends import GitBackend
238
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
239
+
240
+ config = SyncBackendConfig(backend_type=SyncBackendType.GITHUB, repo_url=None)
241
+ backend = GitBackend(config, agent_home)
242
+ with patch("shutil.which", return_value="/usr/bin/git"):
243
+ assert backend.available() is False
244
+
245
+ def test_ensure_repo_returns_false_if_no_repo_url(self, agent_home: Path):
246
+ from skcapstone.sync.backends import GitBackend
247
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
248
+
249
+ config = SyncBackendConfig(backend_type=SyncBackendType.GITHUB, repo_url=None)
250
+ backend = GitBackend(config, agent_home)
251
+ assert backend._ensure_repo() is False
252
+
253
+ def test_ensure_repo_skips_clone_if_git_dir_exists(self, agent_home: Path):
254
+ """If .git dir already exists, _ensure_repo returns True without cloning."""
255
+ backend = self._make_github_backend(agent_home)
256
+ git_dir = backend._repo_dir / ".git"
257
+ git_dir.mkdir(parents=True)
258
+
259
+ with patch("subprocess.run") as mock_run:
260
+ result = backend._ensure_repo()
261
+ assert result is True
262
+ mock_run.assert_not_called()
263
+
264
+ def test_ensure_repo_clones_when_missing(self, agent_home: Path):
265
+ """_ensure_repo calls git clone when .git doesn't exist."""
266
+ backend = self._make_github_backend(agent_home)
267
+ mock_result = MagicMock(returncode=0)
268
+ with patch("subprocess.run", return_value=mock_result) as mock_run:
269
+ result = backend._ensure_repo()
270
+ assert result is True
271
+ args = mock_run.call_args[0][0]
272
+ assert "clone" in args
273
+
274
+ def test_ensure_repo_clone_failure_returns_false(self, agent_home: Path):
275
+ backend = self._make_github_backend(agent_home)
276
+ mock_result = MagicMock(returncode=1, stderr="fatal: repo not found")
277
+ with patch("subprocess.run", return_value=mock_result):
278
+ result = backend._ensure_repo()
279
+ assert result is False
280
+
281
+ def test_ensure_repo_uses_token_env_var(self, agent_home: Path):
282
+ """_ensure_repo should set GIT_TOKEN from token_env_var in environment."""
283
+ from skcapstone.sync.backends import GitBackend
284
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
285
+
286
+ config = SyncBackendConfig(
287
+ backend_type=SyncBackendType.GITHUB,
288
+ repo_url="https://github.com/org/repo",
289
+ branch="main",
290
+ token_env_var="MY_GIT_TOKEN",
291
+ )
292
+ backend = GitBackend(config, agent_home)
293
+ mock_result = MagicMock(returncode=0)
294
+ with patch.dict(os.environ, {"MY_GIT_TOKEN": "secret-token"}), \
295
+ patch("subprocess.run", return_value=mock_result) as mock_run:
296
+ backend._ensure_repo()
297
+
298
+ env_passed = mock_run.call_args[1].get("env", {})
299
+ assert env_passed.get("GIT_TOKEN") == "secret-token"
300
+
301
+ def test_push_calls_git_add_commit_push(self, agent_home: Path, vault_files):
302
+ """Push should run: git add -A, git commit, git push."""
303
+ backend = self._make_github_backend(agent_home)
304
+ # Pre-create .git so _ensure_repo doesn't clone
305
+ (backend._repo_dir / ".git").mkdir(parents=True)
306
+
307
+ mock_ok = MagicMock(returncode=0, stderr="")
308
+ vault, manifest = vault_files
309
+ with patch("subprocess.run", return_value=mock_ok) as mock_run:
310
+ result = backend.push(vault, manifest)
311
+
312
+ assert result is True
313
+ calls = [c[0][0] for c in mock_run.call_args_list]
314
+ git_subcommands = [c[1] for c in calls]
315
+ assert "add" in git_subcommands
316
+ assert "commit" in git_subcommands
317
+ assert "push" in git_subcommands
318
+
319
+ def test_push_returns_false_on_git_failure(self, agent_home: Path, vault_files):
320
+ """Push should return False if any git command fails."""
321
+ backend = self._make_github_backend(agent_home)
322
+ (backend._repo_dir / ".git").mkdir(parents=True)
323
+
324
+ vault, manifest = vault_files
325
+ fail = MagicMock(returncode=1, stderr="fatal: error")
326
+ ok = MagicMock(returncode=0, stderr="")
327
+ # git add ok, git commit fails
328
+ with patch("subprocess.run", side_effect=[ok, fail]):
329
+ result = backend.push(vault, manifest)
330
+ assert result is False
331
+
332
+ def test_push_copies_vault_to_repo_dir(self, agent_home: Path, vault_files):
333
+ """Push should copy vault and manifest into the repo dir."""
334
+ backend = self._make_github_backend(agent_home)
335
+ (backend._repo_dir / ".git").mkdir(parents=True)
336
+
337
+ vault, manifest = vault_files
338
+ mock_ok = MagicMock(returncode=0, stderr="")
339
+ with patch("subprocess.run", return_value=mock_ok):
340
+ backend.push(vault, manifest)
341
+
342
+ assert (backend._repo_dir / vault.name).exists()
343
+ assert (backend._repo_dir / manifest.name).exists()
344
+
345
+ def test_push_returns_false_if_ensure_repo_fails(self, agent_home: Path, vault_files):
346
+ backend = self._make_github_backend(agent_home)
347
+ vault, manifest = vault_files
348
+ with patch.object(backend, "_ensure_repo", return_value=False):
349
+ result = backend.push(vault, manifest)
350
+ assert result is False
351
+
352
+ def test_pull_calls_git_pull(self, agent_home: Path, tmp_path: Path):
353
+ """Pull should run git pull and return the newest vault file."""
354
+ backend = self._make_github_backend(agent_home)
355
+ (backend._repo_dir / ".git").mkdir(parents=True)
356
+
357
+ # Place a fake vault in repo dir
358
+ fake_vault = backend._repo_dir / "vault-host-20260228T000000Z.tar.gz"
359
+ fake_vault.write_bytes(b"vault content")
360
+
361
+ mock_ok = MagicMock(returncode=0, stderr="")
362
+ with patch("subprocess.run", return_value=mock_ok):
363
+ result = backend.pull(tmp_path)
364
+
365
+ assert result is not None
366
+ assert result.read_bytes() == b"vault content"
367
+
368
+ def test_pull_returns_none_on_git_pull_failure(self, agent_home: Path, tmp_path: Path):
369
+ backend = self._make_github_backend(agent_home)
370
+ (backend._repo_dir / ".git").mkdir(parents=True)
371
+ mock_fail = MagicMock(returncode=1, stderr="fatal")
372
+ with patch("subprocess.run", return_value=mock_fail):
373
+ result = backend.pull(tmp_path)
374
+ assert result is None
375
+
376
+ def test_pull_returns_none_when_no_vaults_in_repo(self, agent_home: Path, tmp_path: Path):
377
+ backend = self._make_github_backend(agent_home)
378
+ (backend._repo_dir / ".git").mkdir(parents=True)
379
+ mock_ok = MagicMock(returncode=0, stderr="")
380
+ with patch("subprocess.run", return_value=mock_ok):
381
+ result = backend.pull(tmp_path)
382
+ assert result is None
383
+
384
+ def test_pull_returns_none_if_ensure_repo_fails(self, agent_home: Path, tmp_path: Path):
385
+ backend = self._make_github_backend(agent_home)
386
+ with patch.object(backend, "_ensure_repo", return_value=False):
387
+ result = backend.pull(tmp_path)
388
+ assert result is None
389
+
390
+ def test_push_oserror_returns_false(self, agent_home: Path, tmp_path: Path):
391
+ """Push returns False on OSError when copying files."""
392
+ backend = self._make_github_backend(agent_home)
393
+ (backend._repo_dir / ".git").mkdir(parents=True)
394
+
395
+ missing_vault = tmp_path / "no-such-vault.tar.gz"
396
+ missing_manifest = tmp_path / "no-such-vault.manifest.json"
397
+ result = backend.push(missing_vault, missing_manifest)
398
+ assert result is False
399
+
400
+
401
+ # ---------------------------------------------------------------------------
402
+ # LocalBackend
403
+ # ---------------------------------------------------------------------------
404
+
405
+
406
+ class TestLocalBackend:
407
+ def _make_backend(self, agent_home: Path, target: Path):
408
+ from skcapstone.sync.backends import LocalBackend
409
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
410
+
411
+ config = SyncBackendConfig(
412
+ backend_type=SyncBackendType.LOCAL,
413
+ local_path=target,
414
+ )
415
+ return LocalBackend(config, agent_home)
416
+
417
+ def test_name_property(self, agent_home: Path, tmp_path: Path):
418
+ backend = self._make_backend(agent_home, tmp_path / "backup")
419
+ assert backend.name == "local"
420
+
421
+ def test_available_when_target_exists(self, agent_home: Path, tmp_path: Path):
422
+ target = tmp_path / "backup"
423
+ target.mkdir()
424
+ backend = self._make_backend(agent_home, target)
425
+ assert backend.available() is True
426
+
427
+ def test_available_when_target_missing(self, agent_home: Path, tmp_path: Path):
428
+ from skcapstone.sync.backends import LocalBackend
429
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
430
+
431
+ nonexistent = tmp_path / "does-not-exist"
432
+ config = SyncBackendConfig(
433
+ backend_type=SyncBackendType.LOCAL, local_path=nonexistent
434
+ )
435
+ # Constructor creates the dir, so we need to remove it after
436
+ backend = LocalBackend(config, agent_home)
437
+ nonexistent.rmdir()
438
+ assert backend.available() is False
439
+
440
+ def test_push_copies_vault_and_manifest(self, agent_home: Path, tmp_path: Path, vault_files):
441
+ target = tmp_path / "backup"
442
+ target.mkdir()
443
+ backend = self._make_backend(agent_home, target)
444
+ vault, manifest = vault_files
445
+
446
+ result = backend.push(vault, manifest)
447
+ assert result is True
448
+ assert (target / vault.name).exists()
449
+ assert (target / manifest.name).exists()
450
+
451
+ def test_push_returns_false_on_oserror(self, agent_home: Path, tmp_path: Path):
452
+ target = tmp_path / "backup"
453
+ target.mkdir()
454
+ backend = self._make_backend(agent_home, target)
455
+
456
+ missing = tmp_path / "missing.tar.gz"
457
+ missing_manifest = tmp_path / "missing.manifest.json"
458
+ result = backend.push(missing, missing_manifest)
459
+ assert result is False
460
+
461
+ def test_pull_returns_none_when_empty(self, agent_home: Path, tmp_path: Path):
462
+ target = tmp_path / "backup"
463
+ target.mkdir()
464
+ backend = self._make_backend(agent_home, target)
465
+ # Only put a non-vault file in target
466
+ (target / "unrelated.txt").write_text("ignore")
467
+ result = backend.pull(tmp_path / "dest")
468
+ (tmp_path / "dest").mkdir(exist_ok=True)
469
+ result = backend.pull(tmp_path / "dest")
470
+ assert result is None
471
+
472
+ def test_pull_returns_latest_by_mtime(self, agent_home: Path, tmp_path: Path):
473
+ """Pull should return the most recently modified vault file."""
474
+ import time
475
+
476
+ target = tmp_path / "backup"
477
+ target.mkdir()
478
+ dest = tmp_path / "dest"
479
+ dest.mkdir()
480
+ backend = self._make_backend(agent_home, target)
481
+
482
+ old = target / "vault-host-20260101T000000Z.tar.gz"
483
+ old.write_bytes(b"old vault")
484
+ time.sleep(0.05)
485
+ new = target / "vault-host-20260228T000000Z.tar.gz"
486
+ new.write_bytes(b"new vault")
487
+
488
+ result = backend.pull(dest)
489
+ assert result is not None
490
+ assert result.read_bytes() == b"new vault"
491
+
492
+ def test_default_target_when_no_local_path(self, agent_home: Path):
493
+ """When no local_path configured, uses agent_home/sync/local-backup."""
494
+ from skcapstone.sync.backends import LocalBackend
495
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
496
+
497
+ config = SyncBackendConfig(
498
+ backend_type=SyncBackendType.LOCAL, local_path=None
499
+ )
500
+ backend = LocalBackend(config, agent_home)
501
+ expected = agent_home / "sync" / "local-backup"
502
+ assert backend.target == expected
503
+ assert expected.is_dir()
504
+
505
+
506
+ # ---------------------------------------------------------------------------
507
+ # create_backend factory
508
+ # ---------------------------------------------------------------------------
509
+
510
+
511
+ class TestCreateBackendFactory:
512
+ def test_creates_gdrive_backend(self, agent_home: Path):
513
+ from skcapstone.sync.backends import GDriveBackend, create_backend
514
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
515
+
516
+ config = SyncBackendConfig(backend_type=SyncBackendType.GDRIVE)
517
+ backend = create_backend(config, agent_home)
518
+ assert isinstance(backend, GDriveBackend)
519
+ assert backend.name == "gdrive"
520
+
521
+ def test_syncthing_returns_syncthing_backend(self, agent_home: Path):
522
+ from skcapstone.sync.backends import SyncthingBackend, create_backend
523
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
524
+
525
+ config = SyncBackendConfig(backend_type=SyncBackendType.SYNCTHING)
526
+ assert isinstance(create_backend(config, agent_home), SyncthingBackend)
527
+
528
+ def test_github_returns_git_backend(self, agent_home: Path):
529
+ from skcapstone.sync.backends import GitBackend, create_backend
530
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
531
+
532
+ config = SyncBackendConfig(
533
+ backend_type=SyncBackendType.GITHUB,
534
+ repo_url="https://github.com/x/y",
535
+ )
536
+ backend = create_backend(config, agent_home)
537
+ assert isinstance(backend, GitBackend)
538
+ assert backend.name == "github"
539
+
540
+ def test_forgejo_returns_git_backend(self, agent_home: Path):
541
+ from skcapstone.sync.backends import GitBackend, create_backend
542
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
543
+
544
+ config = SyncBackendConfig(
545
+ backend_type=SyncBackendType.FORGEJO,
546
+ repo_url="https://forgejo.example/x/y",
547
+ )
548
+ backend = create_backend(config, agent_home)
549
+ assert isinstance(backend, GitBackend)
550
+ assert backend.name == "forgejo"
551
+
552
+ def test_local_returns_local_backend(self, agent_home: Path, tmp_path: Path):
553
+ from skcapstone.sync.backends import LocalBackend, create_backend
554
+ from skcapstone.sync.models import SyncBackendConfig, SyncBackendType
555
+
556
+ config = SyncBackendConfig(
557
+ backend_type=SyncBackendType.LOCAL,
558
+ local_path=tmp_path / "bkp",
559
+ )
560
+ assert isinstance(create_backend(config, agent_home), LocalBackend)