@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,781 @@
1
+ """Tests for the skcapstone daemon."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import threading
8
+ import time
9
+ import urllib.request
10
+ from pathlib import Path
11
+ from unittest.mock import MagicMock, patch
12
+
13
+ import pytest
14
+
15
+ from skcapstone.daemon import (
16
+ DaemonConfig,
17
+ DaemonService,
18
+ DaemonState,
19
+ is_running,
20
+ read_pid,
21
+ )
22
+
23
+
24
+ @pytest.fixture
25
+ def daemon_home(tmp_path):
26
+ """Create a minimal agent home for daemon tests."""
27
+ home = tmp_path / ".skcapstone"
28
+ home.mkdir()
29
+ (home / "config").mkdir()
30
+ (home / "logs").mkdir()
31
+ return home
32
+
33
+
34
+ @pytest.fixture
35
+ def daemon_config(daemon_home):
36
+ return DaemonConfig(
37
+ home=daemon_home,
38
+ poll_interval=1,
39
+ sync_interval=60,
40
+ health_interval=5,
41
+ port=0,
42
+ )
43
+
44
+
45
+ class TestDaemonState:
46
+ """Tests for thread-safe DaemonState."""
47
+
48
+ def test_initial_state(self):
49
+ state = DaemonState()
50
+ assert state.running is False
51
+ assert state.messages_received == 0
52
+ assert state.syncs_completed == 0
53
+
54
+ def test_snapshot(self):
55
+ state = DaemonState()
56
+ snap = state.snapshot()
57
+ assert snap["running"] is False
58
+ assert snap["messages_received"] == 0
59
+ assert "pid" in snap
60
+
61
+ def test_record_poll(self):
62
+ state = DaemonState()
63
+ state.record_poll(3)
64
+ assert state.messages_received == 3
65
+ assert state.last_poll is not None
66
+
67
+ def test_record_poll_accumulates(self):
68
+ state = DaemonState()
69
+ state.record_poll(2)
70
+ state.record_poll(5)
71
+ assert state.messages_received == 7
72
+
73
+ def test_record_sync(self):
74
+ state = DaemonState()
75
+ state.record_sync()
76
+ assert state.syncs_completed == 1
77
+ assert state.last_sync is not None
78
+
79
+ def test_record_health(self):
80
+ state = DaemonState()
81
+ state.record_health({"syncthing": {"status": "available"}})
82
+ assert "syncthing" in state.health_reports
83
+
84
+ def test_record_error(self):
85
+ state = DaemonState()
86
+ state.record_error("test error")
87
+ assert len(state.errors) == 1
88
+
89
+ def test_error_limit(self):
90
+ state = DaemonState()
91
+ for i in range(60):
92
+ state.record_error(f"error-{i}")
93
+ assert len(state.errors) == 50
94
+
95
+ def test_thread_safety(self):
96
+ state = DaemonState()
97
+ errors = []
98
+
99
+ def worker(n):
100
+ try:
101
+ for _ in range(100):
102
+ state.record_poll(1)
103
+ state.record_error(f"from-{n}")
104
+ except Exception as e:
105
+ errors.append(e)
106
+
107
+ threads = [threading.Thread(target=worker, args=(i,)) for i in range(4)]
108
+ for t in threads:
109
+ t.start()
110
+ for t in threads:
111
+ t.join()
112
+
113
+ assert errors == []
114
+ assert state.messages_received == 400
115
+
116
+
117
+ class TestDaemonConfig:
118
+ """Tests for DaemonConfig."""
119
+
120
+ def test_defaults(self, daemon_home):
121
+ config = DaemonConfig(home=daemon_home)
122
+ assert config.poll_interval == 10
123
+ assert config.sync_interval == 300
124
+ assert config.health_interval == 60
125
+ assert config.port == 7777
126
+
127
+ def test_custom(self, daemon_home):
128
+ config = DaemonConfig(home=daemon_home, poll_interval=5, port=9999)
129
+ assert config.poll_interval == 5
130
+ assert config.port == 9999
131
+
132
+ def test_creates_log_dir(self, daemon_home):
133
+ config = DaemonConfig(home=daemon_home)
134
+ assert config.log_file.parent.exists()
135
+
136
+
137
+ class TestPidManagement:
138
+ """Tests for PID file read/write."""
139
+
140
+ def test_no_pid_file(self, daemon_home):
141
+ assert read_pid(daemon_home) is None
142
+
143
+ def test_is_running_false(self, daemon_home):
144
+ assert is_running(daemon_home) is False
145
+
146
+ def test_stale_pid_cleaned(self, daemon_home):
147
+ pid_path = daemon_home / "daemon.pid"
148
+ pid_path.write_text("999999999")
149
+ assert read_pid(daemon_home) is None
150
+ assert not pid_path.exists()
151
+
152
+
153
+ class TestDaemonService:
154
+ """Tests for DaemonService lifecycle."""
155
+
156
+ def test_creates_and_stops(self, daemon_home):
157
+ config = DaemonConfig(home=daemon_home, port=0, poll_interval=60)
158
+ svc = DaemonService(config)
159
+
160
+ # Patch preflight + component loading to avoid fs/network deps in tests
161
+ with patch.object(svc, "_run_preflight"):
162
+ with patch.object(svc, "_load_components"):
163
+ with patch.object(svc, "_start_api_server"):
164
+ svc.start()
165
+ assert svc.state.running is True
166
+ assert (daemon_home / "daemon.pid").exists()
167
+
168
+ svc.stop()
169
+ assert svc.state.running is False
170
+ assert not (daemon_home / "daemon.pid").exists()
171
+
172
+ def test_poll_loop_with_mock_skcomm(self, daemon_home):
173
+ config = DaemonConfig(home=daemon_home, poll_interval=1, port=0)
174
+ svc = DaemonService(config)
175
+
176
+ mock_comm = MagicMock()
177
+ mock_comm.receive.return_value = []
178
+ svc._skcomm = mock_comm
179
+
180
+ svc._stop_event = threading.Event()
181
+ t = threading.Thread(target=svc._poll_loop, daemon=True)
182
+ t.start()
183
+
184
+ time.sleep(1.5)
185
+ svc._stop_event.set()
186
+ t.join(timeout=3)
187
+
188
+ assert mock_comm.receive.called
189
+ assert svc.state.last_poll is not None
190
+
191
+
192
+ class TestDaemonAPI:
193
+ """Tests for the HTTP API server."""
194
+
195
+ def test_api_ping(self, daemon_home):
196
+ config = DaemonConfig(home=daemon_home, port=0, poll_interval=60)
197
+ svc = DaemonService(config)
198
+ svc.state.running = True
199
+
200
+ with patch.object(svc, "_load_components"):
201
+ svc.config.port = _find_free_port()
202
+ svc._write_pid()
203
+ svc._start_api_server()
204
+
205
+ time.sleep(0.5)
206
+
207
+ try:
208
+ url = f"http://127.0.0.1:{svc.config.port}/ping"
209
+ with urllib.request.urlopen(url, timeout=2) as resp:
210
+ data = json.loads(resp.read())
211
+ assert data["pong"] is True
212
+ finally:
213
+ svc.stop()
214
+
215
+ def test_api_status(self, daemon_home):
216
+ config = DaemonConfig(home=daemon_home, port=0, poll_interval=60)
217
+ svc = DaemonService(config)
218
+ svc.state.running = True
219
+
220
+ with patch.object(svc, "_load_components"):
221
+ svc.config.port = _find_free_port()
222
+ svc._write_pid()
223
+ svc._start_api_server()
224
+
225
+ time.sleep(0.5)
226
+
227
+ try:
228
+ url = f"http://127.0.0.1:{svc.config.port}/status"
229
+ with urllib.request.urlopen(url, timeout=2) as resp:
230
+ data = json.loads(resp.read())
231
+ assert data["running"] is True
232
+ assert "pid" in data
233
+ finally:
234
+ svc.stop()
235
+
236
+
237
+ class TestHeartbeatBeaconWiring:
238
+ """Tests that HeartbeatBeacon is wired into the daemon health loop."""
239
+
240
+ def test_beacon_defaults_to_none(self, daemon_home):
241
+ """_beacon is None before _load_components runs."""
242
+ config = DaemonConfig(home=daemon_home, port=0)
243
+ svc = DaemonService(config)
244
+ assert svc._beacon is None
245
+
246
+ def test_health_loop_pulses_beacon_consciousness_active(self, daemon_home):
247
+ """_health_loop calls beacon.pulse(consciousness_active=True) when consciousness is set."""
248
+ config = DaemonConfig(home=daemon_home, port=0, health_interval=60)
249
+ svc = DaemonService(config)
250
+
251
+ mock_beacon = MagicMock()
252
+ svc._beacon = mock_beacon
253
+ svc._consciousness = MagicMock() # truthy → consciousness_active=True
254
+
255
+ svc._stop_event = threading.Event()
256
+ t = threading.Thread(target=svc._health_loop, daemon=True)
257
+ t.start()
258
+ time.sleep(0.2)
259
+ svc._stop_event.set()
260
+ t.join(timeout=2)
261
+
262
+ mock_beacon.pulse.assert_called_once()
263
+ _, kwargs = mock_beacon.pulse.call_args
264
+ assert kwargs["consciousness_active"] is True
265
+
266
+ def test_health_loop_pulses_beacon_consciousness_inactive(self, daemon_home):
267
+ """_health_loop calls beacon.pulse(consciousness_active=False) when consciousness is None."""
268
+ config = DaemonConfig(home=daemon_home, port=0, health_interval=60)
269
+ svc = DaemonService(config)
270
+
271
+ mock_beacon = MagicMock()
272
+ svc._beacon = mock_beacon
273
+ svc._consciousness = None # falsy → consciousness_active=False
274
+
275
+ svc._stop_event = threading.Event()
276
+ t = threading.Thread(target=svc._health_loop, daemon=True)
277
+ t.start()
278
+ time.sleep(0.2)
279
+ svc._stop_event.set()
280
+ t.join(timeout=2)
281
+
282
+ mock_beacon.pulse.assert_called_once()
283
+ _, kwargs = mock_beacon.pulse.call_args
284
+ assert kwargs["consciousness_active"] is False
285
+
286
+ def test_health_loop_skips_pulse_when_no_beacon(self, daemon_home):
287
+ """_health_loop does not crash when _beacon is None."""
288
+ config = DaemonConfig(home=daemon_home, port=0, health_interval=60)
289
+ svc = DaemonService(config)
290
+ svc._beacon = None
291
+
292
+ svc._stop_event = threading.Event()
293
+ t = threading.Thread(target=svc._health_loop, daemon=True)
294
+ t.start()
295
+ time.sleep(0.2)
296
+ svc._stop_event.set()
297
+ t.join(timeout=2)
298
+ # No exception → test passes
299
+
300
+ def test_load_components_initializes_beacon(self, daemon_home):
301
+ """_load_components sets _beacon using sys.modules patching."""
302
+ import sys
303
+
304
+ config = DaemonConfig(home=daemon_home, port=0, consciousness_enabled=False)
305
+ svc = DaemonService(config)
306
+
307
+ mock_runtime = MagicMock()
308
+ mock_runtime.manifest.name = "test-agent"
309
+ mock_runtime.is_initialized = True
310
+
311
+ mock_runtime_mod = MagicMock()
312
+ mock_runtime_mod.get_runtime.return_value = mock_runtime
313
+
314
+ mock_beacon_instance = MagicMock()
315
+ mock_heartbeat_mod = MagicMock()
316
+ mock_heartbeat_mod.HeartbeatBeacon.return_value = mock_beacon_instance
317
+
318
+ patched = {
319
+ "skcomm": MagicMock(),
320
+ "skcomm.core": MagicMock(),
321
+ "skcapstone.runtime": mock_runtime_mod,
322
+ "skcapstone.heartbeat": mock_heartbeat_mod,
323
+ "skcapstone.consciousness_config": MagicMock(),
324
+ "skcapstone.consciousness_loop": MagicMock(),
325
+ "skcapstone.self_healing": MagicMock(),
326
+ }
327
+ with patch.dict(sys.modules, patched):
328
+ svc._load_components()
329
+
330
+ assert svc._beacon is mock_beacon_instance
331
+ mock_heartbeat_mod.HeartbeatBeacon.assert_called_once_with(
332
+ config.home, "test-agent"
333
+ )
334
+
335
+
336
+ class TestHouseholdAPI:
337
+ """Tests for the household and conversation HTTP endpoints."""
338
+
339
+ def _start_server(self, daemon_home):
340
+ """Start the API server on a free port and return the service."""
341
+ config = DaemonConfig(home=daemon_home, shared_root=daemon_home, port=0, poll_interval=60)
342
+ svc = DaemonService(config)
343
+ svc.state.running = True
344
+ with patch.object(svc, "_load_components"):
345
+ svc.config.port = _find_free_port()
346
+ svc._write_pid()
347
+ svc._start_api_server()
348
+ time.sleep(0.3)
349
+ return svc
350
+
351
+ def _get(self, port, path):
352
+ url = f"http://127.0.0.1:{port}{path}"
353
+ with urllib.request.urlopen(url, timeout=2) as resp:
354
+ return resp.status, json.loads(resp.read())
355
+
356
+ def _get_404(self, port, path):
357
+ import urllib.error
358
+ url = f"http://127.0.0.1:{port}{path}"
359
+ try:
360
+ with urllib.request.urlopen(url, timeout=2) as resp:
361
+ return resp.status, json.loads(resp.read())
362
+ except urllib.error.HTTPError as exc:
363
+ return exc.code, json.loads(exc.read())
364
+
365
+ # ── /api/v1/household/agents ─────────────────────────────────────────
366
+
367
+ def test_household_agents_empty(self, daemon_home):
368
+ svc = self._start_server(daemon_home)
369
+ try:
370
+ status, data = self._get(svc.config.port, "/api/v1/household/agents")
371
+ assert status == 200
372
+ assert data["agents"] == []
373
+ finally:
374
+ svc.stop()
375
+
376
+ def test_household_agents_with_identity(self, daemon_home):
377
+ agent_dir = daemon_home / "agents" / "testbot"
378
+ (agent_dir / "identity").mkdir(parents=True)
379
+ (agent_dir / "identity" / "identity.json").write_text(
380
+ json.dumps({"name": "Testbot", "fingerprint": "ABC123"})
381
+ )
382
+ svc = self._start_server(daemon_home)
383
+ try:
384
+ _, data = self._get(svc.config.port, "/api/v1/household/agents")
385
+ assert len(data["agents"]) == 1
386
+ agent = data["agents"][0]
387
+ assert agent["name"] == "testbot"
388
+ assert agent["identity"]["fingerprint"] == "ABC123"
389
+ assert agent["status"] == "no_heartbeat"
390
+ finally:
391
+ svc.stop()
392
+
393
+ def test_household_agents_with_fresh_heartbeat(self, daemon_home):
394
+ from datetime import datetime, timezone
395
+ agent_dir = daemon_home / "agents" / "alivebot"
396
+ (agent_dir / "identity").mkdir(parents=True)
397
+ (agent_dir / "identity" / "identity.json").write_text(
398
+ json.dumps({"name": "Alivebot"})
399
+ )
400
+ hb_dir = daemon_home / "heartbeats"
401
+ hb_dir.mkdir(parents=True, exist_ok=True)
402
+ (hb_dir / "alivebot.json").write_text(json.dumps({
403
+ "agent_name": "Alivebot",
404
+ "status": "alive",
405
+ "timestamp": datetime.now(timezone.utc).isoformat(),
406
+ "ttl_seconds": 300,
407
+ }))
408
+ svc = self._start_server(daemon_home)
409
+ try:
410
+ _, data = self._get(svc.config.port, "/api/v1/household/agents")
411
+ agent = data["agents"][0]
412
+ assert agent["heartbeat"]["alive"] is True
413
+ assert agent["status"] == "alive"
414
+ finally:
415
+ svc.stop()
416
+
417
+ def test_household_agents_stale_heartbeat(self, daemon_home):
418
+ agent_dir = daemon_home / "agents" / "stalebot"
419
+ (agent_dir / "identity").mkdir(parents=True)
420
+ (agent_dir / "identity" / "identity.json").write_text(json.dumps({"name": "Stalebot"}))
421
+ hb_dir = daemon_home / "heartbeats"
422
+ hb_dir.mkdir(parents=True, exist_ok=True)
423
+ (hb_dir / "stalebot.json").write_text(json.dumps({
424
+ "agent_name": "Stalebot",
425
+ "status": "alive",
426
+ "timestamp": "2020-01-01T00:00:00+00:00",
427
+ "ttl_seconds": 300,
428
+ }))
429
+ svc = self._start_server(daemon_home)
430
+ try:
431
+ _, data = self._get(svc.config.port, "/api/v1/household/agents")
432
+ agent = data["agents"][0]
433
+ assert agent["heartbeat"]["alive"] is False
434
+ assert agent["status"] == "stale"
435
+ finally:
436
+ svc.stop()
437
+
438
+ # ── /api/v1/household/agent/{name} ───────────────────────────────────
439
+
440
+ def test_single_agent_not_found(self, daemon_home):
441
+ svc = self._start_server(daemon_home)
442
+ try:
443
+ status, data = self._get_404(svc.config.port, "/api/v1/household/agent/nobody")
444
+ assert status == 404
445
+ assert "not found" in data["error"]
446
+ finally:
447
+ svc.stop()
448
+
449
+ def test_single_agent_detail(self, daemon_home):
450
+ agent_dir = daemon_home / "agents" / "opus"
451
+ (agent_dir / "identity").mkdir(parents=True)
452
+ (agent_dir / "identity" / "identity.json").write_text(
453
+ json.dumps({"name": "Opus", "fingerprint": "DEADBEEF"})
454
+ )
455
+ mem_dir = agent_dir / "memory" / "short-term"
456
+ mem_dir.mkdir(parents=True)
457
+ (mem_dir / "mem1.json").write_text("{}")
458
+ (mem_dir / "mem2.json").write_text("{}")
459
+
460
+ svc = self._start_server(daemon_home)
461
+ try:
462
+ status, data = self._get(svc.config.port, "/api/v1/household/agent/opus")
463
+ assert status == 200
464
+ assert data["name"] == "opus"
465
+ assert data["identity"]["fingerprint"] == "DEADBEEF"
466
+ assert data["memory_count"] == 2
467
+ assert "recent_conversations" in data
468
+ finally:
469
+ svc.stop()
470
+
471
+ # ── /api/v1/conversations ────────────────────────────────────────────
472
+
473
+ def test_conversations_empty(self, daemon_home):
474
+ svc = self._start_server(daemon_home)
475
+ try:
476
+ status, data = self._get(svc.config.port, "/api/v1/conversations")
477
+ assert status == 200
478
+ assert data["conversations"] == []
479
+ finally:
480
+ svc.stop()
481
+
482
+ def test_conversations_list(self, daemon_home):
483
+ conv_dir = daemon_home / "conversations"
484
+ conv_dir.mkdir(parents=True)
485
+ msgs = [
486
+ {"role": "user", "content": "hi", "timestamp": "2026-03-01T10:00:00+00:00"},
487
+ {"role": "assistant", "content": "hello", "timestamp": "2026-03-01T10:00:01+00:00"},
488
+ ]
489
+ (conv_dir / "alice.json").write_text(json.dumps(msgs))
490
+
491
+ svc = self._start_server(daemon_home)
492
+ try:
493
+ _, data = self._get(svc.config.port, "/api/v1/conversations")
494
+ assert len(data["conversations"]) == 1
495
+ c = data["conversations"][0]
496
+ assert c["peer"] == "alice"
497
+ assert c["message_count"] == 2
498
+ assert c["last_message_time"] == "2026-03-01T10:00:01+00:00"
499
+ finally:
500
+ svc.stop()
501
+
502
+ # ── /api/v1/conversations/{peer} ─────────────────────────────────────
503
+
504
+ def test_conversation_peer_not_found(self, daemon_home):
505
+ svc = self._start_server(daemon_home)
506
+ try:
507
+ status, data = self._get_404(svc.config.port, "/api/v1/conversations/nobody")
508
+ assert status == 404
509
+ assert "nobody" in data["error"]
510
+ finally:
511
+ svc.stop()
512
+
513
+ def test_conversation_peer_history(self, daemon_home):
514
+ conv_dir = daemon_home / "conversations"
515
+ conv_dir.mkdir(parents=True)
516
+ msgs = [
517
+ {"role": "user", "content": "ping", "timestamp": "2026-03-01T09:00:00+00:00"},
518
+ ]
519
+ (conv_dir / "bob.json").write_text(json.dumps(msgs))
520
+
521
+ svc = self._start_server(daemon_home)
522
+ try:
523
+ status, data = self._get(svc.config.port, "/api/v1/conversations/bob")
524
+ assert status == 200
525
+ assert data["peer"] == "bob"
526
+ assert len(data["messages"]) == 1
527
+ assert data["messages"][0]["content"] == "ping"
528
+ finally:
529
+ svc.stop()
530
+
531
+
532
+ class TestDashboardAPI:
533
+ """Tests for GET / (HTML dashboard) and GET /api/v1/dashboard (JSON)."""
534
+
535
+ def _start_server(self, daemon_home, shared_root=None):
536
+ root = shared_root or daemon_home
537
+ config = DaemonConfig(
538
+ home=daemon_home, shared_root=root, port=0, poll_interval=60
539
+ )
540
+ svc = DaemonService(config)
541
+ svc.state.running = True
542
+ with patch.object(svc, "_load_components"):
543
+ svc.config.port = _find_free_port()
544
+ svc._write_pid()
545
+ svc._start_api_server()
546
+ time.sleep(0.3)
547
+ return svc
548
+
549
+ def _get(self, port, path):
550
+ url = f"http://127.0.0.1:{port}{path}"
551
+ with urllib.request.urlopen(url, timeout=2) as resp:
552
+ return resp.status, resp.read(), resp.headers.get("Content-Type", "")
553
+
554
+ # ── GET /api/v1/dashboard ────────────────────────────────────────────
555
+
556
+ def test_dashboard_json_returns_200(self, daemon_home):
557
+ svc = self._start_server(daemon_home)
558
+ try:
559
+ status, body, ct = self._get(svc.config.port, "/api/v1/dashboard")
560
+ assert status == 200
561
+ assert "application/json" in ct
562
+ data = json.loads(body)
563
+ assert "agent" in data
564
+ assert "daemon" in data
565
+ assert "consciousness" in data
566
+ assert "backends" in data
567
+ assert "conversations" in data
568
+ assert "system" in data
569
+ assert "recent_errors" in data
570
+ finally:
571
+ svc.stop()
572
+
573
+ def test_dashboard_json_daemon_fields(self, daemon_home):
574
+ svc = self._start_server(daemon_home)
575
+ svc.state.record_poll(7)
576
+ try:
577
+ _, body, _ = self._get(svc.config.port, "/api/v1/dashboard")
578
+ data = json.loads(body)
579
+ assert data["daemon"]["running"] is True
580
+ assert data["daemon"]["messages_received"] == 7
581
+ assert "uptime_seconds" in data["daemon"]
582
+ assert "pid" in data["daemon"]
583
+ finally:
584
+ svc.stop()
585
+
586
+ def test_dashboard_json_system_stats(self, daemon_home):
587
+ svc = self._start_server(daemon_home)
588
+ try:
589
+ _, body, _ = self._get(svc.config.port, "/api/v1/dashboard")
590
+ data = json.loads(body)
591
+ sys_stats = data["system"]
592
+ assert "disk_total_gb" in sys_stats
593
+ assert "memory_total_mb" in sys_stats
594
+ assert sys_stats["disk_total_gb"] > 0
595
+ finally:
596
+ svc.stop()
597
+
598
+ def test_dashboard_json_conversations_last5(self, daemon_home):
599
+ conv_dir = daemon_home / "conversations"
600
+ conv_dir.mkdir(parents=True)
601
+ for i in range(7):
602
+ msgs = [{"role": "user", "content": f"msg{i}", "timestamp": f"2026-03-0{i % 9 + 1}T10:00:00+00:00"}]
603
+ (conv_dir / f"peer{i}.json").write_text(json.dumps(msgs))
604
+
605
+ svc = self._start_server(daemon_home, shared_root=daemon_home)
606
+ try:
607
+ _, body, _ = self._get(svc.config.port, "/api/v1/dashboard")
608
+ data = json.loads(body)
609
+ assert len(data["conversations"]) <= 5
610
+ finally:
611
+ svc.stop()
612
+
613
+ def test_dashboard_json_identity_from_file(self, daemon_home):
614
+ (daemon_home / "identity").mkdir(parents=True, exist_ok=True)
615
+ (daemon_home / "identity" / "identity.json").write_text(
616
+ json.dumps({"name": "TestAgent", "fingerprint": "DEADBEEF12345678"})
617
+ )
618
+ svc = self._start_server(daemon_home)
619
+ try:
620
+ _, body, _ = self._get(svc.config.port, "/api/v1/dashboard")
621
+ data = json.loads(body)
622
+ assert data["agent"]["name"] == "TestAgent"
623
+ assert data["agent"]["fingerprint"] == "DEADBEEF12345678"
624
+ finally:
625
+ svc.stop()
626
+
627
+ # ── GET / (HTML dashboard) ───────────────────────────────────────────
628
+
629
+ def test_dashboard_html_returns_200(self, daemon_home):
630
+ svc = self._start_server(daemon_home)
631
+ try:
632
+ status, body, ct = self._get(svc.config.port, "/")
633
+ assert status == 200
634
+ assert "text/html" in ct
635
+ html = body.decode("utf-8")
636
+ assert "<!DOCTYPE html>" in html
637
+ finally:
638
+ svc.stop()
639
+
640
+ def test_dashboard_html_dark_theme(self, daemon_home):
641
+ svc = self._start_server(daemon_home)
642
+ try:
643
+ _, body, _ = self._get(svc.config.port, "/")
644
+ html = body.decode("utf-8")
645
+ assert "#0d1117" in html # GitHub dark background
646
+ finally:
647
+ svc.stop()
648
+
649
+ def test_dashboard_html_auto_refresh(self, daemon_home):
650
+ svc = self._start_server(daemon_home)
651
+ try:
652
+ _, body, _ = self._get(svc.config.port, "/")
653
+ html = body.decode("utf-8")
654
+ assert 'http-equiv="refresh"' in html
655
+ assert "content=\"30\"" in html
656
+ finally:
657
+ svc.stop()
658
+
659
+ def test_dashboard_html_contains_agent_section(self, daemon_home):
660
+ (daemon_home / "identity").mkdir(parents=True, exist_ok=True)
661
+ (daemon_home / "identity" / "identity.json").write_text(
662
+ json.dumps({"name": "Opus", "fingerprint": "ABCD1234ABCD1234ABCD1234"})
663
+ )
664
+ svc = self._start_server(daemon_home)
665
+ try:
666
+ _, body, _ = self._get(svc.config.port, "/")
667
+ html = body.decode("utf-8")
668
+ assert "Opus" in html
669
+ assert "Daemon" in html
670
+ assert "Consciousness" in html
671
+ assert "Backends" in html
672
+ assert "System" in html
673
+ finally:
674
+ svc.stop()
675
+
676
+ def test_dashboard_html_dot_indicators(self, daemon_home):
677
+ """Verify green/red dot CSS classes are present."""
678
+ svc = self._start_server(daemon_home)
679
+ try:
680
+ _, body, _ = self._get(svc.config.port, "/")
681
+ html = body.decode("utf-8")
682
+ assert "dot-green" in html
683
+ assert "dot-red" in html
684
+ finally:
685
+ svc.stop()
686
+
687
+ def test_dashboard_html_shows_conversation_peers(self, daemon_home):
688
+ conv_dir = daemon_home / "conversations"
689
+ conv_dir.mkdir(parents=True)
690
+ msgs = [{"role": "user", "content": "hi", "timestamp": "2026-03-01T10:00:00+00:00"}]
691
+ (conv_dir / "alice.json").write_text(json.dumps(msgs))
692
+
693
+ svc = self._start_server(daemon_home, shared_root=daemon_home)
694
+ try:
695
+ _, body, _ = self._get(svc.config.port, "/")
696
+ html = body.decode("utf-8")
697
+ assert "alice" in html
698
+ finally:
699
+ svc.stop()
700
+
701
+
702
+ class TestCORSHeaders:
703
+ """Tests for CORS headers on all API responses (Flutter web access)."""
704
+
705
+ def _start_server(self, daemon_home):
706
+ config = DaemonConfig(home=daemon_home, shared_root=daemon_home, port=0, poll_interval=60)
707
+ svc = DaemonService(config)
708
+ svc.state.running = True
709
+ with patch.object(svc, "_load_components"):
710
+ svc.config.port = _find_free_port()
711
+ svc._write_pid()
712
+ svc._start_api_server()
713
+ time.sleep(0.3)
714
+ return svc
715
+
716
+ def _request(self, port, path, method="GET"):
717
+ import urllib.error
718
+ url = f"http://127.0.0.1:{port}{path}"
719
+ req = urllib.request.Request(url, method=method)
720
+ try:
721
+ with urllib.request.urlopen(req, timeout=2) as resp:
722
+ return resp.status, resp.headers
723
+ except urllib.error.HTTPError as exc:
724
+ return exc.code, exc.headers
725
+
726
+ def test_options_preflight_returns_204(self, daemon_home):
727
+ svc = self._start_server(daemon_home)
728
+ try:
729
+ status, _ = self._request(svc.config.port, "/ping", method="OPTIONS")
730
+ assert status == 204
731
+ finally:
732
+ svc.stop()
733
+
734
+ def test_options_preflight_cors_headers(self, daemon_home):
735
+ svc = self._start_server(daemon_home)
736
+ try:
737
+ _, headers = self._request(svc.config.port, "/api/v1/status", method="OPTIONS")
738
+ assert headers.get("Access-Control-Allow-Origin") == "*"
739
+ allow_methods = headers.get("Access-Control-Allow-Methods", "")
740
+ assert "GET" in allow_methods
741
+ assert "POST" in allow_methods
742
+ assert "OPTIONS" in allow_methods
743
+ assert "Content-Type" in headers.get("Access-Control-Allow-Headers", "")
744
+ finally:
745
+ svc.stop()
746
+
747
+ def test_get_response_has_cors_origin(self, daemon_home):
748
+ svc = self._start_server(daemon_home)
749
+ try:
750
+ status, headers = self._request(svc.config.port, "/ping")
751
+ assert status == 200
752
+ assert headers.get("Access-Control-Allow-Origin") == "*"
753
+ finally:
754
+ svc.stop()
755
+
756
+ def test_json_response_has_cors_headers(self, daemon_home):
757
+ svc = self._start_server(daemon_home)
758
+ try:
759
+ status, headers = self._request(svc.config.port, "/api/v1/conversations")
760
+ assert status == 200
761
+ assert headers.get("Access-Control-Allow-Origin") == "*"
762
+ assert headers.get("Access-Control-Allow-Methods") is not None
763
+ finally:
764
+ svc.stop()
765
+
766
+ def test_html_response_has_cors_headers(self, daemon_home):
767
+ svc = self._start_server(daemon_home)
768
+ try:
769
+ status, headers = self._request(svc.config.port, "/")
770
+ assert status == 200
771
+ assert headers.get("Access-Control-Allow-Origin") == "*"
772
+ finally:
773
+ svc.stop()
774
+
775
+
776
+ def _find_free_port() -> int:
777
+ """Find an available port for testing."""
778
+ import socket
779
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
780
+ s.bind(("127.0.0.1", 0))
781
+ return s.getsockname()[1]