@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,397 @@
1
+ """Tests for the Team Engine — deployment orchestration for agent teams."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any, Dict
9
+ from unittest.mock import MagicMock
10
+
11
+ import pytest
12
+
13
+ from skcapstone.blueprints.schema import (
14
+ AgentRole,
15
+ AgentSpec,
16
+ BlueprintManifest,
17
+ ProviderType,
18
+ )
19
+ from skcapstone.team_engine import (
20
+ AgentStatus,
21
+ DeployedAgent,
22
+ ProviderBackend,
23
+ TeamDeployment,
24
+ TeamEngine,
25
+ )
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Fixtures
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def _make_blueprint(
34
+ agents: dict[str, dict] | None = None,
35
+ name: str = "test-team",
36
+ ) -> BlueprintManifest:
37
+ """Create a minimal BlueprintManifest for testing."""
38
+ if agents is None:
39
+ agents = {
40
+ "leader": {"role": "manager", "model": "reason"},
41
+ "worker": {"role": "worker", "model": "fast"},
42
+ }
43
+ specs = {}
44
+ for key, kwargs in agents.items():
45
+ specs[key] = AgentSpec(**kwargs)
46
+ return BlueprintManifest(
47
+ name=name,
48
+ slug=name,
49
+ version="1.0",
50
+ description="Test blueprint",
51
+ agents=specs,
52
+ )
53
+
54
+
55
+ class MockProvider(ProviderBackend):
56
+ """Mock provider that tracks calls."""
57
+
58
+ provider_type = ProviderType.LOCAL
59
+
60
+ def __init__(self) -> None:
61
+ self.calls: list[tuple[str, str]] = []
62
+ self.fail_on: set[str] = set()
63
+
64
+ def provision(self, agent_name: str, spec: AgentSpec, team_name: str) -> Dict[str, Any]:
65
+ self.calls.append(("provision", agent_name))
66
+ if agent_name in self.fail_on:
67
+ raise RuntimeError(f"provision failed for {agent_name}")
68
+ return {"host": "localhost", "pid": 1234}
69
+
70
+ def configure(self, agent_name: str, spec: AgentSpec, provision_result: Dict[str, Any]) -> bool:
71
+ self.calls.append(("configure", agent_name))
72
+ if agent_name in self.fail_on:
73
+ raise RuntimeError(f"configure failed for {agent_name}")
74
+ return True
75
+
76
+ def start(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
77
+ self.calls.append(("start", agent_name))
78
+ if agent_name in self.fail_on:
79
+ raise RuntimeError(f"start failed for {agent_name}")
80
+ return True
81
+
82
+ def stop(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
83
+ self.calls.append(("stop", agent_name))
84
+ return True
85
+
86
+ def destroy(self, agent_name: str, provision_result: Dict[str, Any]) -> bool:
87
+ self.calls.append(("destroy", agent_name))
88
+ return True
89
+
90
+ def health_check(self, agent_name: str, provision_result: Dict[str, Any]) -> AgentStatus:
91
+ self.calls.append(("health_check", agent_name))
92
+ return AgentStatus.RUNNING
93
+
94
+
95
+ @pytest.fixture
96
+ def home(tmp_path: Path) -> Path:
97
+ """Create a minimal agent home directory."""
98
+ (tmp_path / "deployments").mkdir()
99
+ (tmp_path / "comms").mkdir()
100
+ return tmp_path
101
+
102
+
103
+ @pytest.fixture
104
+ def provider() -> MockProvider:
105
+ """Create a mock provider backend."""
106
+ return MockProvider()
107
+
108
+
109
+ @pytest.fixture
110
+ def engine(home: Path, provider: MockProvider) -> TeamEngine:
111
+ """Create a TeamEngine with mock provider."""
112
+ return TeamEngine(home=home, provider=provider, comms_root=home / "comms")
113
+
114
+
115
+ @pytest.fixture
116
+ def blueprint() -> BlueprintManifest:
117
+ """Create a basic two-agent blueprint."""
118
+ return _make_blueprint()
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Model tests
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ class TestModels:
127
+ """Tests for team engine data models."""
128
+
129
+ def test_agent_status_values(self) -> None:
130
+ """AgentStatus has expected values."""
131
+ assert AgentStatus.PENDING.value == "pending"
132
+ assert AgentStatus.RUNNING.value == "running"
133
+ assert AgentStatus.FAILED.value == "failed"
134
+
135
+ def test_deployed_agent_defaults(self) -> None:
136
+ """DeployedAgent has sensible defaults."""
137
+ agent = DeployedAgent(
138
+ name="test",
139
+ instance_id="inst-1",
140
+ blueprint_slug="test-bp",
141
+ agent_spec_key="worker",
142
+ )
143
+ assert agent.status == AgentStatus.PENDING
144
+ assert agent.host is None
145
+ assert agent.error is None
146
+
147
+ def test_team_deployment_defaults(self) -> None:
148
+ """TeamDeployment has sensible defaults."""
149
+ dep = TeamDeployment(
150
+ deployment_id="dep-1",
151
+ blueprint_slug="test-bp",
152
+ team_name="test-team",
153
+ provider=ProviderType.LOCAL,
154
+ )
155
+ assert dep.status == "deploying"
156
+ assert dep.agents == {}
157
+
158
+ def test_team_deployment_serialization(self) -> None:
159
+ """TeamDeployment serializes to/from JSON."""
160
+ dep = TeamDeployment(
161
+ deployment_id="dep-1",
162
+ blueprint_slug="test-bp",
163
+ team_name="test-team",
164
+ provider=ProviderType.LOCAL,
165
+ agents={
166
+ "agent-a": DeployedAgent(
167
+ name="agent-a",
168
+ instance_id="i-1",
169
+ blueprint_slug="test-bp",
170
+ agent_spec_key="worker",
171
+ ),
172
+ },
173
+ )
174
+ data = dep.model_dump_json()
175
+ restored = TeamDeployment.model_validate_json(data)
176
+ assert restored.deployment_id == "dep-1"
177
+ assert "agent-a" in restored.agents
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Initialization
182
+ # ---------------------------------------------------------------------------
183
+
184
+
185
+ class TestInitialization:
186
+ """Tests for TeamEngine setup."""
187
+
188
+ def test_engine_creates_deployments_dir(self, tmp_path: Path) -> None:
189
+ """TeamEngine uses given home directory."""
190
+ engine = TeamEngine(home=tmp_path, provider=None)
191
+ assert engine._deployments_dir == tmp_path / "deployments"
192
+
193
+ def test_engine_without_provider(self, home: Path) -> None:
194
+ """TeamEngine can be created without a provider (dry-run)."""
195
+ engine = TeamEngine(home=home, provider=None)
196
+ assert engine._provider is None
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Deploy order resolution
201
+ # ---------------------------------------------------------------------------
202
+
203
+
204
+ class TestDeployOrder:
205
+ """Tests for dependency-ordered deployment waves."""
206
+
207
+ def test_no_dependencies(self) -> None:
208
+ """Agents without dependencies deploy in one wave."""
209
+ bp = _make_blueprint({
210
+ "a": {"role": "worker", "model": "fast"},
211
+ "b": {"role": "worker", "model": "fast"},
212
+ })
213
+ waves = TeamEngine.resolve_deploy_order(bp)
214
+ assert len(waves) == 1
215
+ assert set(waves[0]) == {"a", "b"}
216
+
217
+ def test_simple_dependency(self) -> None:
218
+ """Agent with dependency deploys after its dependency."""
219
+ bp = _make_blueprint({
220
+ "leader": {"role": "manager", "model": "reason"},
221
+ "worker": {"role": "worker", "model": "fast", "depends_on": ["leader"]},
222
+ })
223
+ waves = TeamEngine.resolve_deploy_order(bp)
224
+ assert len(waves) == 2
225
+ assert "leader" in waves[0]
226
+ assert "worker" in waves[1]
227
+
228
+ def test_diamond_dependency(self) -> None:
229
+ """Diamond dependency graph resolves correctly."""
230
+ bp = _make_blueprint({
231
+ "base": {"role": "manager", "model": "reason"},
232
+ "mid-a": {"role": "worker", "model": "fast", "depends_on": ["base"]},
233
+ "mid-b": {"role": "worker", "model": "fast", "depends_on": ["base"]},
234
+ "top": {"role": "worker", "model": "fast", "depends_on": ["mid-a", "mid-b"]},
235
+ })
236
+ waves = TeamEngine.resolve_deploy_order(bp)
237
+ assert len(waves) == 3
238
+ assert "base" in waves[0]
239
+ assert set(waves[1]) == {"mid-a", "mid-b"}
240
+ assert "top" in waves[2]
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Deployment
245
+ # ---------------------------------------------------------------------------
246
+
247
+
248
+ class TestDeploy:
249
+ """Tests for deployment orchestration."""
250
+
251
+ def test_deploy_creates_agents(
252
+ self, engine: TeamEngine, blueprint: BlueprintManifest,
253
+ ) -> None:
254
+ """Deploy creates agents for each spec in blueprint."""
255
+ dep = engine.deploy(blueprint)
256
+ assert len(dep.agents) == 2
257
+ assert "leader" in dep.agents or any(
258
+ "leader" in a.agent_spec_key for a in dep.agents.values()
259
+ )
260
+
261
+ def test_deploy_calls_provider(
262
+ self, engine: TeamEngine, provider: MockProvider, blueprint: BlueprintManifest,
263
+ ) -> None:
264
+ """Deploy calls provision/configure/start on provider."""
265
+ engine.deploy(blueprint)
266
+ actions = [action for action, _ in provider.calls]
267
+ assert "provision" in actions
268
+ assert "configure" in actions
269
+ assert "start" in actions
270
+
271
+ def test_deploy_saves_state(
272
+ self, engine: TeamEngine, blueprint: BlueprintManifest, home: Path,
273
+ ) -> None:
274
+ """Deploy persists state to disk."""
275
+ dep = engine.deploy(blueprint)
276
+ state_file = home / "deployments" / f"{dep.deployment_id}.json"
277
+ assert state_file.exists()
278
+
279
+ def test_deploy_returns_deployment(
280
+ self, engine: TeamEngine, blueprint: BlueprintManifest,
281
+ ) -> None:
282
+ """Deploy returns a TeamDeployment."""
283
+ dep = engine.deploy(blueprint)
284
+ assert isinstance(dep, TeamDeployment)
285
+ assert dep.blueprint_slug == "test-team"
286
+
287
+ def test_deploy_custom_name(
288
+ self, engine: TeamEngine, blueprint: BlueprintManifest,
289
+ ) -> None:
290
+ """Deploy accepts custom deployment name."""
291
+ dep = engine.deploy(blueprint, name="my-team")
292
+ assert dep.team_name == "my-team"
293
+
294
+ def test_deploy_without_provider(
295
+ self, home: Path, blueprint: BlueprintManifest,
296
+ ) -> None:
297
+ """Deploy works in dry-run mode without a provider."""
298
+ engine = TeamEngine(home=home, provider=None)
299
+ dep = engine.deploy(blueprint)
300
+ # All agents should be in pending or some initial state
301
+ assert len(dep.agents) >= 1
302
+
303
+ def test_deploy_handles_agent_failure(
304
+ self, engine: TeamEngine, provider: MockProvider,
305
+ ) -> None:
306
+ """Deploy continues even if one agent fails."""
307
+ provider.fail_on.add("worker")
308
+ bp = _make_blueprint({
309
+ "leader": {"role": "manager", "model": "reason"},
310
+ "worker": {"role": "worker", "model": "fast"},
311
+ })
312
+ dep = engine.deploy(bp)
313
+ # Deployment should still complete
314
+ assert len(dep.agents) >= 1
315
+
316
+
317
+ # ---------------------------------------------------------------------------
318
+ # List / Get / Destroy
319
+ # ---------------------------------------------------------------------------
320
+
321
+
322
+ class TestLifecycle:
323
+ """Tests for listing, getting, and destroying deployments."""
324
+
325
+ def test_list_empty(self, engine: TeamEngine) -> None:
326
+ """Empty engine returns no deployments."""
327
+ assert engine.list_deployments() == []
328
+
329
+ def test_list_after_deploy(
330
+ self, engine: TeamEngine, blueprint: BlueprintManifest,
331
+ ) -> None:
332
+ """Deployed teams appear in listing."""
333
+ engine.deploy(blueprint)
334
+ deps = engine.list_deployments()
335
+ assert len(deps) == 1
336
+
337
+ def test_get_deployment(
338
+ self, engine: TeamEngine, blueprint: BlueprintManifest,
339
+ ) -> None:
340
+ """Can retrieve a deployment by ID."""
341
+ dep = engine.deploy(blueprint)
342
+ retrieved = engine.get_deployment(dep.deployment_id)
343
+ assert retrieved is not None
344
+ assert retrieved.deployment_id == dep.deployment_id
345
+
346
+ def test_get_nonexistent(self, engine: TeamEngine) -> None:
347
+ """Getting nonexistent deployment returns None."""
348
+ assert engine.get_deployment("ghost") is None
349
+
350
+ def test_destroy_deployment(
351
+ self, engine: TeamEngine, provider: MockProvider, blueprint: BlueprintManifest,
352
+ ) -> None:
353
+ """Destroy removes deployment state."""
354
+ dep = engine.deploy(blueprint)
355
+ result = engine.destroy_deployment(dep.deployment_id)
356
+ assert result is True
357
+ assert engine.get_deployment(dep.deployment_id) is None
358
+
359
+ def test_destroy_calls_provider(
360
+ self, engine: TeamEngine, provider: MockProvider, blueprint: BlueprintManifest,
361
+ ) -> None:
362
+ """Destroy calls provider.destroy on agents."""
363
+ dep = engine.deploy(blueprint)
364
+ provider.calls.clear()
365
+ engine.destroy_deployment(dep.deployment_id)
366
+ actions = [action for action, _ in provider.calls]
367
+ assert "destroy" in actions
368
+
369
+ def test_destroy_nonexistent(self, engine: TeamEngine) -> None:
370
+ """Destroying nonexistent deployment returns False."""
371
+ assert engine.destroy_deployment("ghost") is False
372
+
373
+ def test_multiple_deployments(self, engine: TeamEngine) -> None:
374
+ """Can have multiple active deployments."""
375
+ bp1 = _make_blueprint(name="team-alpha")
376
+ bp2 = _make_blueprint(name="team-bravo")
377
+ dep1 = engine.deploy(bp1)
378
+ dep2 = engine.deploy(bp2)
379
+ deps = engine.list_deployments()
380
+ assert len(deps) == 2
381
+ ids = {d.deployment_id for d in deps}
382
+ assert dep1.deployment_id in ids
383
+ assert dep2.deployment_id in ids
384
+
385
+
386
+ # ---------------------------------------------------------------------------
387
+ # Provider backend
388
+ # ---------------------------------------------------------------------------
389
+
390
+
391
+ class TestProviderBackend:
392
+ """Tests for the abstract provider interface."""
393
+
394
+ def test_abstract_methods_raise(self) -> None:
395
+ """ProviderBackend is an ABC — cannot be instantiated directly."""
396
+ with pytest.raises(TypeError):
397
+ ProviderBackend() # type: ignore[abstract]
@@ -0,0 +1,238 @@
1
+ """Tests for skcapstone.testrunner module.
2
+
3
+ Covers PackageResult, TestReport, _tail, _parse_pytest_summary,
4
+ and run_all_tests with mocked subprocess calls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from unittest.mock import patch
11
+
12
+ import pytest
13
+
14
+ from skcapstone.testrunner import (
15
+ ECOSYSTEM_PACKAGES,
16
+ PackageResult,
17
+ TestReport,
18
+ _parse_pytest_summary,
19
+ _tail,
20
+ run_all_tests,
21
+ )
22
+
23
+
24
+ # ── TestPackageResult ────────────────────────────────────────────
25
+
26
+
27
+ class TestPackageResult:
28
+ """Tests for the PackageResult dataclass."""
29
+
30
+ def test_default_values(self):
31
+ """Default numeric fields are zero, available is True."""
32
+ r = PackageResult(name="pkg")
33
+ assert r.passed == 0
34
+ assert r.failed == 0
35
+ assert r.errors == 0
36
+ assert r.skipped == 0
37
+ assert r.duration_s == 0.0
38
+ assert r.exit_code == -1
39
+ assert r.output == ""
40
+ assert r.available is True
41
+
42
+ def test_total_property(self):
43
+ """total = passed + failed + errors."""
44
+ r = PackageResult(name="pkg", passed=3, failed=2, errors=1)
45
+ assert r.total == 6
46
+
47
+ def test_success_when_all_pass(self):
48
+ """success is True when exit_code=0, failed=0, errors=0."""
49
+ r = PackageResult(name="pkg", passed=5, exit_code=0)
50
+ assert r.success is True
51
+
52
+ def test_success_false_when_failed_gt_zero(self):
53
+ """success is False when failed > 0."""
54
+ r = PackageResult(name="pkg", passed=4, failed=1, exit_code=1)
55
+ assert r.success is False
56
+
57
+ def test_success_false_when_errors_gt_zero(self):
58
+ """success is False when errors > 0 even if exit_code=0."""
59
+ r = PackageResult(name="pkg", passed=4, errors=1, exit_code=0)
60
+ assert r.success is False
61
+
62
+ def test_to_dict_serialization(self):
63
+ """to_dict returns expected keys and computed values."""
64
+ r = PackageResult(
65
+ name="demo",
66
+ passed=10,
67
+ failed=2,
68
+ errors=1,
69
+ skipped=3,
70
+ duration_s=1.456,
71
+ exit_code=1,
72
+ available=True,
73
+ )
74
+ d = r.to_dict()
75
+ assert d["name"] == "demo"
76
+ assert d["passed"] == 10
77
+ assert d["failed"] == 2
78
+ assert d["errors"] == 1
79
+ assert d["skipped"] == 3
80
+ assert d["total"] == 13 # 10 + 2 + 1
81
+ assert d["duration_s"] == 1.46 # rounded to 2 decimals
82
+ assert d["success"] is False
83
+ assert d["available"] is True
84
+
85
+
86
+ # ── TestTestReport ───────────────────────────────────────────────
87
+
88
+
89
+ class TestTestReport:
90
+ """Tests for the TestReport dataclass."""
91
+
92
+ def test_default_values(self):
93
+ """Defaults to empty results and 0 duration."""
94
+ report = TestReport()
95
+ assert report.results == []
96
+ assert report.duration_s == 0.0
97
+
98
+ def test_total_passed_aggregation(self):
99
+ """total_passed sums passed across all packages."""
100
+ report = TestReport(results=[
101
+ PackageResult(name="a", passed=3, exit_code=0),
102
+ PackageResult(name="b", passed=7, exit_code=0),
103
+ ])
104
+ assert report.total_passed == 10
105
+
106
+ def test_total_failed_aggregation(self):
107
+ """total_failed sums failed across all packages."""
108
+ report = TestReport(results=[
109
+ PackageResult(name="a", failed=1, exit_code=1),
110
+ PackageResult(name="b", failed=4, exit_code=1),
111
+ ])
112
+ assert report.total_failed == 5
113
+
114
+ def test_all_passed_when_all_succeed(self):
115
+ """all_passed is True when every available package succeeds."""
116
+ report = TestReport(results=[
117
+ PackageResult(name="a", passed=5, exit_code=0),
118
+ PackageResult(name="b", passed=3, exit_code=0),
119
+ ])
120
+ assert report.all_passed is True
121
+
122
+ def test_all_passed_false_when_one_fails(self):
123
+ """all_passed is False when at least one package fails."""
124
+ report = TestReport(results=[
125
+ PackageResult(name="a", passed=5, exit_code=0),
126
+ PackageResult(name="b", passed=2, failed=1, exit_code=1),
127
+ ])
128
+ assert report.all_passed is False
129
+
130
+ def test_packages_tested_counts_only_available(self):
131
+ """packages_tested counts only packages with available=True."""
132
+ report = TestReport(results=[
133
+ PackageResult(name="a", passed=5, exit_code=0, available=True),
134
+ PackageResult(name="b", available=False),
135
+ PackageResult(name="c", passed=2, exit_code=0, available=True),
136
+ ])
137
+ assert report.packages_tested == 2
138
+
139
+ def test_to_dict_serialization(self):
140
+ """to_dict returns all top-level keys and nested package dicts."""
141
+ report = TestReport(
142
+ results=[
143
+ PackageResult(name="x", passed=4, exit_code=0),
144
+ ],
145
+ duration_s=2.789,
146
+ )
147
+ d = report.to_dict()
148
+ assert d["all_passed"] is True
149
+ assert d["total_passed"] == 4
150
+ assert d["total_failed"] == 0
151
+ assert d["total_errors"] == 0
152
+ assert d["packages_tested"] == 1
153
+ assert d["duration_s"] == 2.79
154
+ assert len(d["packages"]) == 1
155
+ assert d["packages"][0]["name"] == "x"
156
+
157
+
158
+ # ── TestTail ─────────────────────────────────────────────────────
159
+
160
+
161
+ class TestTail:
162
+ """Tests for the _tail helper."""
163
+
164
+ def test_basic_tail(self):
165
+ """Returns the last N lines."""
166
+ text = "line1\nline2\nline3\nline4\nline5"
167
+ result = _tail(text, 3)
168
+ assert result == "line3\nline4\nline5"
169
+
170
+ def test_tail_more_than_available(self):
171
+ """Returns all lines when N exceeds total."""
172
+ text = "a\nb"
173
+ result = _tail(text, 10)
174
+ assert result == "a\nb"
175
+
176
+ def test_empty_string(self):
177
+ """Handles empty string without error."""
178
+ result = _tail("", 5)
179
+ assert result == ""
180
+
181
+
182
+ # ── TestParsePytestSummary ───────────────────────────────────────
183
+
184
+
185
+ class TestParsePytestSummary:
186
+ """Tests for _parse_pytest_summary."""
187
+
188
+ def test_parses_passed_only(self):
189
+ """Extracts passed count from a simple summary line."""
190
+ output = "===== 5 passed in 0.42s ====="
191
+ r = PackageResult(name="t")
192
+ _parse_pytest_summary(output, r)
193
+ assert r.passed == 5
194
+ assert r.failed == 0
195
+
196
+ def test_parses_passed_and_failed(self):
197
+ """Extracts both passed and failed counts."""
198
+ output = "===== 3 passed, 1 failed in 1.2s ====="
199
+ r = PackageResult(name="t")
200
+ _parse_pytest_summary(output, r)
201
+ assert r.passed == 3
202
+ assert r.failed == 1
203
+
204
+ def test_parses_passed_and_error(self):
205
+ """Extracts passed and error counts."""
206
+ output = "===== 2 passed, 1 error in 0.8s ====="
207
+ r = PackageResult(name="t")
208
+ _parse_pytest_summary(output, r)
209
+ assert r.passed == 2
210
+ assert r.errors == 1
211
+
212
+ def test_parses_passed_and_skipped(self):
213
+ """Extracts passed and skipped counts."""
214
+ output = "===== 5 passed, 2 skipped in 0.5s ====="
215
+ r = PackageResult(name="t")
216
+ _parse_pytest_summary(output, r)
217
+ assert r.passed == 5
218
+ assert r.skipped == 2
219
+
220
+
221
+ # ── TestRunAllTests ──────────────────────────────────────────────
222
+
223
+
224
+ class TestRunAllTests:
225
+ """Tests for run_all_tests."""
226
+
227
+ def test_missing_test_dirs_marked_unavailable(self, tmp_path: Path):
228
+ """Packages whose test dirs don't exist are marked unavailable."""
229
+ report = run_all_tests(tmp_path, packages=["skcapstone"])
230
+ pkg = report.results[0]
231
+ assert pkg.available is False
232
+ assert "not found" in pkg.output
233
+
234
+ def test_filter_by_package_names(self, tmp_path: Path):
235
+ """Only requested packages appear in the report."""
236
+ report = run_all_tests(tmp_path, packages=["skcomm", "skchat"])
237
+ names = [r.name for r in report.results]
238
+ assert names == ["skcomm", "skchat"]