@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,310 @@
1
+ """Tests for the universal agent context loader.
2
+
3
+ Covers context gathering, all four formatters, and CLI integration.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+ import pytest
12
+ import yaml
13
+
14
+ from skcapstone.context_loader import (
15
+ FORMATTERS,
16
+ _gather_consciousness,
17
+ format_claude_md,
18
+ format_cursor_rules,
19
+ format_json,
20
+ format_text,
21
+ gather_context,
22
+ )
23
+ from skcapstone.memory_engine import store
24
+ from skcapstone.pillars.identity import generate_identity
25
+ from skcapstone.pillars.memory import initialize_memory
26
+ from skcapstone.pillars.security import initialize_security
27
+ from skcapstone.pillars.sync import initialize_sync
28
+ from skcapstone.pillars.trust import initialize_trust, record_trust_state
29
+
30
+
31
+ def _init_agent(home: Path, name: str = "context-test") -> None:
32
+ """Set up a full agent for testing."""
33
+ generate_identity(home, name)
34
+ initialize_memory(home)
35
+ initialize_trust(home)
36
+ initialize_security(home)
37
+ initialize_sync(home)
38
+
39
+ manifest = {"name": name, "version": "0.1.0", "created_at": "2026-01-01T00:00:00Z", "connectors": []}
40
+ (home / "manifest.json").write_text(json.dumps(manifest, indent=2))
41
+ (home / "config").mkdir(exist_ok=True)
42
+ (home / "config" / "config.yaml").write_text(yaml.dump({"agent_name": name}))
43
+
44
+
45
+ class TestGatherContext:
46
+ """Tests for gather_context() data collection."""
47
+
48
+ def test_gathers_from_initialized_agent(self, tmp_agent_home: Path):
49
+ """gather_context returns a full context dict for an initialized agent."""
50
+ _init_agent(tmp_agent_home)
51
+ store(tmp_agent_home, "Test memory for context", tags=["test"])
52
+
53
+ ctx = gather_context(tmp_agent_home)
54
+
55
+ assert "agent" in ctx
56
+ assert "pillars" in ctx
57
+ assert "board" in ctx
58
+ assert "memories" in ctx
59
+ assert "soul" in ctx
60
+ assert "mcp" in ctx
61
+ assert "gathered_at" in ctx
62
+
63
+ def test_agent_section(self, tmp_agent_home: Path):
64
+ """Agent section contains name and consciousness state."""
65
+ _init_agent(tmp_agent_home, "sovereign-ctx")
66
+ ctx = gather_context(tmp_agent_home)
67
+
68
+ assert ctx["agent"]["name"] == "sovereign-ctx"
69
+ assert "is_conscious" in ctx["agent"]
70
+ assert "fingerprint" in ctx["agent"]
71
+
72
+ def test_pillars_section(self, tmp_agent_home: Path):
73
+ """Pillars section lists all five pillars."""
74
+ _init_agent(tmp_agent_home)
75
+ ctx = gather_context(tmp_agent_home)
76
+
77
+ pillars = ctx["pillars"]
78
+ for name in ("identity", "memory", "trust", "security", "sync"):
79
+ assert name in pillars
80
+
81
+ def test_memories_included(self, tmp_agent_home: Path):
82
+ """Recent memories appear in the context."""
83
+ _init_agent(tmp_agent_home)
84
+ store(tmp_agent_home, "First context memory", tags=["alpha"])
85
+ store(tmp_agent_home, "Second context memory", tags=["beta"])
86
+
87
+ ctx = gather_context(tmp_agent_home, memory_limit=5)
88
+ assert len(ctx["memories"]) >= 2
89
+
90
+ def test_memory_limit_respected(self, tmp_agent_home: Path):
91
+ """Memory limit caps the number of memories returned."""
92
+ _init_agent(tmp_agent_home)
93
+ for i in range(10):
94
+ store(tmp_agent_home, f"Memory number {i}")
95
+
96
+ ctx = gather_context(tmp_agent_home, memory_limit=3)
97
+ assert len(ctx["memories"]) <= 3
98
+
99
+ def test_empty_agent_home(self, tmp_agent_home: Path):
100
+ """gather_context handles an uninitialized agent gracefully."""
101
+ ctx = gather_context(tmp_agent_home)
102
+ assert ctx["agent"].get("name") is not None
103
+ assert ctx["memories"] == []
104
+
105
+ def test_board_section(self, tmp_agent_home: Path):
106
+ """Board section includes task counts."""
107
+ _init_agent(tmp_agent_home)
108
+ from skcapstone.coordination import Board, Task
109
+
110
+ board = Board(tmp_agent_home)
111
+ board.ensure_dirs()
112
+ board.create_task(Task(title="Test task"))
113
+
114
+ ctx = gather_context(tmp_agent_home)
115
+ assert ctx["board"]["total"] >= 1
116
+
117
+ def test_soul_section_base(self, tmp_agent_home: Path):
118
+ """Soul section reports base when no overlay is active."""
119
+ _init_agent(tmp_agent_home)
120
+ ctx = gather_context(tmp_agent_home)
121
+ assert ctx["soul"]["active"] is None
122
+
123
+
124
+ class TestFormatText:
125
+ """Tests for plain text formatter."""
126
+
127
+ def test_contains_agent_name(self, tmp_agent_home: Path):
128
+ """Text output includes the agent name."""
129
+ _init_agent(tmp_agent_home, "text-test")
130
+ ctx = gather_context(tmp_agent_home)
131
+ output = format_text(ctx)
132
+
133
+ assert "text-test" in output
134
+ assert "SKCapstone Agent Context" in output
135
+
136
+ def test_contains_pillars(self, tmp_agent_home: Path):
137
+ """Text output lists pillar statuses."""
138
+ _init_agent(tmp_agent_home)
139
+ ctx = gather_context(tmp_agent_home)
140
+ output = format_text(ctx)
141
+
142
+ assert "Pillars" in output
143
+ assert "identity" in output
144
+ assert "memory" in output
145
+
146
+ def test_contains_memories(self, tmp_agent_home: Path):
147
+ """Text output shows recent memories."""
148
+ _init_agent(tmp_agent_home)
149
+ store(tmp_agent_home, "Remember this for text test")
150
+ ctx = gather_context(tmp_agent_home)
151
+ output = format_text(ctx)
152
+
153
+ assert "Recent Memories" in output
154
+ assert "Remember this" in output
155
+
156
+
157
+ class TestFormatJson:
158
+ """Tests for JSON formatter."""
159
+
160
+ def test_valid_json(self, tmp_agent_home: Path):
161
+ """JSON output is valid parseable JSON."""
162
+ _init_agent(tmp_agent_home)
163
+ ctx = gather_context(tmp_agent_home)
164
+ output = format_json(ctx)
165
+
166
+ parsed = json.loads(output)
167
+ assert "agent" in parsed
168
+ assert "pillars" in parsed
169
+
170
+ def test_roundtrip(self, tmp_agent_home: Path):
171
+ """JSON output can be parsed back to the original structure."""
172
+ _init_agent(tmp_agent_home)
173
+ ctx = gather_context(tmp_agent_home)
174
+ output = format_json(ctx)
175
+ parsed = json.loads(output)
176
+
177
+ assert parsed["agent"]["name"] == ctx["agent"]["name"]
178
+
179
+
180
+ class TestFormatClaudeMd:
181
+ """Tests for Claude Code CLAUDE.md formatter."""
182
+
183
+ def test_markdown_structure(self, tmp_agent_home: Path):
184
+ """CLAUDE.md has proper markdown headers."""
185
+ _init_agent(tmp_agent_home, "claude-test")
186
+ ctx = gather_context(tmp_agent_home)
187
+ output = format_claude_md(ctx)
188
+
189
+ assert "# SKCapstone Agent Context" in output
190
+ assert "## Agent Identity" in output
191
+ assert "## Pillar Status" in output
192
+ assert "## Coordination Board" in output
193
+
194
+ def test_contains_cli_reference(self, tmp_agent_home: Path):
195
+ """CLAUDE.md includes CLI command reference."""
196
+ _init_agent(tmp_agent_home)
197
+ ctx = gather_context(tmp_agent_home)
198
+ output = format_claude_md(ctx)
199
+
200
+ assert "## CLI Reference" in output
201
+ assert "skcapstone status" in output
202
+ assert "skcapstone memory" in output
203
+
204
+ def test_contains_agent_name(self, tmp_agent_home: Path):
205
+ """CLAUDE.md contains the agent name."""
206
+ _init_agent(tmp_agent_home, "claude-agent")
207
+ ctx = gather_context(tmp_agent_home)
208
+ output = format_claude_md(ctx)
209
+
210
+ assert "claude-agent" in output
211
+
212
+ def test_pillar_table(self, tmp_agent_home: Path):
213
+ """CLAUDE.md contains a pillar status table."""
214
+ _init_agent(tmp_agent_home)
215
+ ctx = gather_context(tmp_agent_home)
216
+ output = format_claude_md(ctx)
217
+
218
+ assert "| Pillar | Status |" in output
219
+ assert "identity" in output
220
+
221
+
222
+ class TestFormatCursorRules:
223
+ """Tests for Cursor .mdc rule formatter."""
224
+
225
+ def test_mdc_frontmatter(self, tmp_agent_home: Path):
226
+ """Cursor rules file has proper MDC frontmatter."""
227
+ _init_agent(tmp_agent_home)
228
+ ctx = gather_context(tmp_agent_home)
229
+ output = format_cursor_rules(ctx)
230
+
231
+ assert output.startswith("---")
232
+ assert "description:" in output
233
+ assert "alwaysApply: true" in output
234
+
235
+ def test_contains_agent_info(self, tmp_agent_home: Path):
236
+ """Cursor rules contain agent identity info."""
237
+ _init_agent(tmp_agent_home, "cursor-test")
238
+ ctx = gather_context(tmp_agent_home)
239
+ output = format_cursor_rules(ctx)
240
+
241
+ assert "cursor-test" in output
242
+
243
+
244
+ class TestFormattersRegistry:
245
+ """Tests for the FORMATTERS dict."""
246
+
247
+ def test_all_formatters_registered(self):
248
+ """All four formatters are in the registry."""
249
+ assert "text" in FORMATTERS
250
+ assert "json" in FORMATTERS
251
+ assert "claude-md" in FORMATTERS
252
+ assert "cursor-rules" in FORMATTERS
253
+
254
+ def test_all_formatters_callable(self):
255
+ """All formatters are callable."""
256
+ for name, fn in FORMATTERS.items():
257
+ assert callable(fn), f"Formatter '{name}' is not callable"
258
+
259
+
260
+ class TestGatherConsciousness:
261
+ """Tests for _gather_consciousness() fallback logic."""
262
+
263
+ def test_returns_expected_keys(self, tmp_agent_home: Path):
264
+ """_gather_consciousness returns all required keys."""
265
+ result = _gather_consciousness(tmp_agent_home)
266
+
267
+ for key in ("enabled", "backends_available", "messages_processed",
268
+ "active_conversations", "inotify_active"):
269
+ assert key in result, f"missing key: {key}"
270
+
271
+ def test_no_daemon_no_config_returns_disabled(self, tmp_agent_home: Path):
272
+ """Without daemon or config, enabled is False and lists are empty."""
273
+ from unittest.mock import patch
274
+ import urllib.error
275
+
276
+ with patch(
277
+ "urllib.request.urlopen",
278
+ side_effect=urllib.error.URLError("connection refused"),
279
+ ):
280
+ result = _gather_consciousness(tmp_agent_home)
281
+
282
+ assert result["enabled"] is False
283
+ assert result["backends_available"] == []
284
+ assert result["messages_processed"] == 0
285
+ assert result["active_conversations"] == 0
286
+ assert result["inotify_active"] is False
287
+
288
+ def test_config_file_triggers_enabled(self, tmp_agent_home: Path):
289
+ """If consciousness.yaml exists, enabled is True (config fallback)."""
290
+ config_dir = tmp_agent_home / "config"
291
+ config_dir.mkdir(exist_ok=True)
292
+ (config_dir / "consciousness.yaml").write_text("enabled: true\n")
293
+
294
+ result = _gather_consciousness(tmp_agent_home)
295
+
296
+ assert result["enabled"] is True
297
+
298
+ def test_gather_context_includes_consciousness_key(self, tmp_agent_home: Path):
299
+ """gather_context() output includes the 'consciousness' key."""
300
+ ctx = gather_context(tmp_agent_home)
301
+ assert "consciousness" in ctx
302
+
303
+ def test_format_text_includes_consciousness_section(self, tmp_agent_home: Path):
304
+ """format_text() output includes a Consciousness section."""
305
+ _init_agent(tmp_agent_home)
306
+ ctx = gather_context(tmp_agent_home)
307
+ output = format_text(ctx)
308
+
309
+ assert "Consciousness" in output
310
+ assert "Status:" in output
@@ -0,0 +1,306 @@
1
+ """Tests for the conversation API endpoints in the daemon HTTP server.
2
+
3
+ Covers:
4
+ GET /api/v1/conversations — list all peers
5
+ GET /api/v1/conversations/{peer} — full history for a peer
6
+ POST /api/v1/conversations/{peer}/send — send message, write to outbox
7
+ DELETE /api/v1/conversations/{peer} — clear history
8
+ Path-traversal sanitization
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import socket
15
+ import time
16
+ import threading
17
+ import urllib.error
18
+ import urllib.request
19
+ from pathlib import Path
20
+ from unittest.mock import patch
21
+
22
+ import pytest
23
+
24
+ from skcapstone.daemon import DaemonConfig, DaemonService, _sanitize_peer
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Helpers
29
+ # ---------------------------------------------------------------------------
30
+
31
+ def _find_free_port() -> int:
32
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
33
+ s.bind(("127.0.0.1", 0))
34
+ return s.getsockname()[1]
35
+
36
+
37
+ def _api(port: int, path: str, *, method: str = "GET", body: bytes | None = None) -> tuple[int, dict]:
38
+ url = f"http://127.0.0.1:{port}{path}"
39
+ req = urllib.request.Request(url, data=body, method=method)
40
+ if body is not None:
41
+ req.add_header("Content-Type", "application/json")
42
+ req.add_header("Content-Length", str(len(body)))
43
+ try:
44
+ with urllib.request.urlopen(req, timeout=3) as resp:
45
+ return resp.status, json.loads(resp.read())
46
+ except urllib.error.HTTPError as exc:
47
+ return exc.code, json.loads(exc.read())
48
+
49
+
50
+ @pytest.fixture
51
+ def conv_home(tmp_path):
52
+ """Agent home with conversations directory pre-populated."""
53
+ home = tmp_path / ".skcapstone"
54
+ (home / "logs").mkdir(parents=True)
55
+ conv_dir = home / "conversations"
56
+ conv_dir.mkdir()
57
+
58
+ # Two peers with conversation history
59
+ alice = [
60
+ {"role": "user", "content": "hello alice", "timestamp": "2026-01-01T10:00:00+00:00"},
61
+ {"role": "assistant", "content": "hi there", "timestamp": "2026-01-01T10:00:01+00:00"},
62
+ ]
63
+ bob = [
64
+ {"role": "user", "content": "hey bob", "timestamp": "2026-01-02T12:00:00+00:00"},
65
+ ]
66
+ (conv_dir / "alice.json").write_text(json.dumps(alice), encoding="utf-8")
67
+ (conv_dir / "bob.json").write_text(json.dumps(bob), encoding="utf-8")
68
+ return home
69
+
70
+
71
+ @pytest.fixture
72
+ def live_server(conv_home):
73
+ """Start the daemon API server, yield (svc, port), then stop."""
74
+ config = DaemonConfig(
75
+ home=conv_home,
76
+ shared_root=conv_home, # keep test data isolated from real ~/.skcapstone
77
+ port=_find_free_port(),
78
+ poll_interval=60,
79
+ )
80
+ svc = DaemonService(config)
81
+ svc.state.running = True
82
+
83
+ with patch.object(svc, "_load_components"):
84
+ svc._write_pid()
85
+ svc._start_api_server()
86
+ time.sleep(0.4)
87
+ yield svc, config.port
88
+ svc.stop()
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Unit tests: _sanitize_peer
93
+ # ---------------------------------------------------------------------------
94
+
95
+ class TestSanitizePeer:
96
+ def test_normal_name(self):
97
+ assert _sanitize_peer("alice") == "alice"
98
+
99
+ def test_strips_slashes(self):
100
+ assert "/" not in _sanitize_peer("../etc/passwd")
101
+ assert "\\" not in _sanitize_peer("..\\windows")
102
+
103
+ def test_path_traversal(self):
104
+ result = _sanitize_peer("../../etc/passwd")
105
+ assert ".." not in result
106
+ assert "/" not in result
107
+
108
+ def test_empty_string(self):
109
+ assert _sanitize_peer("") == ""
110
+
111
+ def test_none_returns_empty(self):
112
+ assert _sanitize_peer(None) == "" # type: ignore[arg-type]
113
+
114
+ def test_max_length(self):
115
+ long = "a" * 100
116
+ assert len(_sanitize_peer(long)) <= 64
117
+
118
+ def test_allowed_chars(self):
119
+ assert _sanitize_peer("user@domain.io") == "user@domain.io"
120
+ assert _sanitize_peer("my-peer_01") == "my-peer_01"
121
+
122
+ def test_null_bytes_stripped(self):
123
+ assert "\x00" not in _sanitize_peer("evil\x00peer")
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Integration tests: GET /api/v1/conversations
128
+ # ---------------------------------------------------------------------------
129
+
130
+ class TestListConversations:
131
+ def test_returns_list(self, live_server):
132
+ svc, port = live_server
133
+ status, data = _api(port, "/api/v1/conversations")
134
+ assert status == 200
135
+ assert "conversations" in data
136
+ assert isinstance(data["conversations"], list)
137
+
138
+ def test_includes_both_peers(self, live_server):
139
+ svc, port = live_server
140
+ _, data = _api(port, "/api/v1/conversations")
141
+ peers = {c["peer"] for c in data["conversations"]}
142
+ assert "alice" in peers
143
+ assert "bob" in peers
144
+
145
+ def test_message_count(self, live_server):
146
+ svc, port = live_server
147
+ _, data = _api(port, "/api/v1/conversations")
148
+ alice = next(c for c in data["conversations"] if c["peer"] == "alice")
149
+ assert alice["message_count"] == 2
150
+
151
+ def test_last_message_time_present(self, live_server):
152
+ svc, port = live_server
153
+ _, data = _api(port, "/api/v1/conversations")
154
+ alice = next(c for c in data["conversations"] if c["peer"] == "alice")
155
+ assert "last_message_time" in alice
156
+ assert alice["last_message_time"] is not None
157
+
158
+ def test_last_message_preview_present(self, live_server):
159
+ svc, port = live_server
160
+ _, data = _api(port, "/api/v1/conversations")
161
+ alice = next(c for c in data["conversations"] if c["peer"] == "alice")
162
+ assert "last_message_preview" in alice
163
+ assert isinstance(alice["last_message_preview"], str)
164
+
165
+ def test_empty_conversations_dir(self, tmp_path):
166
+ home = tmp_path / ".skcapstone"
167
+ (home / "logs").mkdir(parents=True)
168
+ (home / "conversations").mkdir()
169
+ config = DaemonConfig(
170
+ home=home,
171
+ shared_root=home,
172
+ port=_find_free_port(),
173
+ poll_interval=60,
174
+ )
175
+ svc = DaemonService(config)
176
+ svc.state.running = True
177
+ with patch.object(svc, "_load_components"):
178
+ svc._write_pid()
179
+ svc._start_api_server()
180
+ time.sleep(0.4)
181
+ try:
182
+ _, data = _api(config.port, "/api/v1/conversations")
183
+ assert data["conversations"] == []
184
+ finally:
185
+ svc.stop()
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Integration tests: GET /api/v1/conversations/{peer}
190
+ # ---------------------------------------------------------------------------
191
+
192
+ class TestGetConversation:
193
+ def test_existing_peer(self, live_server):
194
+ svc, port = live_server
195
+ status, data = _api(port, "/api/v1/conversations/alice")
196
+ assert status == 200
197
+ assert data["peer"] == "alice"
198
+ assert len(data["messages"]) == 2
199
+
200
+ def test_missing_peer_404(self, live_server):
201
+ svc, port = live_server
202
+ status, _ = _api(port, "/api/v1/conversations/nobody")
203
+ assert status == 404
204
+
205
+ def test_path_traversal_rejected(self, live_server):
206
+ svc, port = live_server
207
+ # After sanitization "../../etc/passwd" → "etcpasswd" which doesn't exist → 404 or 400
208
+ status, _ = _api(port, "/api/v1/conversations/../../etc/passwd")
209
+ assert status in (400, 404)
210
+
211
+ def test_get_on_send_returns_405(self, live_server):
212
+ svc, port = live_server
213
+ status, data = _api(port, "/api/v1/conversations/alice/send")
214
+ assert status == 405
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Integration tests: POST /api/v1/conversations/{peer}/send
219
+ # ---------------------------------------------------------------------------
220
+
221
+ class TestSendMessage:
222
+ def test_send_returns_sent(self, live_server):
223
+ svc, port = live_server
224
+ body = json.dumps({"content": "hello world"}).encode()
225
+ status, data = _api(port, "/api/v1/conversations/alice/send", method="POST", body=body)
226
+ assert status == 200
227
+ assert data["status"] == "sent"
228
+ assert "message_id" in data
229
+
230
+ def test_send_writes_outbox_file(self, live_server, conv_home):
231
+ svc, port = live_server
232
+ body = json.dumps({"content": "test outbox write"}).encode()
233
+ status, resp = _api(port, "/api/v1/conversations/alice/send", method="POST", body=body)
234
+ assert status == 200, resp
235
+ msg_id = resp["message_id"]
236
+ # shared_root == conv_home in tests
237
+ outbox = svc.config.shared_root / "sync" / "comms" / "outbox" / f"{msg_id}.skc.json"
238
+ assert outbox.exists(), f"Outbox file not created: {outbox}"
239
+ envelope = json.loads(outbox.read_text())
240
+ assert envelope["recipient"] == "alice"
241
+ assert envelope["payload"]["content"] == "test outbox write"
242
+
243
+ def test_send_missing_content_400(self, live_server):
244
+ svc, port = live_server
245
+ body = json.dumps({"content": ""}).encode()
246
+ status, data = _api(port, "/api/v1/conversations/alice/send", method="POST", body=body)
247
+ assert status == 400
248
+ assert "content" in data["error"].lower()
249
+
250
+ def test_send_invalid_json_400(self, live_server):
251
+ svc, port = live_server
252
+ status, data = _api(
253
+ port, "/api/v1/conversations/alice/send", method="POST", body=b"not-json"
254
+ )
255
+ assert status == 400
256
+
257
+ def test_send_path_traversal_rejected(self, live_server):
258
+ svc, port = live_server
259
+ body = json.dumps({"content": "hi"}).encode()
260
+ # URL path traversal: peer sanitizes to empty or benign string
261
+ status, _ = _api(
262
+ port, "/api/v1/conversations/../../evil/send", method="POST", body=body
263
+ )
264
+ # Either 400 (invalid peer) or 200 (sanitized to "evil" which is fine) — not a server error
265
+ assert status in (200, 400)
266
+
267
+ def test_send_unique_message_ids(self, live_server):
268
+ svc, port = live_server
269
+ body = json.dumps({"content": "msg"}).encode()
270
+ ids = set()
271
+ for _ in range(5):
272
+ _, resp = _api(port, "/api/v1/conversations/alice/send", method="POST", body=body)
273
+ ids.add(resp["message_id"])
274
+ assert len(ids) == 5, "message_id should be unique per send"
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # Integration tests: DELETE /api/v1/conversations/{peer}
279
+ # ---------------------------------------------------------------------------
280
+
281
+ class TestDeleteConversation:
282
+ def test_delete_existing_peer(self, live_server, conv_home):
283
+ svc, port = live_server
284
+ assert (conv_home / "conversations" / "bob.json").exists()
285
+ status, data = _api(port, "/api/v1/conversations/bob", method="DELETE")
286
+ assert status == 200
287
+ assert data["status"] == "deleted"
288
+ assert not (conv_home / "conversations" / "bob.json").exists()
289
+
290
+ def test_delete_missing_peer_404(self, live_server):
291
+ svc, port = live_server
292
+ status, _ = _api(port, "/api/v1/conversations/nobody", method="DELETE")
293
+ assert status == 404
294
+
295
+ def test_delete_invalid_peer_400(self, live_server):
296
+ svc, port = live_server
297
+ status, _ = _api(port, "/api/v1/conversations/", method="DELETE")
298
+ assert status in (400, 404)
299
+
300
+ def test_delete_does_not_affect_other_peers(self, live_server, conv_home):
301
+ svc, port = live_server
302
+ _api(port, "/api/v1/conversations/bob", method="DELETE")
303
+ # alice should still exist
304
+ assert (conv_home / "conversations" / "alice.json").exists()
305
+ status, _ = _api(port, "/api/v1/conversations/alice")
306
+ assert status == 200