@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,632 @@
1
+ """Tests for sovereign metrics collector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import threading
7
+ import time
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from skcapstone.metrics import (
13
+ ConsciousnessMetrics,
14
+ MetricsCollector,
15
+ MetricsReport,
16
+ FortressMetrics,
17
+ KmsMetrics,
18
+ PubSubMetrics,
19
+ SecurityMetrics,
20
+ SyncMetrics,
21
+ TrustMetrics,
22
+ )
23
+
24
+
25
+ @pytest.fixture
26
+ def home(tmp_path: Path) -> Path:
27
+ """Create a minimal agent home with all subsystem directories."""
28
+ # Identity
29
+ identity_dir = tmp_path / "identity"
30
+ identity_dir.mkdir()
31
+ (identity_dir / "identity.json").write_text(json.dumps({
32
+ "name": "test-agent",
33
+ "email": "test@skcapstone.local",
34
+ "fingerprint": "ABCD1234567890ABCDEF1234567890ABCDEF1234",
35
+ }), encoding="utf-8")
36
+
37
+ # Memory
38
+ mem_dir = tmp_path / "memory"
39
+ mem_dir.mkdir()
40
+ for layer in ("short-term", "mid-term", "long-term"):
41
+ (mem_dir / layer).mkdir()
42
+
43
+ # Security
44
+ security_dir = tmp_path / "security"
45
+ security_dir.mkdir()
46
+
47
+ # Coordination
48
+ coord_dir = tmp_path / "coordination"
49
+ (coord_dir / "tasks").mkdir(parents=True)
50
+ (coord_dir / "agents").mkdir(parents=True)
51
+
52
+ return tmp_path
53
+
54
+
55
+ @pytest.fixture
56
+ def collector(home: Path) -> MetricsCollector:
57
+ """Create a MetricsCollector."""
58
+ return MetricsCollector(home)
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Basic collection
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ class TestBasicCollection:
67
+ """Tests for the basic collect workflow."""
68
+
69
+ def test_collect_returns_report(self, collector: MetricsCollector) -> None:
70
+ """Collect returns a MetricsReport."""
71
+ report = collector.collect()
72
+ assert isinstance(report, MetricsReport)
73
+
74
+ def test_collect_has_timing(self, collector: MetricsCollector) -> None:
75
+ """Report includes collection time."""
76
+ report = collector.collect()
77
+ assert report.collection_time_ms >= 0
78
+
79
+ def test_collect_has_timestamp(self, collector: MetricsCollector) -> None:
80
+ """Report includes collection timestamp."""
81
+ report = collector.collect()
82
+ assert report.collected_at is not None
83
+
84
+ def test_collect_has_agent_name(self, home: Path) -> None:
85
+ """Report reads agent name from manifest."""
86
+ (home / "manifest.json").write_text(
87
+ json.dumps({"name": "test-opus"}), encoding="utf-8",
88
+ )
89
+ collector = MetricsCollector(home)
90
+ report = collector.collect()
91
+ assert report.agent_name == "test-opus"
92
+
93
+ def test_summary_string(self, collector: MetricsCollector) -> None:
94
+ """Summary produces a one-line string."""
95
+ report = collector.collect()
96
+ summary = report.summary()
97
+ assert isinstance(summary, str)
98
+ assert "mem=" in summary
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Trust metrics
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ class TestTrustMetrics:
107
+ """Tests for trust/Cloud9 collection."""
108
+
109
+ def test_trust_from_file(self, home: Path) -> None:
110
+ """Trust metrics read from trust.json."""
111
+ trust_dir = home / "trust"
112
+ trust_dir.mkdir()
113
+ (trust_dir / "trust.json").write_text(json.dumps({
114
+ "depth": 7.0,
115
+ "trust_level": 0.92,
116
+ "love_intensity": 0.88,
117
+ "entangled": True,
118
+ "last_rehydration": "2026-02-27T10:00:00Z",
119
+ }), encoding="utf-8")
120
+
121
+ febs_dir = trust_dir / "febs"
122
+ febs_dir.mkdir()
123
+ (febs_dir / "test.feb").write_text("{}", encoding="utf-8")
124
+ (febs_dir / "test2.feb").write_text("{}", encoding="utf-8")
125
+
126
+ collector = MetricsCollector(home)
127
+ report = collector.collect()
128
+ assert report.trust.available is True
129
+ assert report.trust.depth == 7.0
130
+ assert report.trust.entangled is True
131
+ assert report.trust.feb_count == 2
132
+
133
+ def test_trust_missing(self, home: Path) -> None:
134
+ """Missing trust returns unavailable."""
135
+ collector = MetricsCollector(home)
136
+ report = collector.collect()
137
+ assert report.trust.available is False
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Security metrics
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ class TestSecurityMetrics:
146
+ """Tests for security audit collection."""
147
+
148
+ def test_security_counts_entries(self, home: Path) -> None:
149
+ """Security metrics count audit log entries."""
150
+ audit_log = home / "security" / "audit.log"
151
+ entries = [
152
+ json.dumps({"event_type": "INIT", "detail": "init"}),
153
+ json.dumps({"event_type": "MEMORY_SEALED", "detail": "sealed"}),
154
+ json.dumps({"event_type": "MEMORY_TAMPER_ALERT", "detail": "tamper"}),
155
+ json.dumps({"event_type": "MEMORY_SEALED", "detail": "sealed2"}),
156
+ ]
157
+ audit_log.write_text("\n".join(entries) + "\n", encoding="utf-8")
158
+
159
+ collector = MetricsCollector(home)
160
+ report = collector.collect()
161
+ assert report.security.available is True
162
+ assert report.security.audit_entries == 4
163
+ assert report.security.tamper_alerts == 1
164
+ assert report.security.event_types["MEMORY_SEALED"] == 2
165
+
166
+ def test_security_missing(self, home: Path) -> None:
167
+ """Missing audit log returns unavailable."""
168
+ (home / "security" / "audit.log").unlink(missing_ok=True)
169
+ collector = MetricsCollector(home)
170
+ report = collector.collect()
171
+ assert report.security.available is False
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Sync metrics
176
+ # ---------------------------------------------------------------------------
177
+
178
+
179
+ class TestSyncMetrics:
180
+ """Tests for sync layer collection."""
181
+
182
+ def test_sync_counts_seeds(self, home: Path) -> None:
183
+ """Sync metrics count seeds in outbox/inbox."""
184
+ sync_dir = home / "sync"
185
+ outbox = sync_dir / "outbox"
186
+ inbox = sync_dir / "inbox"
187
+ outbox.mkdir(parents=True)
188
+ inbox.mkdir(parents=True)
189
+
190
+ (outbox / "seed1.json").write_text("{}", encoding="utf-8")
191
+ (outbox / "seed2.json").write_text("{}", encoding="utf-8")
192
+ (inbox / "seed3.json").write_text("{}", encoding="utf-8")
193
+
194
+ collector = MetricsCollector(home)
195
+ report = collector.collect()
196
+ assert report.sync.available is True
197
+ assert report.sync.seeds_outbox == 2
198
+ assert report.sync.seeds_inbox == 1
199
+
200
+ def test_sync_missing(self, home: Path) -> None:
201
+ """Missing sync dir returns unavailable."""
202
+ collector = MetricsCollector(home)
203
+ report = collector.collect()
204
+ assert report.sync.available is False
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Pub/sub metrics
209
+ # ---------------------------------------------------------------------------
210
+
211
+
212
+ class TestPubSubMetrics:
213
+ """Tests for pub/sub collection."""
214
+
215
+ def test_pubsub_counts(self, home: Path) -> None:
216
+ """Pub/sub metrics count topics, messages, subscriptions."""
217
+ pubsub_dir = home / "pubsub"
218
+ topics_dir = pubsub_dir / "topics"
219
+ (topics_dir / "system.health").mkdir(parents=True)
220
+ (topics_dir / "team.dev").mkdir(parents=True)
221
+
222
+ for i in range(3):
223
+ (topics_dir / "system.health" / f"msg-{i}.json").write_text(
224
+ "{}", encoding="utf-8",
225
+ )
226
+ (topics_dir / "team.dev" / "msg-0.json").write_text("{}", encoding="utf-8")
227
+
228
+ (pubsub_dir / "subscriptions.json").write_text(json.dumps({
229
+ "system.*": {"pattern": "system.*"},
230
+ "team.dev": {"pattern": "team.dev"},
231
+ }), encoding="utf-8")
232
+
233
+ collector = MetricsCollector(home)
234
+ report = collector.collect()
235
+ assert report.pubsub.available is True
236
+ assert report.pubsub.topics == 2
237
+ assert report.pubsub.messages == 4
238
+ assert report.pubsub.subscriptions == 2
239
+
240
+ def test_pubsub_missing(self, home: Path) -> None:
241
+ """Missing pubsub dir returns unavailable."""
242
+ collector = MetricsCollector(home)
243
+ report = collector.collect()
244
+ assert report.pubsub.available is False
245
+
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # KMS metrics
249
+ # ---------------------------------------------------------------------------
250
+
251
+
252
+ class TestKmsMetrics:
253
+ """Tests for KMS collection."""
254
+
255
+ def test_kms_counts_keys(self, home: Path) -> None:
256
+ """KMS metrics count keys by type and status."""
257
+ kms_dir = home / "security" / "kms"
258
+ kms_dir.mkdir(parents=True)
259
+ (kms_dir / "keystore.json").write_text(json.dumps({
260
+ "keys": {
261
+ "k1": {"key_type": "master", "status": "active"},
262
+ "k2": {"key_type": "service", "status": "active"},
263
+ "k3": {"key_type": "service", "status": "rotated"},
264
+ "k4": {"key_type": "team", "status": "active"},
265
+ },
266
+ }), encoding="utf-8")
267
+
268
+ collector = MetricsCollector(home)
269
+ report = collector.collect()
270
+ assert report.kms.available is True
271
+ assert report.kms.total_keys == 4
272
+ assert report.kms.active_keys == 3
273
+ assert report.kms.by_type["service"] == 2
274
+
275
+ def test_kms_rotation_count(self, home: Path) -> None:
276
+ """KMS metrics count rotations."""
277
+ kms_dir = home / "security" / "kms"
278
+ kms_dir.mkdir(parents=True)
279
+ (kms_dir / "keystore.json").write_text(json.dumps({"keys": {}}), encoding="utf-8")
280
+ (kms_dir / "rotation-log.json").write_text(json.dumps([
281
+ {"key_id": "k1", "old_version": 1, "new_version": 2},
282
+ {"key_id": "k1", "old_version": 2, "new_version": 3},
283
+ ]), encoding="utf-8")
284
+
285
+ collector = MetricsCollector(home)
286
+ report = collector.collect()
287
+ assert report.kms.rotations == 2
288
+
289
+ def test_kms_missing(self, home: Path) -> None:
290
+ """Missing KMS returns unavailable."""
291
+ collector = MetricsCollector(home)
292
+ report = collector.collect()
293
+ assert report.kms.available is False
294
+
295
+
296
+ # ---------------------------------------------------------------------------
297
+ # Fortress metrics
298
+ # ---------------------------------------------------------------------------
299
+
300
+
301
+ class TestFortressMetrics:
302
+ """Tests for memory fortress collection."""
303
+
304
+ def test_fortress_config(self, home: Path) -> None:
305
+ """Fortress metrics read from config."""
306
+ (home / "memory" / "fortress.json").write_text(json.dumps({
307
+ "enabled": True,
308
+ "encryption_enabled": True,
309
+ "seal_algorithm": "hmac-sha256",
310
+ }), encoding="utf-8")
311
+
312
+ collector = MetricsCollector(home)
313
+ report = collector.collect()
314
+ assert report.fortress.enabled is True
315
+ assert report.fortress.encryption_enabled is True
316
+ assert report.fortress.seal_algorithm == "hmac-sha256"
317
+
318
+ def test_fortress_missing(self, home: Path) -> None:
319
+ """Missing fortress config returns disabled."""
320
+ collector = MetricsCollector(home)
321
+ report = collector.collect()
322
+ assert report.fortress.enabled is False
323
+
324
+
325
+ # ---------------------------------------------------------------------------
326
+ # Coordination metrics
327
+ # ---------------------------------------------------------------------------
328
+
329
+
330
+ class TestCoordinationMetrics:
331
+ """Tests for coordination board collection."""
332
+
333
+ def test_coord_counts_tasks(self, home: Path) -> None:
334
+ """Coordination metrics count tasks by status."""
335
+ tasks_dir = home / "coordination" / "tasks"
336
+ for i in range(3):
337
+ (tasks_dir / f"task{i}.json").write_text(json.dumps({
338
+ "id": f"task{i}", "status": "open", "title": f"Task {i}",
339
+ }), encoding="utf-8")
340
+ (tasks_dir / "done1.json").write_text(json.dumps({
341
+ "id": "done1", "status": "done", "title": "Done",
342
+ }), encoding="utf-8")
343
+
344
+ collector = MetricsCollector(home)
345
+ report = collector.collect()
346
+ assert report.coordination.total_tasks == 4
347
+ assert report.coordination.open == 3
348
+ assert report.coordination.done == 1
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # Error resilience
353
+ # ---------------------------------------------------------------------------
354
+
355
+
356
+ class TestErrorResilience:
357
+ """Tests for graceful error handling."""
358
+
359
+ def test_corrupt_json_doesnt_crash(self, home: Path) -> None:
360
+ """Corrupt JSON files don't crash collection."""
361
+ (home / "trust").mkdir(exist_ok=True)
362
+ (home / "trust" / "trust.json").write_text("not json {{{", encoding="utf-8")
363
+
364
+ collector = MetricsCollector(home)
365
+ report = collector.collect()
366
+ assert isinstance(report, MetricsReport)
367
+
368
+ def test_missing_home_doesnt_crash(self, tmp_path: Path) -> None:
369
+ """Non-existent home doesn't crash."""
370
+ collector = MetricsCollector(tmp_path / "nonexistent")
371
+ report = collector.collect()
372
+ assert isinstance(report, MetricsReport)
373
+
374
+ def test_all_sections_isolated(self, home: Path) -> None:
375
+ """One failing section doesn't prevent others."""
376
+ (home / "security" / "audit.log").write_text("not json\n", encoding="utf-8")
377
+ (home / "trust").mkdir(exist_ok=True)
378
+ (home / "trust" / "trust.json").write_text(json.dumps({
379
+ "depth": 5.0, "trust_level": 0.8,
380
+ }), encoding="utf-8")
381
+
382
+ collector = MetricsCollector(home)
383
+ report = collector.collect()
384
+ assert report.trust.available is True
385
+
386
+
387
+ # ---------------------------------------------------------------------------
388
+ # Model tests
389
+ # ---------------------------------------------------------------------------
390
+
391
+
392
+ class TestModels:
393
+ """Tests for metrics models."""
394
+
395
+ def test_report_serializable(self, collector: MetricsCollector) -> None:
396
+ """Report can be serialized to JSON."""
397
+ report = collector.collect()
398
+ data = report.model_dump_json()
399
+ assert isinstance(data, str)
400
+ parsed = json.loads(data)
401
+ assert "identity" in parsed
402
+ assert "trust" in parsed
403
+ assert "kms" in parsed
404
+
405
+ def test_trust_metrics_defaults(self) -> None:
406
+ """TrustMetrics has sensible defaults."""
407
+ t = TrustMetrics()
408
+ assert t.available is False
409
+ assert t.depth == 0.0
410
+
411
+ def test_kms_metrics_defaults(self) -> None:
412
+ """KmsMetrics has sensible defaults."""
413
+ k = KmsMetrics()
414
+ assert k.available is False
415
+ assert k.active_keys == 0
416
+
417
+
418
+ # ---------------------------------------------------------------------------
419
+ # ConsciousnessMetrics
420
+ # ---------------------------------------------------------------------------
421
+
422
+
423
+ class TestConsciousnessMetrics:
424
+ """Tests for the consciousness loop runtime metrics collector."""
425
+
426
+ @pytest.fixture
427
+ def cm(self, tmp_path: Path) -> ConsciousnessMetrics:
428
+ """ConsciousnessMetrics with no background thread."""
429
+ return ConsciousnessMetrics(home=tmp_path, persist_interval=0)
430
+
431
+ # ------------------------------------------------------------------
432
+ # Basic counters
433
+ # ------------------------------------------------------------------
434
+
435
+ def test_initial_counters_zero(self, cm: ConsciousnessMetrics) -> None:
436
+ """All counters start at zero."""
437
+ d = cm.to_dict()
438
+ assert d["messages_processed"] == 0
439
+ assert d["responses_sent"] == 0
440
+ assert d["errors"] == 0
441
+
442
+ def test_record_message_increments(self, cm: ConsciousnessMetrics) -> None:
443
+ """record_message increments messages_processed and peer counter."""
444
+ cm.record_message("alice")
445
+ cm.record_message("alice")
446
+ cm.record_message("bob")
447
+ d = cm.to_dict()
448
+ assert d["messages_processed"] == 3
449
+ assert d["messages_per_peer"]["alice"] == 2
450
+ assert d["messages_per_peer"]["bob"] == 1
451
+
452
+ def test_record_response_increments(self, cm: ConsciousnessMetrics) -> None:
453
+ """record_response increments responses_sent, backend, and tier."""
454
+ cm.record_response(120.5, "ollama", "fast")
455
+ cm.record_response(80.0, "anthropic", "standard")
456
+ cm.record_response(95.0, "ollama", "fast")
457
+ d = cm.to_dict()
458
+ assert d["responses_sent"] == 3
459
+ assert d["backend_usage"]["ollama"] == 2
460
+ assert d["backend_usage"]["anthropic"] == 1
461
+ assert d["tier_usage"]["fast"] == 2
462
+ assert d["tier_usage"]["standard"] == 1
463
+
464
+ def test_record_error_increments(self, cm: ConsciousnessMetrics) -> None:
465
+ """record_error increments the errors counter."""
466
+ cm.record_error()
467
+ cm.record_error()
468
+ assert cm.to_dict()["errors"] == 2
469
+
470
+ # ------------------------------------------------------------------
471
+ # Histogram
472
+ # ------------------------------------------------------------------
473
+
474
+ def test_histogram_stats_empty(self, cm: ConsciousnessMetrics) -> None:
475
+ """Histogram returns zeros when no samples."""
476
+ stats = cm.to_dict()["response_time_ms"]
477
+ assert stats["count"] == 0
478
+ assert stats["min"] == 0.0
479
+ assert stats["avg"] == 0.0
480
+ assert stats["p99"] == 0.0
481
+
482
+ def test_histogram_min_max_avg(self, cm: ConsciousnessMetrics) -> None:
483
+ """Histogram computes min/max/avg correctly."""
484
+ for ms in [10.0, 20.0, 30.0, 40.0, 50.0]:
485
+ cm.record_response(ms, "passthrough", "fast")
486
+ stats = cm.to_dict()["response_time_ms"]
487
+ assert stats["min"] == 10.0
488
+ assert stats["max"] == 50.0
489
+ assert stats["avg"] == 30.0
490
+ assert stats["count"] == 5
491
+
492
+ def test_histogram_p99_single(self, cm: ConsciousnessMetrics) -> None:
493
+ """p99 of a single sample equals that sample."""
494
+ cm.record_response(42.0, "passthrough", "fast")
495
+ stats = cm.to_dict()["response_time_ms"]
496
+ assert stats["p99"] == 42.0
497
+
498
+ def test_histogram_p99_hundred_samples(self, cm: ConsciousnessMetrics) -> None:
499
+ """p99 of 100 evenly-spaced samples is the 99th value."""
500
+ for i in range(1, 101):
501
+ cm.record_response(float(i), "passthrough", "fast")
502
+ stats = cm.to_dict()["response_time_ms"]
503
+ # p99_idx = min(99, int(100 * 0.99)) = 98 → sorted[98] = 99.0
504
+ assert stats["p99"] == 99.0
505
+
506
+ def test_histogram_capped_at_1000(self, cm: ConsciousnessMetrics) -> None:
507
+ """Histogram caps sample list at 1 000 to bound memory."""
508
+ for i in range(1200):
509
+ cm.record_response(float(i), "passthrough", "fast")
510
+ assert cm.to_dict()["response_time_ms"]["count"] == 1000
511
+
512
+ # ------------------------------------------------------------------
513
+ # Persistence
514
+ # ------------------------------------------------------------------
515
+
516
+ def test_save_and_reload(self, tmp_path: Path) -> None:
517
+ """save() writes JSON; a new instance with the same home loads it."""
518
+ cm1 = ConsciousnessMetrics(home=tmp_path, persist_interval=0)
519
+ cm1.record_message("peer-a")
520
+ cm1.record_response(55.0, "ollama", "fast")
521
+ cm1.record_error()
522
+ cm1.save()
523
+
524
+ cm2 = ConsciousnessMetrics(home=tmp_path, persist_interval=0)
525
+ d = cm2.to_dict()
526
+ assert d["messages_processed"] == 1
527
+ assert d["responses_sent"] == 1
528
+ assert d["errors"] == 1
529
+ assert d["backend_usage"]["ollama"] == 1
530
+ assert d["tier_usage"]["fast"] == 1
531
+ assert d["messages_per_peer"]["peer-a"] == 1
532
+
533
+ def test_save_creates_daily_file(self, tmp_path: Path) -> None:
534
+ """save() creates the daily JSON file under metrics/daily/."""
535
+ from datetime import datetime, timezone
536
+ cm = ConsciousnessMetrics(home=tmp_path, persist_interval=0)
537
+ cm.record_message("x")
538
+ cm.save()
539
+
540
+ date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
541
+ daily = tmp_path / "metrics" / "daily" / f"{date_str}.json"
542
+ assert daily.exists()
543
+ data = json.loads(daily.read_text(encoding="utf-8"))
544
+ assert data["messages_processed"] == 1
545
+
546
+ def test_load_missing_file_doesnt_crash(self, tmp_path: Path) -> None:
547
+ """Creating ConsciousnessMetrics when no file exists doesn't fail."""
548
+ cm = ConsciousnessMetrics(home=tmp_path / "nonexistent", persist_interval=0)
549
+ assert cm.to_dict()["messages_processed"] == 0
550
+
551
+ def test_load_corrupt_file_doesnt_crash(self, tmp_path: Path) -> None:
552
+ """Corrupt daily JSON is silently ignored."""
553
+ from datetime import datetime, timezone
554
+ date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
555
+ daily = tmp_path / "metrics" / "daily" / f"{date_str}.json"
556
+ daily.parent.mkdir(parents=True)
557
+ daily.write_text("not json {{{", encoding="utf-8")
558
+ cm = ConsciousnessMetrics(home=tmp_path, persist_interval=0)
559
+ assert cm.to_dict()["messages_processed"] == 0
560
+
561
+ # ------------------------------------------------------------------
562
+ # Thread safety
563
+ # ------------------------------------------------------------------
564
+
565
+ def test_concurrent_record_message(self, cm: ConsciousnessMetrics) -> None:
566
+ """Concurrent record_message calls produce correct total."""
567
+ n = 200
568
+ barrier = threading.Barrier(n)
569
+
570
+ def _record():
571
+ barrier.wait()
572
+ cm.record_message("stress-peer")
573
+
574
+ threads = [threading.Thread(target=_record) for _ in range(n)]
575
+ for t in threads:
576
+ t.start()
577
+ for t in threads:
578
+ t.join()
579
+
580
+ d = cm.to_dict()
581
+ assert d["messages_processed"] == n
582
+ assert d["messages_per_peer"]["stress-peer"] == n
583
+
584
+ def test_concurrent_record_response(self, cm: ConsciousnessMetrics) -> None:
585
+ """Concurrent record_response calls produce correct total."""
586
+ n = 100
587
+ barrier = threading.Barrier(n)
588
+
589
+ def _record():
590
+ barrier.wait()
591
+ cm.record_response(10.0, "passthrough", "fast")
592
+
593
+ threads = [threading.Thread(target=_record) for _ in range(n)]
594
+ for t in threads:
595
+ t.start()
596
+ for t in threads:
597
+ t.join()
598
+
599
+ assert cm.to_dict()["responses_sent"] == n
600
+
601
+ # ------------------------------------------------------------------
602
+ # to_dict structure
603
+ # ------------------------------------------------------------------
604
+
605
+ def test_to_dict_keys(self, cm: ConsciousnessMetrics) -> None:
606
+ """to_dict returns all required keys."""
607
+ d = cm.to_dict()
608
+ required = {
609
+ "date", "session_start", "messages_processed", "responses_sent",
610
+ "errors", "response_time_ms", "backend_usage", "tier_usage",
611
+ "messages_per_peer",
612
+ }
613
+ assert required.issubset(d.keys())
614
+
615
+ def test_to_dict_json_serializable(self, cm: ConsciousnessMetrics) -> None:
616
+ """to_dict output can be serialized to JSON."""
617
+ cm.record_message("peer")
618
+ cm.record_response(50.0, "ollama", "local")
619
+ data = json.dumps(cm.to_dict())
620
+ assert isinstance(data, str)
621
+
622
+ # ------------------------------------------------------------------
623
+ # Peer name sanitization
624
+ # ------------------------------------------------------------------
625
+
626
+ def test_peer_name_truncated_to_64(self, cm: ConsciousnessMetrics) -> None:
627
+ """Peer names longer than 64 chars are truncated in the counter key."""
628
+ long_peer = "a" * 100
629
+ cm.record_message(long_peer)
630
+ d = cm.to_dict()
631
+ assert "a" * 64 in d["messages_per_peer"]
632
+ assert long_peer not in d["messages_per_peer"]