@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,441 @@
1
+ """Tests for the sovereign agent export/import bundle system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ import yaml
10
+
11
+ from skcapstone.export import (
12
+ BUNDLE_VERSION,
13
+ export_bundle,
14
+ import_bundle,
15
+ )
16
+ from skcapstone.memory_engine import list_memories, store as memory_store
17
+ from skcapstone.models import MemoryLayer
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Fixtures
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ @pytest.fixture
26
+ def agent_home(tmp_path: Path) -> Path:
27
+ """Minimal agent home with required directory structure."""
28
+ home = tmp_path / ".skcapstone"
29
+ home.mkdir()
30
+ return home
31
+
32
+
33
+ @pytest.fixture
34
+ def populated_home(agent_home: Path) -> Path:
35
+ """Agent home pre-populated with identity, config, soul, memories, conversations."""
36
+ # Identity
37
+ identity_dir = agent_home / "identity"
38
+ identity_dir.mkdir()
39
+ (identity_dir / "identity.json").write_text(
40
+ json.dumps({"name": "test-agent", "fingerprint": "DEADBEEF", "email": "test@example.com"}),
41
+ encoding="utf-8",
42
+ )
43
+
44
+ # Config
45
+ config_dir = agent_home / "config"
46
+ config_dir.mkdir()
47
+ (config_dir / "config.yaml").write_text(
48
+ yaml.safe_dump({"agent_name": "test-agent", "auto_rehydrate": False}),
49
+ encoding="utf-8",
50
+ )
51
+
52
+ # Soul
53
+ soul_dir = agent_home / "soul"
54
+ soul_dir.mkdir()
55
+ (soul_dir / "base.json").write_text(
56
+ json.dumps({"name": "base", "display_name": "Base Soul"}), encoding="utf-8"
57
+ )
58
+ installed_dir = soul_dir / "installed"
59
+ installed_dir.mkdir()
60
+ (installed_dir / "lumina.json").write_text(
61
+ json.dumps({"name": "lumina", "display_name": "Lumina", "vibe": "warm"}),
62
+ encoding="utf-8",
63
+ )
64
+
65
+ # Memories
66
+ memory_store(agent_home, "Sovereign memory one", tags=["test"], importance=0.6)
67
+ memory_store(agent_home, "Sovereign memory two", tags=["core"], importance=0.9)
68
+
69
+ # Conversations
70
+ conv_dir = agent_home / "conversations"
71
+ conv_dir.mkdir()
72
+ (conv_dir / "peer-alice.json").write_text(
73
+ json.dumps([
74
+ {"role": "user", "content": "Hello", "timestamp": "2026-03-01T10:00:00+00:00"},
75
+ {"role": "assistant", "content": "Hi there!", "timestamp": "2026-03-01T10:00:01+00:00"},
76
+ ]),
77
+ encoding="utf-8",
78
+ )
79
+
80
+ return agent_home
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Test: export_bundle structure
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ class TestExportBundle:
89
+ """Tests for export_bundle()."""
90
+
91
+ def test_export_returns_dict_with_required_keys(self, populated_home: Path):
92
+ """Exported bundle must contain all required top-level keys."""
93
+ bundle = export_bundle(populated_home)
94
+
95
+ assert isinstance(bundle, dict)
96
+ for key in ("bundle_version", "exported_at", "agent_name", "skcapstone_version",
97
+ "identity", "config", "soul", "memories", "conversations"):
98
+ assert key in bundle, f"Missing key: {key}"
99
+
100
+ def test_bundle_version_is_correct(self, agent_home: Path):
101
+ """bundle_version must equal BUNDLE_VERSION constant."""
102
+ bundle = export_bundle(agent_home)
103
+ assert bundle["bundle_version"] == BUNDLE_VERSION
104
+
105
+ def test_export_includes_identity(self, populated_home: Path):
106
+ """Identity section must include the agent fingerprint."""
107
+ bundle = export_bundle(populated_home)
108
+ assert bundle["identity"]["fingerprint"] == "DEADBEEF"
109
+ assert bundle["identity"]["name"] == "test-agent"
110
+
111
+ def test_export_includes_config(self, populated_home: Path):
112
+ """Config section must include the agent name."""
113
+ bundle = export_bundle(populated_home)
114
+ assert bundle["config"]["agent_name"] == "test-agent"
115
+
116
+ def test_export_includes_soul(self, populated_home: Path):
117
+ """Soul section must include base soul and installed overlays."""
118
+ bundle = export_bundle(populated_home)
119
+ soul = bundle["soul"]
120
+ assert soul["base"]["name"] == "base"
121
+ assert "lumina" in soul["installed"]
122
+ assert soul["installed"]["lumina"]["vibe"] == "warm"
123
+
124
+ def test_export_includes_memories(self, populated_home: Path):
125
+ """Memories list must contain all stored memories."""
126
+ bundle = export_bundle(populated_home)
127
+ assert len(bundle["memories"]) == 2
128
+ contents = {m["content"] for m in bundle["memories"]}
129
+ assert "Sovereign memory one" in contents
130
+ assert "Sovereign memory two" in contents
131
+
132
+ def test_export_includes_conversations(self, populated_home: Path):
133
+ """Conversations dict must include peer histories."""
134
+ bundle = export_bundle(populated_home)
135
+ assert "peer-alice" in bundle["conversations"]
136
+ msgs = bundle["conversations"]["peer-alice"]
137
+ assert len(msgs) == 2
138
+ assert msgs[0]["role"] == "user"
139
+ assert msgs[0]["content"] == "Hello"
140
+
141
+ def test_export_is_json_serializable(self, populated_home: Path):
142
+ """The bundle must be fully JSON serializable without errors."""
143
+ bundle = export_bundle(populated_home)
144
+ serialized = json.dumps(bundle)
145
+ assert len(serialized) > 0
146
+ # Round-trip: must be parseable back
147
+ parsed = json.loads(serialized)
148
+ assert parsed["bundle_version"] == BUNDLE_VERSION
149
+
150
+ def test_export_empty_home(self, agent_home: Path):
151
+ """Export from an empty agent home must succeed with empty sections."""
152
+ bundle = export_bundle(agent_home)
153
+ assert bundle["identity"] == {}
154
+ assert bundle["config"] == {}
155
+ assert bundle["memories"] == []
156
+ assert bundle["conversations"] == {}
157
+
158
+ def test_export_agent_name_from_identity(self, populated_home: Path):
159
+ """Agent name is read from identity.json."""
160
+ bundle = export_bundle(populated_home)
161
+ assert bundle["agent_name"] == "test-agent"
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # Test: import_bundle memories
166
+ # ---------------------------------------------------------------------------
167
+
168
+
169
+ class TestImportBundleMemories:
170
+ """Tests for memory import in import_bundle()."""
171
+
172
+ def test_import_restores_memories(self, agent_home: Path, populated_home: Path):
173
+ """Importing a bundle should restore all memories into target home."""
174
+ bundle = export_bundle(populated_home)
175
+
176
+ target = agent_home.parent / "target"
177
+ target.mkdir()
178
+
179
+ result = import_bundle(target, bundle)
180
+ assert result["memories_imported"] == 2
181
+
182
+ memories = list_memories(target)
183
+ contents = {m.content for m in memories}
184
+ assert "Sovereign memory one" in contents
185
+ assert "Sovereign memory two" in contents
186
+
187
+ def test_import_is_idempotent_for_memories(self, agent_home: Path, populated_home: Path):
188
+ """Re-importing the same bundle should not duplicate memories."""
189
+ bundle = export_bundle(populated_home)
190
+
191
+ target = agent_home.parent / "target2"
192
+ target.mkdir()
193
+
194
+ first = import_bundle(target, bundle)
195
+ second = import_bundle(target, bundle)
196
+
197
+ assert first["memories_imported"] == 2
198
+ assert second["memories_imported"] == 0 # already present
199
+
200
+ memories = list_memories(target)
201
+ assert len(memories) == 2
202
+
203
+ def test_import_preserves_existing_memories(self, populated_home: Path):
204
+ """Import should not overwrite memories already in the target."""
205
+ # Pre-store a memory in the target
206
+ pre_entry = memory_store(populated_home, "Pre-existing memory")
207
+
208
+ # Export from a second home
209
+ source = populated_home.parent / "source"
210
+ source.mkdir()
211
+ memory_store(source, "New from source", tags=["new"])
212
+ bundle = export_bundle(source)
213
+
214
+ result = import_bundle(populated_home, bundle)
215
+ assert result["memories_imported"] == 1
216
+
217
+ memories = list_memories(populated_home)
218
+ contents = {m.content for m in memories}
219
+ assert "Pre-existing memory" in contents
220
+ assert "New from source" in contents
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # Test: import_bundle conversations
225
+ # ---------------------------------------------------------------------------
226
+
227
+
228
+ class TestImportBundleConversations:
229
+ """Tests for conversation import in import_bundle()."""
230
+
231
+ def test_import_restores_conversations(self, agent_home: Path, populated_home: Path):
232
+ """Importing a bundle should restore conversation histories."""
233
+ bundle = export_bundle(populated_home)
234
+
235
+ target = agent_home.parent / "conv-target"
236
+ target.mkdir()
237
+
238
+ result = import_bundle(target, bundle)
239
+ assert result["conversations_imported"] == 2
240
+
241
+ conv_file = target / "conversations" / "peer-alice.json"
242
+ assert conv_file.exists()
243
+ messages = json.loads(conv_file.read_text())
244
+ assert len(messages) == 2
245
+
246
+ def test_import_merges_conversations(self, tmp_path: Path, populated_home: Path):
247
+ """Import should merge new messages without duplicating existing ones."""
248
+ # Create a fresh target with one existing message for peer-alice
249
+ target = tmp_path / "merge-target"
250
+ target.mkdir()
251
+ conv_dir = target / "conversations"
252
+ conv_dir.mkdir()
253
+ existing = [{"role": "user", "content": "Hello", "timestamp": "2026-03-01T10:00:00+00:00"}]
254
+ (conv_dir / "peer-alice.json").write_text(json.dumps(existing), encoding="utf-8")
255
+
256
+ bundle = export_bundle(populated_home)
257
+ result = import_bundle(target, bundle)
258
+
259
+ # Should add only the "assistant" message (second msg), not re-add "Hello"
260
+ assert result["conversations_imported"] == 1
261
+
262
+ messages = json.loads((conv_dir / "peer-alice.json").read_text())
263
+ assert len(messages) == 2
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # Test: import_bundle identity / config / soul
268
+ # ---------------------------------------------------------------------------
269
+
270
+
271
+ class TestImportBundleFiles:
272
+ """Tests for identity, config, and soul file import."""
273
+
274
+ def test_import_writes_identity_when_absent(self, tmp_path: Path, populated_home: Path):
275
+ """Identity should be written when the target file does not exist."""
276
+ target = tmp_path / "fresh-agent"
277
+ target.mkdir()
278
+ bundle = export_bundle(populated_home)
279
+ result = import_bundle(target, bundle)
280
+ assert result["identity_written"] is True
281
+ identity = json.loads((target / "identity" / "identity.json").read_text())
282
+ assert identity["fingerprint"] == "DEADBEEF"
283
+
284
+ def test_import_skips_identity_when_present(self, populated_home: Path):
285
+ """Identity must not be overwritten by default when already present."""
286
+ bundle = export_bundle(populated_home)
287
+ # Modify bundle identity
288
+ bundle["identity"]["fingerprint"] = "MODIFIED"
289
+ result = import_bundle(populated_home, bundle)
290
+ assert result["identity_written"] is False
291
+ # Original fingerprint is preserved
292
+ identity = json.loads((populated_home / "identity" / "identity.json").read_text())
293
+ assert identity["fingerprint"] == "DEADBEEF"
294
+
295
+ def test_import_overwrites_identity_with_flag(self, populated_home: Path):
296
+ """--overwrite-identity should force-write identity."""
297
+ bundle = export_bundle(populated_home)
298
+ bundle["identity"]["fingerprint"] = "NEWPRINT"
299
+ result = import_bundle(populated_home, bundle, overwrite_identity=True)
300
+ assert result["identity_written"] is True
301
+ identity = json.loads((populated_home / "identity" / "identity.json").read_text())
302
+ assert identity["fingerprint"] == "NEWPRINT"
303
+
304
+ def test_import_writes_soul_files(self, tmp_path: Path, populated_home: Path):
305
+ """Soul base and installed overlays should be written to a new home."""
306
+ target = tmp_path / "soul-target"
307
+ target.mkdir()
308
+ bundle = export_bundle(populated_home)
309
+ result = import_bundle(target, bundle)
310
+ assert result["soul_files_written"] >= 1
311
+ assert (target / "soul" / "base.json").exists()
312
+ assert (target / "soul" / "installed" / "lumina.json").exists()
313
+
314
+ def test_import_writes_config(self, tmp_path: Path, populated_home: Path):
315
+ """Config should be written to a new home."""
316
+ target = tmp_path / "config-target"
317
+ target.mkdir()
318
+ bundle = export_bundle(populated_home)
319
+ result = import_bundle(target, bundle)
320
+ assert result["config_written"] is True
321
+ config = yaml.safe_load((target / "config" / "config.yaml").read_text())
322
+ assert config["agent_name"] == "test-agent"
323
+
324
+
325
+ # ---------------------------------------------------------------------------
326
+ # Test: import_bundle validation
327
+ # ---------------------------------------------------------------------------
328
+
329
+
330
+ class TestImportBundleValidation:
331
+ """Tests for bundle validation in import_bundle()."""
332
+
333
+ def test_import_rejects_non_dict(self, agent_home: Path):
334
+ """A non-dict bundle should raise ValueError."""
335
+ with pytest.raises(ValueError, match="JSON object"):
336
+ import_bundle(agent_home, []) # type: ignore[arg-type]
337
+
338
+ def test_import_rejects_missing_version(self, agent_home: Path):
339
+ """A bundle with no bundle_version should raise ValueError."""
340
+ with pytest.raises(ValueError, match="bundle_version"):
341
+ import_bundle(agent_home, {"memories": []})
342
+
343
+ def test_import_rejects_wrong_version(self, agent_home: Path):
344
+ """A bundle with a wrong version should raise ValueError."""
345
+ with pytest.raises(ValueError, match="Unsupported bundle_version"):
346
+ import_bundle(agent_home, {"bundle_version": 99, "memories": []})
347
+
348
+ def test_import_empty_bundle_succeeds(self, agent_home: Path):
349
+ """A minimal valid bundle with no data should import without error."""
350
+ minimal = {"bundle_version": BUNDLE_VERSION}
351
+ result = import_bundle(agent_home, minimal)
352
+ assert result["memories_imported"] == 0
353
+ assert result["conversations_imported"] == 0
354
+ assert result["errors"] == []
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Test: CLI via Click test runner
359
+ # ---------------------------------------------------------------------------
360
+
361
+
362
+ class TestExportCLI:
363
+ """Tests for skcapstone export/import via Click test runner."""
364
+
365
+ def test_export_command_stdout(self, populated_home: Path):
366
+ """skcapstone export should write valid JSON to stdout."""
367
+ from click.testing import CliRunner
368
+ from skcapstone.cli import main
369
+
370
+ runner = CliRunner()
371
+ result = runner.invoke(main, ["--agent", "", "export", "--home", str(populated_home)])
372
+ assert result.exit_code == 0, result.output
373
+ bundle = json.loads(result.output)
374
+ assert bundle["bundle_version"] == BUNDLE_VERSION
375
+ assert len(bundle["memories"]) == 2
376
+
377
+ def test_export_command_to_file(self, populated_home: Path, tmp_path: Path):
378
+ """skcapstone export --output should write a JSON file."""
379
+ from click.testing import CliRunner
380
+ from skcapstone.cli import main
381
+
382
+ out_file = tmp_path / "bundle.json"
383
+ runner = CliRunner()
384
+ result = runner.invoke(
385
+ main,
386
+ ["--agent", "", "export", "--home", str(populated_home), "--output", str(out_file)],
387
+ )
388
+ assert result.exit_code == 0, result.output
389
+ assert out_file.exists()
390
+ bundle = json.loads(out_file.read_text())
391
+ assert bundle["bundle_version"] == BUNDLE_VERSION
392
+
393
+ def test_import_command(self, populated_home: Path, tmp_path: Path):
394
+ """skcapstone import should restore memories from a bundle file."""
395
+ from click.testing import CliRunner
396
+ from skcapstone.cli import main
397
+
398
+ # Export first
399
+ out_file = tmp_path / "bundle.json"
400
+ runner = CliRunner()
401
+ runner.invoke(
402
+ main,
403
+ ["--agent", "", "export", "--home", str(populated_home), "--output", str(out_file)],
404
+ )
405
+
406
+ # Import into a fresh home
407
+ new_home = tmp_path / "new_agent"
408
+ new_home.mkdir()
409
+ result = runner.invoke(
410
+ main,
411
+ ["--agent", "", "import", str(out_file), "--home", str(new_home)],
412
+ )
413
+ assert result.exit_code == 0, result.output
414
+ assert "Import complete" in result.output
415
+
416
+ memories = list_memories(new_home)
417
+ assert len(memories) == 2
418
+
419
+ def test_export_nonexistent_home_fails(self, tmp_path: Path):
420
+ """export from a non-existent home should exit with error."""
421
+ from click.testing import CliRunner
422
+ from skcapstone.cli import main
423
+
424
+ runner = CliRunner()
425
+ result = runner.invoke(
426
+ main,
427
+ ["--agent", "", "export", "--home", str(tmp_path / "does-not-exist")],
428
+ )
429
+ assert result.exit_code != 0
430
+
431
+ def test_import_nonexistent_bundle_fails(self, agent_home: Path):
432
+ """import from a missing file should exit with error."""
433
+ from click.testing import CliRunner
434
+ from skcapstone.cli import main
435
+
436
+ runner = CliRunner()
437
+ result = runner.invoke(
438
+ main,
439
+ ["--agent", "", "import", "/tmp/nonexistent_bundle.json", "--home", str(agent_home)],
440
+ )
441
+ assert result.exit_code != 0
@@ -0,0 +1,219 @@
1
+ """Tests for the FallbackTracker — graceful degradation logging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from skcapstone.fallback_tracker import FallbackEvent, FallbackTracker, get_tracker
11
+
12
+
13
+ def _event(
14
+ primary="gpt-4o",
15
+ primary_backend="openai",
16
+ fallback_model="llama3.2",
17
+ fallback_backend="ollama",
18
+ reason="primary failed",
19
+ success=True,
20
+ ) -> FallbackEvent:
21
+ return FallbackEvent(
22
+ primary_model=primary,
23
+ primary_backend=primary_backend,
24
+ fallback_model=fallback_model,
25
+ fallback_backend=fallback_backend,
26
+ reason=reason,
27
+ success=success,
28
+ )
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # FallbackEvent — model tests
33
+ # ---------------------------------------------------------------------------
34
+
35
+
36
+ class TestFallbackEvent:
37
+ def test_defaults_set_timestamp(self):
38
+ """Timestamp is populated automatically."""
39
+ evt = _event()
40
+ assert evt.timestamp # non-empty string
41
+ assert "T" in evt.timestamp # ISO format
42
+
43
+ def test_fields_round_trip(self):
44
+ """model_dump() and re-instantiation preserve all fields."""
45
+ evt = _event(reason="timeout", success=False)
46
+ dumped = evt.model_dump()
47
+ restored = FallbackEvent(**dumped)
48
+ assert restored.reason == "timeout"
49
+ assert restored.success is False
50
+ assert restored.primary_model == "gpt-4o"
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # FallbackTracker — happy path
55
+ # ---------------------------------------------------------------------------
56
+
57
+
58
+ class TestFallbackTrackerHappyPath:
59
+ def test_record_and_load(self, tmp_path):
60
+ """Record an event, then load it back."""
61
+ tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
62
+ evt = _event()
63
+ tracker.record(evt)
64
+
65
+ loaded = tracker.load_events()
66
+ assert len(loaded) == 1
67
+ assert loaded[0].primary_model == "gpt-4o"
68
+ assert loaded[0].fallback_backend == "ollama"
69
+
70
+ def test_multiple_events_newest_first(self, tmp_path):
71
+ """load_events returns events newest-first."""
72
+ tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
73
+ tracker.record(_event(reason="first"))
74
+ tracker.record(_event(reason="second"))
75
+ tracker.record(_event(reason="third"))
76
+
77
+ loaded = tracker.load_events()
78
+ assert loaded[0].reason == "third"
79
+ assert loaded[1].reason == "second"
80
+ assert loaded[2].reason == "first"
81
+
82
+ def test_limit_parameter(self, tmp_path):
83
+ """limit= caps the returned events."""
84
+ tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
85
+ for i in range(5):
86
+ tracker.record(_event(reason=f"event-{i}"))
87
+
88
+ assert len(tracker.load_events(limit=2)) == 2
89
+ assert len(tracker.load_events(limit=0)) == 5
90
+
91
+ def test_file_is_valid_json(self, tmp_path):
92
+ """The written file is valid JSON list."""
93
+ path = tmp_path / "fallbacks.json"
94
+ tracker = FallbackTracker(path=path)
95
+ tracker.record(_event())
96
+
97
+ data = json.loads(path.read_text())
98
+ assert isinstance(data, list)
99
+ assert len(data) == 1
100
+ assert data[0]["primary_model"] == "gpt-4o"
101
+
102
+ def test_success_and_failure_events(self, tmp_path):
103
+ """success=True and success=False events are both stored."""
104
+ tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
105
+ tracker.record(_event(success=True, reason="worked"))
106
+ tracker.record(_event(success=False, reason="failed"))
107
+
108
+ events = tracker.load_events()
109
+ successes = [e for e in events if e.success]
110
+ failures = [e for e in events if not e.success]
111
+ assert len(successes) == 1
112
+ assert len(failures) == 1
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # FallbackTracker — edge cases
117
+ # ---------------------------------------------------------------------------
118
+
119
+
120
+ class TestFallbackTrackerEdgeCases:
121
+ def test_missing_file_returns_empty(self, tmp_path):
122
+ """load_events on a non-existent file returns []."""
123
+ tracker = FallbackTracker(path=tmp_path / "nonexistent.json")
124
+ assert tracker.load_events() == []
125
+
126
+ def test_corrupt_file_returns_empty(self, tmp_path):
127
+ """A corrupt JSON file is treated as empty (no exception raised)."""
128
+ path = tmp_path / "fallbacks.json"
129
+ path.write_text("not valid json!!!", encoding="utf-8")
130
+
131
+ tracker = FallbackTracker(path=path)
132
+ assert tracker.load_events() == []
133
+
134
+ def test_max_events_pruning(self, tmp_path):
135
+ """Old events are pruned when max_events is exceeded."""
136
+ tracker = FallbackTracker(path=tmp_path / "fallbacks.json", max_events=3)
137
+ for i in range(5):
138
+ tracker.record(_event(reason=f"e{i}"))
139
+
140
+ events = tracker.load_events()
141
+ assert len(events) == 3
142
+ # Newest three should be retained (newest-first order)
143
+ reasons = [e.reason for e in events]
144
+ assert "e4" in reasons
145
+ assert "e3" in reasons
146
+ assert "e2" in reasons
147
+ assert "e0" not in reasons
148
+
149
+ def test_clear_removes_all_events(self, tmp_path):
150
+ """clear() deletes all events and returns count."""
151
+ tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
152
+ for i in range(4):
153
+ tracker.record(_event(reason=f"e{i}"))
154
+
155
+ count = tracker.clear()
156
+ assert count == 4
157
+ assert tracker.load_events() == []
158
+
159
+ def test_clear_on_empty_returns_zero(self, tmp_path):
160
+ """clear() on an empty store returns 0."""
161
+ tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
162
+ assert tracker.clear() == 0
163
+
164
+ def test_parent_dir_created_automatically(self, tmp_path):
165
+ """Missing parent directories are created on first write."""
166
+ nested = tmp_path / "a" / "b" / "c" / "fallbacks.json"
167
+ tracker = FallbackTracker(path=nested)
168
+ tracker.record(_event())
169
+ assert nested.exists()
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # FallbackTracker — thread safety
174
+ # ---------------------------------------------------------------------------
175
+
176
+
177
+ class TestFallbackTrackerConcurrency:
178
+ def test_concurrent_writes(self, tmp_path):
179
+ """Concurrent record() calls from multiple threads don't corrupt the file."""
180
+ import threading
181
+
182
+ tracker = FallbackTracker(path=tmp_path / "fallbacks.json")
183
+ errors: list[Exception] = []
184
+
185
+ def write_events():
186
+ try:
187
+ for i in range(10):
188
+ tracker.record(_event(reason=f"thread-{i}"))
189
+ except Exception as exc: # noqa: BLE001
190
+ errors.append(exc)
191
+
192
+ threads = [threading.Thread(target=write_events) for _ in range(4)]
193
+ for t in threads:
194
+ t.start()
195
+ for t in threads:
196
+ t.join()
197
+
198
+ assert not errors, f"Thread errors: {errors}"
199
+ events = tracker.load_events()
200
+ # max_events default is 1000, 4*10=40 events — all should be present
201
+ assert len(events) == 40
202
+
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # get_tracker singleton
206
+ # ---------------------------------------------------------------------------
207
+
208
+
209
+ class TestGetTrackerSingleton:
210
+ def test_same_instance_returned(self):
211
+ """get_tracker() returns the same object on repeated calls."""
212
+ t1 = get_tracker()
213
+ t2 = get_tracker()
214
+ assert t1 is t2
215
+
216
+ def test_singleton_is_fallback_tracker(self):
217
+ """Singleton is a FallbackTracker instance."""
218
+ tracker = get_tracker()
219
+ assert isinstance(tracker, FallbackTracker)