@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
@@ -1,12 +1,18 @@
1
- """Tests for the Syncthing setup skill."""
1
+ """Tests for the Syncthing setup skill — Sovereign Singularity."""
2
2
 
3
- from unittest.mock import patch
3
+ import xml.etree.ElementTree as ET
4
4
  from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ import pytest
5
8
 
6
9
  from skcapstone.skills.syncthing_setup import (
10
+ SHARED_FOLDER_ID,
11
+ STIGNORE_CONTENTS,
12
+ configure_syncthing_folder,
7
13
  detect_syncthing,
8
- get_install_instructions,
9
14
  ensure_shared_folder,
15
+ get_install_instructions,
10
16
  )
11
17
 
12
18
 
@@ -16,7 +22,7 @@ class TestDetectSyncthing:
16
22
  @patch("shutil.which", return_value="/usr/bin/syncthing")
17
23
  def test_found(self, mock_which):
18
24
  """Returns path when syncthing is installed."""
19
- assert detect_syncthing() == "/usr/bin/syncthing"
25
+ assert detect_syncthing() is not None
20
26
 
21
27
  @patch("shutil.which", return_value=None)
22
28
  def test_not_found(self, mock_which):
@@ -47,30 +53,170 @@ class TestGetInstallInstructions:
47
53
  assert "winget" in instructions
48
54
 
49
55
 
56
+ def _patch_homes(monkeypatch, tmp_path):
57
+ """Set AGENT_HOME and SYNC_DIR to a temp directory for testing."""
58
+ agent_home = tmp_path / ".skcapstone"
59
+ sync_dir = agent_home / "sync"
60
+ monkeypatch.setattr("skcapstone.skills.syncthing_setup.AGENT_HOME", agent_home)
61
+ monkeypatch.setattr("skcapstone.skills.syncthing_setup.SYNC_DIR", sync_dir)
62
+ return agent_home, sync_dir
63
+
64
+
50
65
  class TestEnsureSharedFolder:
51
- """Tests for ensure_shared_folder."""
66
+ """Tests for ensure_shared_folder — creates the full agent home."""
52
67
 
53
- def test_creates_directories(self, tmp_path, monkeypatch):
54
- """Creates outbox, inbox, archive subdirectories."""
55
- monkeypatch.setattr(
56
- "skcapstone.skills.syncthing_setup.SYNC_DIR",
57
- tmp_path / "sync",
58
- )
59
- from skcapstone.skills.syncthing_setup import ensure_shared_folder
68
+ def test_creates_all_pillar_directories(self, tmp_path, monkeypatch):
69
+ """Creates every pillar data directory under agent home."""
70
+ agent_home, _ = _patch_homes(monkeypatch, tmp_path)
60
71
 
61
72
  result = ensure_shared_folder()
62
- assert (result / "outbox").exists()
63
- assert (result / "inbox").exists()
64
- assert (result / "archive").exists()
73
+
74
+ assert result == agent_home
75
+ assert (agent_home / "identity").is_dir()
76
+ assert (agent_home / "memory" / "short-term").is_dir()
77
+ assert (agent_home / "memory" / "mid-term").is_dir()
78
+ assert (agent_home / "memory" / "long-term").is_dir()
79
+ assert (agent_home / "trust" / "febs").is_dir()
80
+ assert (agent_home / "security").is_dir()
81
+ assert (agent_home / "coordination" / "tasks").is_dir()
82
+ assert (agent_home / "coordination" / "agents").is_dir()
83
+ assert (agent_home / "config").is_dir()
84
+ assert (agent_home / "skills").is_dir()
85
+
86
+ def test_creates_sync_seed_directories(self, tmp_path, monkeypatch):
87
+ """Creates the sync seed outbox/inbox/archive."""
88
+ agent_home, sync_dir = _patch_homes(monkeypatch, tmp_path)
89
+
90
+ ensure_shared_folder()
91
+
92
+ assert (sync_dir / "outbox").is_dir()
93
+ assert (sync_dir / "inbox").is_dir()
94
+ assert (sync_dir / "archive").is_dir()
95
+
96
+ def test_creates_stignore(self, tmp_path, monkeypatch):
97
+ """Creates .stignore to protect private keys."""
98
+ agent_home, _ = _patch_homes(monkeypatch, tmp_path)
99
+
100
+ ensure_shared_folder()
101
+
102
+ stignore = agent_home / ".stignore"
103
+ assert stignore.exists()
104
+ contents = stignore.read_text()
105
+ assert "*.key" in contents
106
+ assert "*.pem" in contents
107
+ assert "__pycache__" in contents
65
108
 
66
109
  def test_idempotent(self, tmp_path, monkeypatch):
67
- """Calling twice doesn't fail."""
68
- monkeypatch.setattr(
69
- "skcapstone.skills.syncthing_setup.SYNC_DIR",
70
- tmp_path / "sync",
71
- )
72
- from skcapstone.skills.syncthing_setup import ensure_shared_folder
110
+ """Calling twice doesn't fail or corrupt anything."""
111
+ agent_home, _ = _patch_homes(monkeypatch, tmp_path)
73
112
 
74
113
  ensure_shared_folder()
75
114
  ensure_shared_folder()
76
- assert (tmp_path / "sync" / "outbox").exists()
115
+
116
+ assert (agent_home / "identity").is_dir()
117
+ assert (agent_home / ".stignore").exists()
118
+
119
+ def test_returns_agent_home_not_sync_dir(self, tmp_path, monkeypatch):
120
+ """ensure_shared_folder returns agent home (the Syncthing share root)."""
121
+ agent_home, sync_dir = _patch_homes(monkeypatch, tmp_path)
122
+
123
+ result = ensure_shared_folder()
124
+
125
+ assert result == agent_home
126
+ assert result != sync_dir
127
+
128
+
129
+ class TestConfigureSyncthingFolder:
130
+ """Tests for configure_syncthing_folder — Syncthing XML config."""
131
+
132
+ def _make_config(self, tmp_path, existing_folder=None):
133
+ """Create a minimal Syncthing config.xml for testing."""
134
+ config_path = tmp_path / "config.xml"
135
+ root = ET.Element("configuration")
136
+
137
+ if existing_folder:
138
+ folder = ET.SubElement(root, "folder")
139
+ for k, v in existing_folder.items():
140
+ folder.set(k, v)
141
+
142
+ tree = ET.ElementTree(root)
143
+ tree.write(str(config_path), xml_declaration=True)
144
+ return config_path
145
+
146
+ def test_adds_folder_pointing_at_agent_home(self, tmp_path, monkeypatch):
147
+ """New folder in config points at ~/.skcapstone, not sync/."""
148
+ agent_home, _ = _patch_homes(monkeypatch, tmp_path)
149
+ config_path = self._make_config(tmp_path)
150
+ monkeypatch.setattr(
151
+ "skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
152
+ config_path,
153
+ )
154
+
155
+ assert configure_syncthing_folder() is True
156
+
157
+ tree = ET.parse(config_path)
158
+ folders = list(tree.getroot().iter("folder"))
159
+ assert len(folders) == 1
160
+ assert folders[0].get("id") == SHARED_FOLDER_ID
161
+ assert folders[0].get("path") == str(agent_home)
162
+ assert folders[0].get("label") == "SKCapstone Sovereign"
163
+
164
+ def test_upgrades_old_sync_dir_path(self, tmp_path, monkeypatch):
165
+ """Existing folder pointing at sync/ gets upgraded to agent home."""
166
+ agent_home, sync_dir = _patch_homes(monkeypatch, tmp_path)
167
+ config_path = self._make_config(
168
+ tmp_path,
169
+ existing_folder={
170
+ "id": SHARED_FOLDER_ID,
171
+ "label": "SKCapstone Sync",
172
+ "path": str(sync_dir),
173
+ },
174
+ )
175
+ monkeypatch.setattr(
176
+ "skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
177
+ config_path,
178
+ )
179
+
180
+ assert configure_syncthing_folder() is True
181
+
182
+ tree = ET.parse(config_path)
183
+ folder = list(tree.getroot().iter("folder"))[0]
184
+ assert folder.get("path") == str(agent_home)
185
+ assert folder.get("label") == "SKCapstone Sovereign"
186
+
187
+ def test_already_correct_path_is_noop(self, tmp_path, monkeypatch):
188
+ """Folder already pointing at agent home returns True without writing."""
189
+ agent_home, _ = _patch_homes(monkeypatch, tmp_path)
190
+ config_path = self._make_config(
191
+ tmp_path,
192
+ existing_folder={
193
+ "id": SHARED_FOLDER_ID,
194
+ "path": str(agent_home),
195
+ },
196
+ )
197
+ monkeypatch.setattr(
198
+ "skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
199
+ config_path,
200
+ )
201
+
202
+ assert configure_syncthing_folder() is True
203
+
204
+ def test_no_config_file_returns_false(self, tmp_path, monkeypatch):
205
+ """Returns False when Syncthing config doesn't exist."""
206
+ monkeypatch.setattr(
207
+ "skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
208
+ tmp_path / "nonexistent.xml",
209
+ )
210
+
211
+ assert configure_syncthing_folder() is False
212
+
213
+ def test_corrupt_config_returns_false(self, tmp_path, monkeypatch):
214
+ """Returns False for unparseable XML."""
215
+ bad_config = tmp_path / "config.xml"
216
+ bad_config.write_text("not xml at all")
217
+ monkeypatch.setattr(
218
+ "skcapstone.skills.syncthing_setup.SYNCTHING_CONFIG_FILE",
219
+ bad_config,
220
+ )
221
+
222
+ assert configure_syncthing_folder() is False
@@ -0,0 +1,323 @@
1
+ """Tests for systemd service management module.
2
+
3
+ Tests unit file generation, install/uninstall logic, and status parsing.
4
+ Actual systemctl commands are mocked to avoid system dependencies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ from pathlib import Path
11
+ from unittest.mock import MagicMock, patch
12
+
13
+ import pytest
14
+
15
+ from skcapstone.systemd import (
16
+ ALL_UNITS,
17
+ HEARTBEAT_SERVICE,
18
+ HEARTBEAT_TIMER,
19
+ QUEUE_DRAIN_SERVICE,
20
+ QUEUE_DRAIN_TIMER,
21
+ SERVICE_NAME,
22
+ SOCKET_NAME,
23
+ TIMER_UNITS,
24
+ ServiceStatus,
25
+ generate_unit_file,
26
+ install_service,
27
+ service_status,
28
+ systemd_available,
29
+ uninstall_service,
30
+ )
31
+
32
+
33
+ class TestGenerateUnitFile:
34
+ """Tests for unit file generation."""
35
+
36
+ def test_default_unit_file(self) -> None:
37
+ """Generated unit file contains expected sections and defaults."""
38
+ content = generate_unit_file()
39
+ assert "[Unit]" in content
40
+ assert "[Service]" in content
41
+ assert "[Install]" in content
42
+ assert "ExecStart=skcapstone daemon start --foreground" in content
43
+ assert "Restart=on-failure" in content
44
+ assert "NoNewPrivileges=true" in content
45
+ assert "WantedBy=default.target" in content
46
+
47
+ def test_custom_python_path(self) -> None:
48
+ """Custom Python path is used in ExecStart."""
49
+ content = generate_unit_file(python_path="/usr/local/bin/skcapstone")
50
+ assert "ExecStart=/usr/local/bin/skcapstone daemon start" in content
51
+
52
+ def test_extra_env_vars(self) -> None:
53
+ """Extra environment variables are included."""
54
+ content = generate_unit_file(extra_env={"LOG_LEVEL": "debug", "PORT": "8888"})
55
+ assert "Environment=LOG_LEVEL=debug" in content
56
+ assert "Environment=PORT=8888" in content
57
+
58
+ def test_security_hardening_present(self) -> None:
59
+ """Security directives are in the generated unit."""
60
+ content = generate_unit_file()
61
+ assert "ProtectSystem=strict" in content
62
+ assert "ProtectHome=read-only" in content
63
+ assert "PrivateTmp=true" in content
64
+ assert "ReadWritePaths=" in content
65
+
66
+
67
+ class TestSystemdAvailable:
68
+ """Tests for systemd detection."""
69
+
70
+ @patch("skcapstone.systemd._run")
71
+ def test_available_when_systemctl_works(self, mock_run: MagicMock) -> None:
72
+ """Returns True when systemctl --user works."""
73
+ mock_run.return_value = subprocess.CompletedProcess([], 0, stdout="systemd 256")
74
+ assert systemd_available() is True
75
+
76
+ @patch("skcapstone.systemd._run")
77
+ def test_unavailable_when_systemctl_fails(self, mock_run: MagicMock) -> None:
78
+ """Returns False when systemctl is missing."""
79
+ mock_run.return_value = subprocess.CompletedProcess([], 1, stdout="")
80
+ assert systemd_available() is False
81
+
82
+
83
+ class TestInstallService:
84
+ """Tests for service installation."""
85
+
86
+ @patch("skcapstone.systemd._systemctl")
87
+ def test_install_copies_files(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
88
+ """Install copies unit files to target directory."""
89
+ mock_ctl.return_value = subprocess.CompletedProcess([], 0)
90
+
91
+ source = tmp_path / "source"
92
+ source.mkdir()
93
+ (source / SERVICE_NAME).write_text("[Unit]\nDescription=Test\n")
94
+ (source / SOCKET_NAME).write_text("[Socket]\nListenStream=127.0.0.1:7777\n")
95
+
96
+ target = tmp_path / "target"
97
+
98
+ result = install_service(
99
+ unit_dir=target, source_dir=source, enable=True, start=True,
100
+ )
101
+
102
+ assert result["installed"] is True
103
+ assert (target / SERVICE_NAME).exists()
104
+ assert (target / SOCKET_NAME).exists()
105
+
106
+ @patch("skcapstone.systemd._systemctl")
107
+ def test_install_enables_and_starts(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
108
+ """Install calls enable and start."""
109
+ mock_ctl.return_value = subprocess.CompletedProcess([], 0)
110
+
111
+ source = tmp_path / "src"
112
+ source.mkdir()
113
+ (source / SERVICE_NAME).write_text("[Unit]\n")
114
+
115
+ result = install_service(
116
+ unit_dir=tmp_path / "tgt", source_dir=source,
117
+ )
118
+
119
+ assert result["enabled"] is True
120
+ assert result["started"] is True
121
+
122
+ calls = [c.args[0] for c in mock_ctl.call_args_list]
123
+ enable_calls = [c for c in calls if "enable" in c]
124
+ start_calls = [c for c in calls if "start" in c]
125
+ assert len(enable_calls) >= 1
126
+ assert len(start_calls) >= 1
127
+
128
+ def test_install_missing_source_returns_false(self, tmp_path: Path) -> None:
129
+ """Install fails gracefully when source unit doesn't exist."""
130
+ result = install_service(
131
+ unit_dir=tmp_path / "tgt",
132
+ source_dir=tmp_path / "nonexistent",
133
+ )
134
+ assert result["installed"] is False
135
+
136
+
137
+ class TestUninstallService:
138
+ """Tests for service uninstallation."""
139
+
140
+ @patch("skcapstone.systemd._systemctl")
141
+ def test_uninstall_removes_files(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
142
+ """Uninstall removes unit files from target directory."""
143
+ mock_ctl.return_value = subprocess.CompletedProcess([], 0)
144
+
145
+ (tmp_path / SERVICE_NAME).write_text("[Unit]\n")
146
+ (tmp_path / SOCKET_NAME).write_text("[Socket]\n")
147
+
148
+ result = uninstall_service(unit_dir=tmp_path)
149
+
150
+ assert result["stopped"] is True
151
+ assert result["disabled"] is True
152
+ assert result["removed"] is True
153
+ assert not (tmp_path / SERVICE_NAME).exists()
154
+ assert not (tmp_path / SOCKET_NAME).exists()
155
+
156
+
157
+ class TestServiceStatus:
158
+ """Tests for status querying."""
159
+
160
+ def test_status_not_installed(self, tmp_path: Path) -> None:
161
+ """Status reports not installed when unit file is missing."""
162
+ with patch("skcapstone.systemd.SYSTEMD_USER_DIR", tmp_path / "nonexistent"):
163
+ status = service_status()
164
+ assert status.installed is False
165
+ assert status.active is False
166
+
167
+ @patch("skcapstone.systemd._systemctl")
168
+ def test_status_running(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
169
+ """Status reports active when service is running."""
170
+ (tmp_path / SERVICE_NAME).write_text("[Unit]\n")
171
+
172
+ def side_effect(*args):
173
+ cmd = args[0] if args else ""
174
+ if cmd == "is-enabled":
175
+ return subprocess.CompletedProcess([], 0, stdout="enabled\n")
176
+ if cmd == "is-active":
177
+ return subprocess.CompletedProcess([], 0, stdout="active\n")
178
+ if cmd == "show":
179
+ return subprocess.CompletedProcess(
180
+ [], 0,
181
+ stdout="MainPID=12345\nActiveEnterTimestamp=Mon 2026-02-24 05:00:00 UTC\nMemoryCurrent=52428800\nExecMainStatus=0\n",
182
+ )
183
+ return subprocess.CompletedProcess([], 0, stdout="")
184
+
185
+ mock_ctl.side_effect = side_effect
186
+
187
+ with patch("skcapstone.systemd.SYSTEMD_USER_DIR", tmp_path):
188
+ status = service_status()
189
+
190
+ assert status.installed is True
191
+ assert status.enabled is True
192
+ assert status.active is True
193
+ assert status.pid == 12345
194
+ assert "50.0 MB" in status.memory
195
+
196
+
197
+ class TestServiceStatusModel:
198
+ """Tests for the ServiceStatus dataclass."""
199
+
200
+ def test_defaults(self) -> None:
201
+ """Default status is all-false."""
202
+ s = ServiceStatus()
203
+ assert s.installed is False
204
+ assert s.enabled is False
205
+ assert s.active is False
206
+ assert s.pid == 0
207
+
208
+
209
+ class TestUnitConstants:
210
+ """Tests for unit file constants and bundled files."""
211
+
212
+ def test_all_units_includes_timers(self) -> None:
213
+ """ALL_UNITS includes heartbeat and queue drain timers."""
214
+ assert HEARTBEAT_TIMER in ALL_UNITS
215
+ assert QUEUE_DRAIN_TIMER in ALL_UNITS
216
+ assert HEARTBEAT_SERVICE in ALL_UNITS
217
+ assert QUEUE_DRAIN_SERVICE in ALL_UNITS
218
+
219
+ def test_timer_units_list(self) -> None:
220
+ """TIMER_UNITS contains exactly the two timers."""
221
+ assert len(TIMER_UNITS) == 2
222
+ assert HEARTBEAT_TIMER in TIMER_UNITS
223
+ assert QUEUE_DRAIN_TIMER in TIMER_UNITS
224
+
225
+ def test_all_units_count(self) -> None:
226
+ """ALL_UNITS has the expected number of units."""
227
+ assert len(ALL_UNITS) == 6
228
+
229
+ def test_bundled_service_file_exists(self) -> None:
230
+ """The bundled skcapstone.service file exists."""
231
+ from skcapstone.systemd import BUNDLED_DIR
232
+ assert (BUNDLED_DIR / SERVICE_NAME).exists()
233
+
234
+ def test_bundled_heartbeat_timer_exists(self) -> None:
235
+ """The bundled heartbeat timer file exists."""
236
+ from skcapstone.systemd import BUNDLED_DIR
237
+ assert (BUNDLED_DIR / HEARTBEAT_TIMER).exists()
238
+
239
+ def test_bundled_queue_drain_timer_exists(self) -> None:
240
+ """The bundled queue drain timer file exists."""
241
+ from skcapstone.systemd import BUNDLED_DIR
242
+ assert (BUNDLED_DIR / QUEUE_DRAIN_TIMER).exists()
243
+
244
+ def test_bundled_heartbeat_service_exists(self) -> None:
245
+ """The bundled heartbeat service file exists."""
246
+ from skcapstone.systemd import BUNDLED_DIR
247
+ assert (BUNDLED_DIR / HEARTBEAT_SERVICE).exists()
248
+
249
+ def test_bundled_queue_drain_service_exists(self) -> None:
250
+ """The bundled queue drain service file exists."""
251
+ from skcapstone.systemd import BUNDLED_DIR
252
+ assert (BUNDLED_DIR / QUEUE_DRAIN_SERVICE).exists()
253
+
254
+
255
+ class TestTimerInstall:
256
+ """Tests for timer unit installation alongside the main service."""
257
+
258
+ @patch("skcapstone.systemd._systemctl")
259
+ def test_install_copies_all_units(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
260
+ """Install copies service, socket, and timer units."""
261
+ mock_ctl.return_value = subprocess.CompletedProcess([], 0)
262
+
263
+ source = tmp_path / "source"
264
+ source.mkdir()
265
+ for name in ALL_UNITS:
266
+ (source / name).write_text(f"[Unit]\nDescription={name}\n")
267
+
268
+ target = tmp_path / "target"
269
+ result = install_service(unit_dir=target, source_dir=source)
270
+
271
+ assert result["installed"] is True
272
+ assert result["timers_enabled"] is True
273
+ for name in ALL_UNITS:
274
+ assert (target / name).exists(), f"{name} not copied"
275
+
276
+ @patch("skcapstone.systemd._systemctl")
277
+ def test_install_enables_timers(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
278
+ """Install enables timer units."""
279
+ mock_ctl.return_value = subprocess.CompletedProcess([], 0)
280
+
281
+ source = tmp_path / "src"
282
+ source.mkdir()
283
+ for name in ALL_UNITS:
284
+ (source / name).write_text("[Unit]\n")
285
+
286
+ install_service(unit_dir=tmp_path / "tgt", source_dir=source)
287
+
288
+ enable_calls = [
289
+ c.args[0] for c in mock_ctl.call_args_list
290
+ if len(c.args) > 0 and c.args[0] == "enable"
291
+ ]
292
+ assert len(enable_calls) >= 3
293
+
294
+ @patch("skcapstone.systemd._systemctl")
295
+ def test_uninstall_removes_all_units(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
296
+ """Uninstall removes all unit files including timers."""
297
+ mock_ctl.return_value = subprocess.CompletedProcess([], 0)
298
+
299
+ for name in ALL_UNITS:
300
+ (tmp_path / name).write_text("[Unit]\n")
301
+
302
+ result = uninstall_service(unit_dir=tmp_path)
303
+
304
+ assert result["removed"] is True
305
+ for name in ALL_UNITS:
306
+ assert not (tmp_path / name).exists(), f"{name} not removed"
307
+
308
+ @patch("skcapstone.systemd._systemctl")
309
+ def test_uninstall_stops_timers_before_service(self, mock_ctl: MagicMock, tmp_path: Path) -> None:
310
+ """Uninstall stops timers before stopping the main service."""
311
+ calls: list[tuple] = []
312
+
313
+ def track(*args):
314
+ calls.append(args)
315
+ return subprocess.CompletedProcess([], 0)
316
+
317
+ mock_ctl.side_effect = track
318
+ (tmp_path / SERVICE_NAME).write_text("[Unit]\n")
319
+
320
+ uninstall_service(unit_dir=tmp_path)
321
+
322
+ stop_calls = [c[0] for c in calls if c[0] == "stop"]
323
+ assert len(stop_calls) >= 3