@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,292 @@
1
+ """
2
+ tests/test_e2e_automated.py — Automated multi-agent E2E test via subprocess.
3
+
4
+ Starts the real skcapstone daemon, injects a message into the inbox,
5
+ and verifies that a response appears within the timeout window.
6
+
7
+ Marks are applied so the test is automatically skipped in unit-test
8
+ environments where the CLI is not installed or system requirements
9
+ are not met.
10
+
11
+ Run manually:
12
+ pytest tests/test_e2e_automated.py -v -s --timeout=360
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import shutil
20
+ import signal
21
+ import subprocess
22
+ import sys
23
+ import tempfile
24
+ import time
25
+ from pathlib import Path
26
+
27
+ import pytest
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Skip guards
31
+ # ---------------------------------------------------------------------------
32
+
33
+ pytestmark = [
34
+ pytest.mark.skipif(
35
+ not shutil.which("skcapstone"),
36
+ reason="skcapstone CLI not installed — skipping live E2E",
37
+ ),
38
+ pytest.mark.e2e,
39
+ ]
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Constants
43
+ # ---------------------------------------------------------------------------
44
+
45
+ DAEMON_PORT = int(os.environ.get("E2E_PORT", "17777")) # offset to avoid collision
46
+ STARTUP_WAIT = int(os.environ.get("E2E_STARTUP_WAIT", "10"))
47
+ POLL_TIMEOUT = int(os.environ.get("E2E_POLL_TIMEOUT", "300"))
48
+ PEER = os.environ.get("E2E_PEER", "test-peer")
49
+ AGENT_HOME = Path(
50
+ os.environ.get("SKCAPSTONE_ROOT", os.environ.get("SKCAPSTONE_HOME", "~/.skcapstone"))
51
+ ).expanduser()
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Helpers
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ def _write_test_message(inbox_dir: Path, peer: str) -> tuple[Path, str]:
60
+ """Write a test .skc.json message to inbox_dir and return (path, msg_id)."""
61
+ ts = int(time.time())
62
+ msg_id = f"e2e-auto-{ts}"
63
+ msg = {
64
+ "sender": peer,
65
+ "recipient": "Opus",
66
+ "payload": {
67
+ "content": f"Ping test — automated pytest E2E at {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
68
+ "content_type": "text",
69
+ },
70
+ "message_id": msg_id,
71
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime()),
72
+ }
73
+ path = inbox_dir / f"{msg_id}.skc.json"
74
+ path.write_text(json.dumps(msg))
75
+ return path, msg_id
76
+
77
+
78
+ def _poll_for_response(
79
+ outbox_dir: Path,
80
+ conv_file: Path,
81
+ inbox_msg_path: Path,
82
+ timeout_secs: int,
83
+ ) -> bool:
84
+ """
85
+ Return True if a response is detected within timeout_secs.
86
+
87
+ Detection strategy (either satisfies the check):
88
+ 1. A new .skc.json appears in outbox_dir AFTER inbox_msg_path's mtime.
89
+ 2. conv_file is created/updated AFTER inbox_msg_path's mtime.
90
+ """
91
+ outbox_dir.mkdir(parents=True, exist_ok=True)
92
+ ref_mtime = inbox_msg_path.stat().st_mtime
93
+
94
+ deadline = time.monotonic() + timeout_secs
95
+ poll_interval = 2.0
96
+ last_log = time.monotonic()
97
+
98
+ while time.monotonic() < deadline:
99
+ # Check outbox for new envelope
100
+ for skc in outbox_dir.glob("*.skc.json"):
101
+ if skc.stat().st_mtime > ref_mtime:
102
+ return True
103
+
104
+ # Check conversations file (passthrough / no-SKComm fallback)
105
+ if conv_file.exists() and conv_file.stat().st_mtime > ref_mtime:
106
+ return True
107
+
108
+ now = time.monotonic()
109
+ if now - last_log >= 30:
110
+ elapsed = timeout_secs - (deadline - now)
111
+ print(f"\n [e2e] still waiting… {elapsed:.0f}s elapsed / {timeout_secs}s timeout")
112
+ last_log = now
113
+
114
+ time.sleep(poll_interval)
115
+
116
+ return False
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Fixtures
121
+ # ---------------------------------------------------------------------------
122
+
123
+
124
+ @pytest.fixture(scope="module")
125
+ def daemon_process():
126
+ """
127
+ Start the skcapstone daemon in the background for the duration of the module.
128
+
129
+ Yields the subprocess.Popen handle; tears down on module exit.
130
+ """
131
+ log_fd, log_path = tempfile.mkstemp(prefix="skcapstone-e2e-", suffix=".log")
132
+
133
+ env = os.environ.copy()
134
+ env.setdefault("SKCAPSTONE_ROOT", str(AGENT_HOME))
135
+
136
+ proc = subprocess.Popen(
137
+ [
138
+ "skcapstone",
139
+ "daemon",
140
+ "start",
141
+ "--foreground",
142
+ "--port",
143
+ str(DAEMON_PORT),
144
+ ],
145
+ stdout=log_fd,
146
+ stderr=log_fd,
147
+ env=env,
148
+ preexec_fn=os.setsid, # separate process group for clean teardown
149
+ )
150
+ os.close(log_fd)
151
+
152
+ print(f"\n [e2e] Daemon started (PID {proc.pid}) — log: {log_path}")
153
+ print(f" [e2e] Waiting {STARTUP_WAIT}s for startup…")
154
+ time.sleep(STARTUP_WAIT)
155
+
156
+ if proc.poll() is not None:
157
+ with open(log_path) as fh:
158
+ tail = fh.read()[-2000:]
159
+ pytest.fail(
160
+ f"Daemon exited prematurely (rc={proc.returncode}).\n"
161
+ f"Log tail:\n{tail}"
162
+ )
163
+
164
+ yield proc, log_path
165
+
166
+ # Teardown — send SIGTERM to the whole process group
167
+ try:
168
+ os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
169
+ except ProcessLookupError:
170
+ pass
171
+ try:
172
+ proc.wait(timeout=10)
173
+ except subprocess.TimeoutExpired:
174
+ try:
175
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
176
+ except ProcessLookupError:
177
+ pass
178
+ print(f"\n [e2e] Daemon stopped. Log: {log_path}")
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Tests
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ class TestDaemonStartup:
187
+ """Verify the daemon starts and exposes its HTTP API."""
188
+
189
+ def test_daemon_is_running(self, daemon_process):
190
+ """The daemon subprocess must still be alive after startup wait."""
191
+ proc, _ = daemon_process
192
+ assert proc.poll() is None, "Daemon exited before tests ran"
193
+
194
+ def test_consciousness_endpoint_responds(self, daemon_process):
195
+ """GET /consciousness must return valid JSON."""
196
+ import urllib.request
197
+ import urllib.error
198
+
199
+ url = f"http://127.0.0.1:{DAEMON_PORT}/consciousness"
200
+ try:
201
+ with urllib.request.urlopen(url, timeout=10) as resp:
202
+ body = resp.read().decode()
203
+ data = json.loads(body)
204
+ except urllib.error.URLError as exc:
205
+ pytest.fail(f"/consciousness unreachable on port {DAEMON_PORT}: {exc}")
206
+
207
+ assert isinstance(data, dict), f"Expected JSON object, got: {body[:200]}"
208
+ # The endpoint should include some status indicator
209
+ assert data, "Response JSON is empty"
210
+
211
+ def test_consciousness_status_active(self, daemon_process):
212
+ """The /consciousness endpoint should report an active/running status."""
213
+ import urllib.request
214
+
215
+ url = f"http://127.0.0.1:{DAEMON_PORT}/consciousness"
216
+ with urllib.request.urlopen(url, timeout=10) as resp:
217
+ data = json.loads(resp.read())
218
+
219
+ status = str(data.get("status", "")).lower()
220
+ # Accept various status strings that indicate the loop is running
221
+ active_statuses = {"active", "ok", "running", "started", "conscious"}
222
+ assert status in active_statuses or data.get("conscious") is True, (
223
+ f"Expected active status, got: {data}"
224
+ )
225
+
226
+
227
+ class TestMessageRoundTrip:
228
+ """End-to-end: inject message → daemon processes → response appears."""
229
+
230
+ @pytest.fixture(autouse=True)
231
+ def _setup_dirs(self):
232
+ """Ensure inbox and outbox directories exist before each test."""
233
+ inbox = AGENT_HOME / "sync" / "comms" / "inbox" / PEER
234
+ outbox = AGENT_HOME / "sync" / "comms" / "outbox" / PEER
235
+ inbox.mkdir(parents=True, exist_ok=True)
236
+ outbox.mkdir(parents=True, exist_ok=True)
237
+
238
+ def test_inbox_message_is_processed(self, daemon_process):
239
+ """
240
+ Writing a .skc.json to the inbox triggers the consciousness loop
241
+ and produces a response in the outbox OR updates conversations/.
242
+ """
243
+ inbox_dir = AGENT_HOME / "sync" / "comms" / "inbox" / PEER
244
+ outbox_dir = AGENT_HOME / "sync" / "comms" / "outbox" / PEER
245
+ conv_file = AGENT_HOME / "conversations" / f"{PEER}.json"
246
+
247
+ msg_path, msg_id = _write_test_message(inbox_dir, PEER)
248
+ print(f"\n [e2e] Message written: {msg_path} (id={msg_id})")
249
+
250
+ found = _poll_for_response(
251
+ outbox_dir=outbox_dir,
252
+ conv_file=conv_file,
253
+ inbox_msg_path=msg_path,
254
+ timeout_secs=POLL_TIMEOUT,
255
+ )
256
+
257
+ assert found, (
258
+ f"No response detected within {POLL_TIMEOUT}s.\n"
259
+ f" inbox_dir: {inbox_dir}\n"
260
+ f" outbox_dir: {outbox_dir}\n"
261
+ f" conv_file: {conv_file}"
262
+ )
263
+
264
+ def test_conversations_file_updated(self, daemon_process):
265
+ """
266
+ After a message is processed, ~/.skcapstone/conversations/<peer>.json
267
+ must exist and contain valid JSON with the peer's conversation history.
268
+ """
269
+ inbox_dir = AGENT_HOME / "sync" / "comms" / "inbox" / PEER
270
+ outbox_dir = AGENT_HOME / "sync" / "comms" / "outbox" / PEER
271
+ conv_file = AGENT_HOME / "conversations" / f"{PEER}.json"
272
+
273
+ msg_path, msg_id = _write_test_message(inbox_dir, PEER)
274
+ print(f"\n [e2e] Message written: {msg_path} (id={msg_id})")
275
+
276
+ # Wait for conversations file to appear
277
+ deadline = time.monotonic() + POLL_TIMEOUT
278
+ ref_mtime = msg_path.stat().st_mtime
279
+ while time.monotonic() < deadline:
280
+ if conv_file.exists() and conv_file.stat().st_mtime >= ref_mtime:
281
+ break
282
+ time.sleep(2)
283
+
284
+ assert conv_file.exists(), (
285
+ f"conversations/{PEER}.json not found after {POLL_TIMEOUT}s"
286
+ )
287
+
288
+ content = conv_file.read_text()
289
+ data = json.loads(content) # raises if invalid JSON
290
+ assert isinstance(data, (dict, list)), (
291
+ f"Unexpected conversations format: {content[:200]}"
292
+ )
@@ -0,0 +1,404 @@
1
+ """Tests for the persistent error recovery queue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from datetime import datetime, timedelta, timezone
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from skcapstone.error_queue import (
13
+ ErrorEntry,
14
+ ErrorQueue,
15
+ ErrorStatus,
16
+ _backoff_ts,
17
+ _now_iso,
18
+ )
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Fixtures
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ @pytest.fixture
27
+ def queue_path(tmp_path: Path) -> Path:
28
+ """Return a temp path for the error queue JSON file."""
29
+ return tmp_path / "error_queue.json"
30
+
31
+
32
+ @pytest.fixture
33
+ def queue(queue_path: Path) -> ErrorQueue:
34
+ """Return an ErrorQueue backed by a temp file with fast backoff."""
35
+ return ErrorQueue(path=queue_path, max_retries=3, base_backoff=0)
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # ErrorEntry serialisation
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ class TestErrorEntry:
44
+ """Tests for ErrorEntry (de)serialisation."""
45
+
46
+ def test_roundtrip(self) -> None:
47
+ """to_dict / from_dict round-trip preserves all fields."""
48
+ entry = ErrorEntry(
49
+ operation_type="llm_call",
50
+ payload={"model": "llama3", "prompt": "hello"},
51
+ error_message="timeout",
52
+ )
53
+ entry.retry_count = 2
54
+ entry.next_retry_at = "2099-01-01T00:00:00+00:00"
55
+ entry.status = ErrorStatus.PENDING
56
+
57
+ restored = ErrorEntry.from_dict(entry.to_dict())
58
+
59
+ assert restored.entry_id == entry.entry_id
60
+ assert restored.operation_type == "llm_call"
61
+ assert restored.payload == {"model": "llama3", "prompt": "hello"}
62
+ assert restored.error_message == "timeout"
63
+ assert restored.retry_count == 2
64
+ assert restored.next_retry_at == entry.next_retry_at
65
+ assert restored.status == ErrorStatus.PENDING
66
+
67
+ def test_defaults(self) -> None:
68
+ """entry_id and created_at are auto-generated when omitted."""
69
+ entry = ErrorEntry(
70
+ operation_type="sync", payload={}, error_message="network error"
71
+ )
72
+ assert len(entry.entry_id) == 32 # uuid4 hex
73
+ assert "T" in entry.created_at # ISO-8601
74
+ assert entry.retry_count == 0
75
+ assert entry.status == ErrorStatus.PENDING
76
+
77
+ def test_repr(self) -> None:
78
+ """__repr__ includes type, retry count, and status."""
79
+ entry = ErrorEntry("message_send", {}, "refused")
80
+ r = repr(entry)
81
+ assert "message_send" in r
82
+ assert "retries=0" in r
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # ErrorQueue — basic operations
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ class TestErrorQueueBasic:
91
+ """Tests for enqueue, list, and persistence."""
92
+
93
+ def test_enqueue_creates_entry(self, queue: ErrorQueue) -> None:
94
+ """enqueue() adds an entry and returns it."""
95
+ entry = queue.enqueue("llm_call", {"model": "grok"}, "500 error")
96
+
97
+ assert entry.operation_type == "llm_call"
98
+ assert entry.status == ErrorStatus.PENDING
99
+ assert entry.retry_count == 0
100
+
101
+ def test_enqueue_persists_to_disk(self, queue: ErrorQueue, queue_path: Path) -> None:
102
+ """enqueue() writes JSON to the configured path."""
103
+ queue.enqueue("sync", {}, "network timeout")
104
+
105
+ assert queue_path.exists()
106
+ data = json.loads(queue_path.read_text())
107
+ assert len(data) == 1
108
+ assert data[0]["operation_type"] == "sync"
109
+
110
+ def test_list_returns_newest_first(self, queue: ErrorQueue) -> None:
111
+ """list_entries() returns entries sorted newest-first."""
112
+ e1 = queue.enqueue("llm_call", {}, "err1")
113
+ e2 = queue.enqueue("message_send", {}, "err2")
114
+
115
+ entries = queue.list_entries()
116
+ ids = [e.entry_id for e in entries]
117
+ assert ids.index(e2.entry_id) < ids.index(e1.entry_id)
118
+
119
+ def test_list_excludes_resolved_by_default(self, queue: ErrorQueue) -> None:
120
+ """list_entries() hides resolved entries unless include_resolved=True."""
121
+ entry = queue.enqueue("llm_call", {}, "err")
122
+ # Force-resolve by marking directly in JSON
123
+ entries = queue._load()
124
+ entries[0].status = ErrorStatus.RESOLVED
125
+ queue._save(entries)
126
+
127
+ assert queue.list_entries() == []
128
+ assert len(queue.list_entries(include_resolved=True)) == 1
129
+
130
+ def test_list_filter_by_status(self, queue: ErrorQueue) -> None:
131
+ """list_entries(status=...) filters correctly."""
132
+ queue.enqueue("llm_call", {}, "err")
133
+ entries = queue._load()
134
+ entries[0].status = ErrorStatus.EXHAUSTED
135
+ queue._save(entries)
136
+
137
+ assert len(queue.list_entries(status="exhausted")) == 1
138
+ assert queue.list_entries(status="pending") == []
139
+
140
+ def test_queue_survives_reload(self, queue_path: Path) -> None:
141
+ """Data persists across separate ErrorQueue instances."""
142
+ q1 = ErrorQueue(path=queue_path, base_backoff=0)
143
+ q1.enqueue("sync", {"x": 1}, "disk full")
144
+
145
+ q2 = ErrorQueue(path=queue_path, base_backoff=0)
146
+ entries = q2.list_entries()
147
+ assert len(entries) == 1
148
+ assert entries[0].payload == {"x": 1}
149
+
150
+ def test_empty_queue_when_file_missing(self, tmp_path: Path) -> None:
151
+ """list_entries() returns [] when queue file does not exist."""
152
+ q = ErrorQueue(path=tmp_path / "nonexistent.json")
153
+ assert q.list_entries() == []
154
+
155
+ def test_corrupt_file_returns_empty(self, queue_path: Path) -> None:
156
+ """A corrupt JSON file is treated as an empty queue."""
157
+ queue_path.write_text("NOT JSON", encoding="utf-8")
158
+ q = ErrorQueue(path=queue_path)
159
+ assert q.list_entries() == []
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # ErrorQueue — retry logic
164
+ # ---------------------------------------------------------------------------
165
+
166
+
167
+ class TestErrorQueueRetry:
168
+ """Tests for retry and exponential backoff."""
169
+
170
+ def test_retry_success_marks_resolved(self, queue: ErrorQueue) -> None:
171
+ """A successful retry marks the entry resolved."""
172
+ entry = queue.enqueue("llm_call", {}, "timeout")
173
+
174
+ result = queue.retry(entry.entry_id, handler=lambda e: True)
175
+
176
+ assert result is True
177
+ loaded = queue._load()
178
+ assert loaded[0].status == ErrorStatus.RESOLVED
179
+
180
+ def test_retry_failure_increments_count(self, queue: ErrorQueue) -> None:
181
+ """A failed retry increments retry_count and stays PENDING."""
182
+ entry = queue.enqueue("message_send", {}, "refused")
183
+
184
+ queue.retry(entry.entry_id, handler=lambda e: False)
185
+
186
+ loaded = queue._load()
187
+ assert loaded[0].retry_count == 1
188
+ assert loaded[0].status == ErrorStatus.PENDING
189
+
190
+ def test_retry_exhausted_after_max_retries(self, queue: ErrorQueue) -> None:
191
+ """After max_retries failed attempts the entry is EXHAUSTED."""
192
+ entry = queue.enqueue("sync", {}, "server down")
193
+
194
+ for _ in range(queue._max_retries):
195
+ queue.retry(entry.entry_id, handler=lambda e: False)
196
+
197
+ loaded = queue._load()
198
+ assert loaded[0].status == ErrorStatus.EXHAUSTED
199
+ assert loaded[0].next_retry_at is None
200
+
201
+ def test_exhausted_entry_skips_retry(self, queue: ErrorQueue) -> None:
202
+ """Retrying an exhausted entry returns False immediately."""
203
+ entry = queue.enqueue("llm_call", {}, "404")
204
+ entries = queue._load()
205
+ entries[0].status = ErrorStatus.EXHAUSTED
206
+ queue._save(entries)
207
+
208
+ result = queue.retry(entry.entry_id, handler=lambda e: True)
209
+ assert result is False
210
+
211
+ def test_retry_unknown_id_returns_false(self, queue: ErrorQueue) -> None:
212
+ """Retrying an unknown entry_id returns False."""
213
+ result = queue.retry("deadbeef" * 4, handler=lambda e: True)
214
+ assert result is False
215
+
216
+ def test_backoff_increases_with_attempts(self) -> None:
217
+ """_backoff_ts produces later timestamps for higher attempt numbers."""
218
+ t0 = _backoff_ts(0, base=10)
219
+ t1 = _backoff_ts(1, base=10)
220
+ t2 = _backoff_ts(2, base=10)
221
+ assert t0 < t1 < t2
222
+
223
+ def test_retry_all_due_processes_only_due(self, queue_path: Path) -> None:
224
+ """retry_all_due() skips entries whose next_retry_at is in the future."""
225
+ q = ErrorQueue(path=queue_path, base_backoff=0)
226
+ past = (datetime.now(timezone.utc) - timedelta(seconds=5)).isoformat()
227
+ future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
228
+
229
+ # Create both entries then manually set their next_retry_at
230
+ e_due = q.enqueue("llm_call", {}, "err")
231
+ e_future = q.enqueue("sync", {}, "err2")
232
+
233
+ entries = q._load()
234
+ for e in entries:
235
+ if e.entry_id == e_due.entry_id:
236
+ e.next_retry_at = past
237
+ else:
238
+ e.next_retry_at = future
239
+ q._save(entries)
240
+
241
+ called: list[str] = []
242
+
243
+ def handler(entry: ErrorEntry) -> bool:
244
+ called.append(entry.entry_id)
245
+ return True
246
+
247
+ q.retry_all_due(handler=handler)
248
+ assert e_due.entry_id in called
249
+ assert e_future.entry_id not in called
250
+
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # ErrorQueue — remove / clear
254
+ # ---------------------------------------------------------------------------
255
+
256
+
257
+ class TestErrorQueueClear:
258
+ """Tests for remove and clear_all."""
259
+
260
+ def test_remove_existing_entry(self, queue: ErrorQueue) -> None:
261
+ """remove() deletes a specific entry and returns True."""
262
+ entry = queue.enqueue("sync", {}, "err")
263
+ result = queue.remove(entry.entry_id)
264
+ assert result is True
265
+ assert queue.list_entries() == []
266
+
267
+ def test_remove_nonexistent_returns_false(self, queue: ErrorQueue) -> None:
268
+ """remove() returns False for an unknown entry_id."""
269
+ assert queue.remove("no-such-id") is False
270
+
271
+ def test_clear_all(self, queue: ErrorQueue) -> None:
272
+ """clear_all() removes every entry and returns the count."""
273
+ queue.enqueue("llm_call", {}, "err1")
274
+ queue.enqueue("sync", {}, "err2")
275
+
276
+ removed = queue.clear_all()
277
+ assert removed == 2
278
+ assert queue.list_entries() == []
279
+
280
+ def test_clear_by_status(self, queue: ErrorQueue) -> None:
281
+ """clear_all(status=...) only removes matching entries."""
282
+ e1 = queue.enqueue("llm_call", {}, "err1")
283
+ e2 = queue.enqueue("sync", {}, "err2")
284
+
285
+ entries = queue._load()
286
+ for e in entries:
287
+ if e.entry_id == e1.entry_id:
288
+ e.status = ErrorStatus.EXHAUSTED
289
+ queue._save(entries)
290
+
291
+ removed = queue.clear_all(status=ErrorStatus.EXHAUSTED)
292
+ assert removed == 1
293
+
294
+ remaining = queue.list_entries(include_resolved=True)
295
+ assert len(remaining) == 1
296
+ assert remaining[0].entry_id == e2.entry_id
297
+
298
+
299
+ # ---------------------------------------------------------------------------
300
+ # ErrorQueue — stats
301
+ # ---------------------------------------------------------------------------
302
+
303
+
304
+ class TestErrorQueueStats:
305
+ """Tests for the stats() summary method."""
306
+
307
+ def test_stats_counts_correctly(self, queue: ErrorQueue) -> None:
308
+ """stats() returns accurate counts per status."""
309
+ queue.enqueue("llm_call", {}, "err1")
310
+ queue.enqueue("sync", {}, "err2")
311
+
312
+ entries = queue._load()
313
+ entries[0].status = ErrorStatus.EXHAUSTED
314
+ queue._save(entries)
315
+
316
+ s = queue.stats()
317
+ assert s["total"] == 2
318
+ assert s["exhausted"] == 1
319
+ assert s["pending"] == 1
320
+
321
+ def test_stats_empty_queue(self, queue: ErrorQueue) -> None:
322
+ """stats() on an empty queue returns zeros."""
323
+ s = queue.stats()
324
+ assert s["total"] == 0
325
+ for status in ErrorStatus:
326
+ assert s.get(status.value, 0) == 0
327
+
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # CLI smoke-tests
331
+ # ---------------------------------------------------------------------------
332
+
333
+
334
+ class TestErrorQueueCLI:
335
+ """Smoke-tests for the `skcapstone errors` CLI commands."""
336
+
337
+ def test_cli_list_empty(self, tmp_path: Path) -> None:
338
+ """errors list on empty queue exits 0 and prints 'empty'."""
339
+ from click.testing import CliRunner
340
+ from skcapstone.cli import main
341
+
342
+ runner = CliRunner()
343
+ result = runner.invoke(
344
+ main,
345
+ ["errors", "list", "--path", str(tmp_path / "eq.json")],
346
+ )
347
+ assert result.exit_code == 0
348
+ assert "empty" in result.output.lower() or "0 total" in result.output.lower() or "Queue" in result.output
349
+
350
+ def test_cli_list_shows_entry(self, tmp_path: Path) -> None:
351
+ """errors list shows an enqueued entry."""
352
+ from click.testing import CliRunner
353
+ from skcapstone.cli import main
354
+
355
+ q_path = tmp_path / "eq.json"
356
+ q = ErrorQueue(path=q_path, base_backoff=0)
357
+ q.enqueue("llm_call", {"model": "grok"}, "test error msg")
358
+
359
+ runner = CliRunner()
360
+ result = runner.invoke(main, ["errors", "list", "--path", str(q_path)])
361
+ assert result.exit_code == 0
362
+ assert "llm_call" in result.output
363
+
364
+ def test_cli_stats(self, tmp_path: Path) -> None:
365
+ """errors stats exits 0 and shows totals panel."""
366
+ from click.testing import CliRunner
367
+ from skcapstone.cli import main
368
+
369
+ q_path = tmp_path / "eq.json"
370
+ q = ErrorQueue(path=q_path, base_backoff=0)
371
+ q.enqueue("sync", {}, "err")
372
+
373
+ runner = CliRunner()
374
+ result = runner.invoke(main, ["errors", "stats", "--path", str(q_path)])
375
+ assert result.exit_code == 0
376
+ assert "Total" in result.output or "1" in result.output
377
+
378
+ def test_cli_clear_all_with_force(self, tmp_path: Path) -> None:
379
+ """errors clear --all --force removes all entries."""
380
+ from click.testing import CliRunner
381
+ from skcapstone.cli import main
382
+
383
+ q_path = tmp_path / "eq.json"
384
+ q = ErrorQueue(path=q_path, base_backoff=0)
385
+ q.enqueue("llm_call", {}, "err1")
386
+ q.enqueue("sync", {}, "err2")
387
+
388
+ runner = CliRunner()
389
+ result = runner.invoke(
390
+ main, ["errors", "clear", "--all", "--force", "--path", str(q_path)]
391
+ )
392
+ assert result.exit_code == 0
393
+ assert q.list_entries() == []
394
+
395
+ def test_cli_retry_no_args_fails(self, tmp_path: Path) -> None:
396
+ """errors retry without ENTRY_ID and without --all exits non-zero."""
397
+ from click.testing import CliRunner
398
+ from skcapstone.cli import main
399
+
400
+ runner = CliRunner()
401
+ result = runner.invoke(
402
+ main, ["errors", "retry", "--path", str(tmp_path / "eq.json")]
403
+ )
404
+ assert result.exit_code != 0 or "ENTRY_ID" in result.output or "all" in result.output