@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,832 @@
1
+ """Tests for the skcapstone FUSE mount module.
2
+
3
+ Covers helper functions, the SovereignFS virtual filesystem class, and the
4
+ FUSEDaemon lifecycle manager.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import errno
10
+ import json
11
+ import os
12
+ import stat
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Any, Dict
16
+ from unittest.mock import MagicMock, patch
17
+
18
+ import pytest
19
+
20
+ from skcapstone.fuse_mount import (
21
+ FUSEDaemon,
22
+ SovereignFS,
23
+ _build_fingerprint_txt,
24
+ _build_identity_card,
25
+ _dir_stat,
26
+ _file_stat,
27
+ _list_coordination_tasks,
28
+ _list_documents,
29
+ _list_inbox,
30
+ _list_memory_ids,
31
+ _load_memory_file,
32
+ _memory_to_markdown,
33
+ _parse_path,
34
+ _read_coordination_task,
35
+ _read_document,
36
+ _read_inbox_file,
37
+ _send_via_skcomm,
38
+ )
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Fixtures
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ @pytest.fixture
47
+ def agent_home(tmp_path: Path) -> Path:
48
+ """Provide a minimal agent home directory for FUSE tests."""
49
+ home = tmp_path / ".skcapstone"
50
+ home.mkdir()
51
+ return home
52
+
53
+
54
+ @pytest.fixture
55
+ def memory_dir(agent_home: Path) -> Path:
56
+ """Provide a memory directory with short/mid/long-term subdirs."""
57
+ mem = agent_home / "memory"
58
+ mem.mkdir()
59
+ for layer in ("short-term", "mid-term", "long-term"):
60
+ (mem / layer).mkdir()
61
+ return mem
62
+
63
+
64
+ @pytest.fixture
65
+ def sample_memory() -> Dict[str, Any]:
66
+ """Return a sample memory dict with all typical fields populated."""
67
+ return {
68
+ "memory_id": "abc123",
69
+ "created_at": "2026-02-28T12:00:00+00:00",
70
+ "layer": "short-term",
71
+ "importance": 0.85,
72
+ "tags": ["test", "fuse"],
73
+ "soul_context": "lumina",
74
+ "source": "mcp",
75
+ "content": "This is a test memory for FUSE.",
76
+ "metadata": {"origin": "test_suite", "version": "1"},
77
+ }
78
+
79
+
80
+ @pytest.fixture
81
+ def sovereign_fs(agent_home: Path, memory_dir: Path) -> SovereignFS:
82
+ """Provide a SovereignFS instance backed by a tmp agent home."""
83
+ return SovereignFS(agent_home=agent_home)
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # TestParsePath
88
+ # ---------------------------------------------------------------------------
89
+
90
+
91
+ class TestParsePath:
92
+ """Tests for _parse_path helper that splits virtual FS paths."""
93
+
94
+ def test_root_path(self) -> None:
95
+ """Root path returns an empty tuple."""
96
+ assert _parse_path("/") == ()
97
+
98
+ def test_single_component(self) -> None:
99
+ """Single-level path returns a one-element tuple."""
100
+ assert _parse_path("/memories") == ("memories",)
101
+
102
+ def test_multi_component(self) -> None:
103
+ """Multi-level path returns all components."""
104
+ assert _parse_path("/memories/short/abc123.md") == (
105
+ "memories",
106
+ "short",
107
+ "abc123.md",
108
+ )
109
+
110
+ def test_trailing_slash_stripped(self) -> None:
111
+ """Trailing slashes are removed and do not produce empty elements."""
112
+ assert _parse_path("/inbox/") == ("inbox",)
113
+
114
+ def test_double_slashes_ignored(self) -> None:
115
+ """Consecutive slashes do not produce empty strings."""
116
+ assert _parse_path("//memories//short//") == ("memories", "short")
117
+
118
+ def test_empty_string(self) -> None:
119
+ """Empty string returns an empty tuple like root."""
120
+ assert _parse_path("") == ()
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # TestStatHelpers
125
+ # ---------------------------------------------------------------------------
126
+
127
+
128
+ class TestStatHelpers:
129
+ """Tests for _dir_stat and _file_stat stat-dict builders."""
130
+
131
+ def test_dir_stat_mode_is_directory(self) -> None:
132
+ """_dir_stat sets the S_IFDIR flag in st_mode."""
133
+ result = _dir_stat()
134
+ assert result["st_mode"] & stat.S_IFDIR
135
+
136
+ def test_dir_stat_default_nlink(self) -> None:
137
+ """Default nlink for a directory is 2."""
138
+ result = _dir_stat()
139
+ assert result["st_nlink"] == 2
140
+
141
+ def test_dir_stat_custom_nlink(self) -> None:
142
+ """Custom nlink value is honoured."""
143
+ result = _dir_stat(nlink=5)
144
+ assert result["st_nlink"] == 5
145
+
146
+ def test_dir_stat_permissions(self) -> None:
147
+ """Directories have 0o555 permission bits (r-xr-xr-x)."""
148
+ result = _dir_stat()
149
+ perms = result["st_mode"] & 0o777
150
+ assert perms == 0o555
151
+
152
+ def test_dir_stat_uid_gid(self) -> None:
153
+ """Directory uid/gid match the current process."""
154
+ result = _dir_stat()
155
+ assert result["st_uid"] == os.getuid()
156
+ assert result["st_gid"] == os.getgid()
157
+
158
+ def test_file_stat_mode_is_regular(self) -> None:
159
+ """_file_stat sets the S_IFREG flag in st_mode."""
160
+ result = _file_stat(size=42)
161
+ assert result["st_mode"] & stat.S_IFREG
162
+
163
+ def test_file_stat_readonly_permissions(self) -> None:
164
+ """Read-only files have 0o444 permission bits."""
165
+ result = _file_stat(size=10, writable=False)
166
+ perms = result["st_mode"] & 0o777
167
+ assert perms == 0o444
168
+
169
+ def test_file_stat_writable_permissions(self) -> None:
170
+ """Writable files have 0o644 permission bits."""
171
+ result = _file_stat(size=10, writable=True)
172
+ perms = result["st_mode"] & 0o777
173
+ assert perms == 0o644
174
+
175
+ def test_file_stat_size(self) -> None:
176
+ """File size is correctly stored in st_size."""
177
+ result = _file_stat(size=1024)
178
+ assert result["st_size"] == 1024
179
+
180
+ def test_file_stat_nlink_always_one(self) -> None:
181
+ """Regular files always have nlink == 1."""
182
+ result = _file_stat(size=0)
183
+ assert result["st_nlink"] == 1
184
+
185
+ def test_file_stat_timestamps_present(self) -> None:
186
+ """All three timestamps are numeric and recent."""
187
+ result = _file_stat(size=0)
188
+ now = time.time()
189
+ for key in ("st_atime", "st_mtime", "st_ctime"):
190
+ assert isinstance(result[key], float)
191
+ assert abs(result[key] - now) < 5
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # TestMemoryMarkdown
196
+ # ---------------------------------------------------------------------------
197
+
198
+
199
+ class TestMemoryMarkdown:
200
+ """Tests for _memory_to_markdown that renders memory dicts as Markdown."""
201
+
202
+ def test_basic_fields(self, sample_memory: Dict[str, Any]) -> None:
203
+ """All standard fields appear in the rendered output."""
204
+ md = _memory_to_markdown(sample_memory).decode("utf-8")
205
+ assert "# Memory: abc123" in md
206
+ assert "**Created:** 2026-02-28T12:00:00+00:00" in md
207
+ assert "**Layer:** short-term" in md
208
+ assert "**Importance:** 0.85" in md
209
+ assert "**Tags:** test, fuse" in md
210
+ assert "**Soul:** lumina" in md
211
+ assert "**Source:** mcp" in md
212
+ assert "## Content" in md
213
+ assert "This is a test memory for FUSE." in md
214
+
215
+ def test_metadata_section(self, sample_memory: Dict[str, Any]) -> None:
216
+ """Metadata dict is rendered as a bullet list under ## Metadata."""
217
+ md = _memory_to_markdown(sample_memory).decode("utf-8")
218
+ assert "## Metadata" in md
219
+ assert "- **origin:** test_suite" in md
220
+ assert "- **version:** 1" in md
221
+
222
+ def test_missing_optional_fields(self) -> None:
223
+ """Minimal memory with only memory_id and content still renders."""
224
+ minimal: Dict[str, Any] = {
225
+ "memory_id": "min1",
226
+ "content": "Minimal memory.",
227
+ }
228
+ md = _memory_to_markdown(minimal).decode("utf-8")
229
+ assert "# Memory: min1" in md
230
+ assert "Minimal memory." in md
231
+ # Optional fields should not appear
232
+ assert "**Layer:**" not in md
233
+ assert "**Tags:**" not in md
234
+
235
+ def test_empty_tags_excluded(self) -> None:
236
+ """An empty tags list does not produce a Tags line."""
237
+ mem: Dict[str, Any] = {"memory_id": "t1", "content": "x", "tags": []}
238
+ md = _memory_to_markdown(mem).decode("utf-8")
239
+ assert "**Tags:**" not in md
240
+
241
+ def test_importance_format(self) -> None:
242
+ """Importance is formatted to two decimal places."""
243
+ mem: Dict[str, Any] = {"memory_id": "t2", "content": "x", "importance": 0.5}
244
+ md = _memory_to_markdown(mem).decode("utf-8")
245
+ assert "**Importance:** 0.50" in md
246
+
247
+ def test_returns_bytes(self, sample_memory: Dict[str, Any]) -> None:
248
+ """Return type is bytes (UTF-8 encoded)."""
249
+ result = _memory_to_markdown(sample_memory)
250
+ assert isinstance(result, bytes)
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # TestFileHelpers
255
+ # ---------------------------------------------------------------------------
256
+
257
+
258
+ class TestFileHelpers:
259
+ """Tests for file listing and reading helpers that work with on-disk data."""
260
+
261
+ def test_list_memory_ids_empty_layer(self, memory_dir: Path) -> None:
262
+ """Empty layer directory returns an empty list."""
263
+ ids = _list_memory_ids(memory_dir, "short-term")
264
+ assert ids == []
265
+
266
+ def test_list_memory_ids_sorted(self, memory_dir: Path) -> None:
267
+ """Memory IDs are returned sorted alphabetically."""
268
+ layer = memory_dir / "short-term"
269
+ for name in ("zzz", "aaa", "mmm"):
270
+ (layer / f"{name}.json").write_text("{}")
271
+ ids = _list_memory_ids(memory_dir, "short-term")
272
+ assert ids == ["aaa", "mmm", "zzz"]
273
+
274
+ def test_list_memory_ids_nonexistent_layer(self, memory_dir: Path) -> None:
275
+ """Non-existent layer directory returns an empty list."""
276
+ ids = _list_memory_ids(memory_dir, "nonexistent")
277
+ assert ids == []
278
+
279
+ def test_load_memory_file_success(
280
+ self, memory_dir: Path, sample_memory: Dict[str, Any]
281
+ ) -> None:
282
+ """A valid memory JSON loads and renders as Markdown bytes."""
283
+ layer_dir = memory_dir / "short-term"
284
+ (layer_dir / "abc123.json").write_text(
285
+ json.dumps(sample_memory), encoding="utf-8"
286
+ )
287
+ result = _load_memory_file(memory_dir, "short-term", "abc123")
288
+ assert result is not None
289
+ assert b"# Memory: abc123" in result
290
+
291
+ def test_load_memory_file_missing(self, memory_dir: Path) -> None:
292
+ """Missing memory file returns None."""
293
+ result = _load_memory_file(memory_dir, "short-term", "nonexistent")
294
+ assert result is None
295
+
296
+ def test_load_memory_file_invalid_json(self, memory_dir: Path) -> None:
297
+ """Corrupt JSON returns None instead of raising."""
298
+ layer_dir = memory_dir / "short-term"
299
+ (layer_dir / "bad.json").write_text("{not valid json", encoding="utf-8")
300
+ result = _load_memory_file(memory_dir, "short-term", "bad")
301
+ assert result is None
302
+
303
+ def test_list_inbox_empty(self, agent_home: Path) -> None:
304
+ """No inbox directory returns an empty list."""
305
+ assert _list_inbox(agent_home) == []
306
+
307
+ def test_list_inbox_with_files(self, agent_home: Path) -> None:
308
+ """Inbox files are listed and sorted."""
309
+ inbox = agent_home / "comms" / "inbox"
310
+ inbox.mkdir(parents=True)
311
+ (inbox / "msg_002.json").write_text("{}")
312
+ (inbox / "msg_001.json").write_text("{}")
313
+ result = _list_inbox(agent_home)
314
+ assert result == ["msg_001.json", "msg_002.json"]
315
+
316
+ def test_read_inbox_file_success(self, agent_home: Path) -> None:
317
+ """Reading a valid inbox file returns its bytes."""
318
+ inbox = agent_home / "comms" / "inbox"
319
+ inbox.mkdir(parents=True)
320
+ (inbox / "test.msg").write_bytes(b"Hello from sender")
321
+ result = _read_inbox_file(agent_home, "test.msg")
322
+ assert result == b"Hello from sender"
323
+
324
+ def test_read_inbox_file_missing(self, agent_home: Path) -> None:
325
+ """Reading a missing inbox file returns None."""
326
+ assert _read_inbox_file(agent_home, "ghost.msg") is None
327
+
328
+ def test_list_documents_empty(self, agent_home: Path) -> None:
329
+ """No documents directory returns an empty list."""
330
+ assert _list_documents(agent_home) == []
331
+
332
+ def test_list_documents_with_files(self, agent_home: Path) -> None:
333
+ """Document files are listed and sorted."""
334
+ docs = agent_home / "documents"
335
+ docs.mkdir()
336
+ (docs / "contract_b.pdf").write_bytes(b"pdf")
337
+ (docs / "contract_a.pdf").write_bytes(b"pdf")
338
+ result = _list_documents(agent_home)
339
+ assert result == ["contract_a.pdf", "contract_b.pdf"]
340
+
341
+ def test_read_document_success(self, agent_home: Path) -> None:
342
+ """A valid document is returned as bytes."""
343
+ docs = agent_home / "documents"
344
+ docs.mkdir()
345
+ (docs / "doc.txt").write_bytes(b"Signed content")
346
+ assert _read_document(agent_home, "doc.txt") == b"Signed content"
347
+
348
+ def test_read_document_missing(self, agent_home: Path) -> None:
349
+ """Missing document returns None."""
350
+ assert _read_document(agent_home, "nope.txt") is None
351
+
352
+ def test_list_coordination_tasks_empty(self, agent_home: Path) -> None:
353
+ """No coordination directory returns an empty list."""
354
+ assert _list_coordination_tasks(agent_home) == []
355
+
356
+ def test_list_coordination_tasks_with_files(self, agent_home: Path) -> None:
357
+ """Coordination task files are listed and sorted."""
358
+ tasks = agent_home / "coordination" / "tasks"
359
+ tasks.mkdir(parents=True)
360
+ (tasks / "task_02.json").write_text("{}")
361
+ (tasks / "task_01.json").write_text("{}")
362
+ result = _list_coordination_tasks(agent_home)
363
+ assert result == ["task_01.json", "task_02.json"]
364
+
365
+ def test_read_coordination_task_success(self, agent_home: Path) -> None:
366
+ """A valid task JSON file is returned as bytes."""
367
+ tasks = agent_home / "coordination" / "tasks"
368
+ tasks.mkdir(parents=True)
369
+ payload = json.dumps({"title": "Test task"})
370
+ (tasks / "task_01.json").write_text(payload, encoding="utf-8")
371
+ result = _read_coordination_task(agent_home, "task_01.json")
372
+ assert result is not None
373
+ assert b"Test task" in result
374
+
375
+ def test_read_coordination_task_missing(self, agent_home: Path) -> None:
376
+ """Missing task file returns None."""
377
+ assert _read_coordination_task(agent_home, "ghost.json") is None
378
+
379
+ def test_build_identity_card_fallback(self, agent_home: Path) -> None:
380
+ """Without CapAuth or manifest, falls back to unknown card."""
381
+ with patch("skcapstone.fuse_mount.Path.expanduser", return_value=Path("/nonexistent")):
382
+ card_bytes = _build_identity_card(agent_home)
383
+ card = json.loads(card_bytes)
384
+ assert card["name"] == "unknown"
385
+ assert card["source"] == "fallback"
386
+
387
+ def test_build_identity_card_from_manifest(self, agent_home: Path) -> None:
388
+ """Identity card loads from manifest.json when CapAuth is absent."""
389
+ manifest = {
390
+ "name": "opus",
391
+ "identity": {"fingerprint": "DEADBEEF"},
392
+ "created_at": "2026-01-01",
393
+ }
394
+ (agent_home / "manifest.json").write_text(
395
+ json.dumps(manifest), encoding="utf-8"
396
+ )
397
+ # Ensure CapAuth profile path does not exist
398
+ fake_capauth = agent_home / "no_capauth"
399
+ with patch(
400
+ "skcapstone.fuse_mount.Path.expanduser",
401
+ return_value=fake_capauth,
402
+ ):
403
+ card_bytes = _build_identity_card(agent_home)
404
+ card = json.loads(card_bytes)
405
+ assert card["name"] == "opus"
406
+ assert card["fingerprint"] == "DEADBEEF"
407
+ assert card["source"] == "manifest"
408
+
409
+ def test_build_fingerprint_txt_fallback(self, agent_home: Path) -> None:
410
+ """Without CapAuth or manifest, returns placeholder text."""
411
+ with patch("skcapstone.fuse_mount.Path.expanduser", return_value=Path("/nonexistent")):
412
+ fp = _build_fingerprint_txt(agent_home)
413
+ assert fp == b"(no fingerprint)\n"
414
+
415
+ def test_build_fingerprint_txt_from_manifest(self, agent_home: Path) -> None:
416
+ """Fingerprint is extracted from manifest.json."""
417
+ manifest = {"identity": {"fingerprint": "AABBCCDD"}}
418
+ (agent_home / "manifest.json").write_text(
419
+ json.dumps(manifest), encoding="utf-8"
420
+ )
421
+ fake_capauth = agent_home / "no_capauth"
422
+ with patch(
423
+ "skcapstone.fuse_mount.Path.expanduser",
424
+ return_value=fake_capauth,
425
+ ):
426
+ fp = _build_fingerprint_txt(agent_home)
427
+ assert fp == b"AABBCCDD\n"
428
+
429
+ def test_send_via_skcomm_fallback_to_outbox(self, agent_home: Path) -> None:
430
+ """When CLI is unavailable, message is queued as JSON envelope in outbox."""
431
+ with patch("skcapstone.fuse_mount.subprocess.run", side_effect=FileNotFoundError):
432
+ result = _send_via_skcomm(agent_home, "jarvis", "Hello Jarvis")
433
+ assert result is True
434
+ outbox = agent_home / "comms" / "outbox"
435
+ files = list(outbox.glob("jarvis_*.json"))
436
+ assert len(files) == 1
437
+ envelope = json.loads(files[0].read_text(encoding="utf-8"))
438
+ assert envelope["recipient"] == "jarvis"
439
+ assert envelope["message"] == "Hello Jarvis"
440
+ assert envelope["delivered"] is False
441
+
442
+
443
+ # ---------------------------------------------------------------------------
444
+ # TestSovereignFS
445
+ # ---------------------------------------------------------------------------
446
+
447
+
448
+ class TestSovereignFS:
449
+ """Tests for the SovereignFS FUSE operations class."""
450
+
451
+ # -- getattr -----------------------------------------------------------
452
+
453
+ def test_getattr_root_is_directory(self, sovereign_fs: SovereignFS) -> None:
454
+ """Root path returns directory stat attributes."""
455
+ result = sovereign_fs.getattr("/")
456
+ assert result["st_mode"] & stat.S_IFDIR
457
+
458
+ def test_getattr_top_level_dir(self, sovereign_fs: SovereignFS) -> None:
459
+ """Top-level virtual dirs like /memories are directories."""
460
+ result = sovereign_fs.getattr("/memories")
461
+ assert result["st_mode"] & stat.S_IFDIR
462
+
463
+ def test_getattr_memories_nlink(self, sovereign_fs: SovereignFS) -> None:
464
+ """The /memories dir has nlink == 2 + number of memory subdirs (3)."""
465
+ result = sovereign_fs.getattr("/memories")
466
+ assert result["st_nlink"] == 5 # 2 + short, mid, long
467
+
468
+ def test_getattr_memory_subdir(self, sovereign_fs: SovereignFS) -> None:
469
+ """/memories/short is a valid directory."""
470
+ result = sovereign_fs.getattr("/memories/short")
471
+ assert result["st_mode"] & stat.S_IFDIR
472
+
473
+ def test_getattr_identity_file(self, sovereign_fs: SovereignFS) -> None:
474
+ """/identity/fingerprint.txt is a regular file."""
475
+ result = sovereign_fs.getattr("/identity/fingerprint.txt")
476
+ assert result["st_mode"] & stat.S_IFREG
477
+
478
+ def test_getattr_enoent(self, sovereign_fs: SovereignFS) -> None:
479
+ """Non-existent path raises OSError with ENOENT."""
480
+ with pytest.raises(OSError) as exc_info:
481
+ sovereign_fs.getattr("/does_not_exist")
482
+ assert exc_info.value.errno == errno.ENOENT
483
+
484
+ def test_getattr_outbox_file_is_writable(
485
+ self, sovereign_fs: SovereignFS
486
+ ) -> None:
487
+ """Outbox files have writable permission bits."""
488
+ # Seed a buffer so the file exists in the virtual FS
489
+ sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] = b"hello"
490
+ result = sovereign_fs.getattr("/outbox/jarvis.msg")
491
+ perms = result["st_mode"] & 0o777
492
+ assert perms == 0o644
493
+
494
+ # -- readdir -----------------------------------------------------------
495
+
496
+ def test_readdir_root(self, sovereign_fs: SovereignFS) -> None:
497
+ """Root listing includes dot entries and all top-level dirs."""
498
+ entries = sovereign_fs.readdir("/", fh=0)
499
+ assert "." in entries
500
+ assert ".." in entries
501
+ for d in ("memories", "documents", "identity", "inbox", "outbox", "coordination"):
502
+ assert d in entries
503
+
504
+ def test_readdir_memories(self, sovereign_fs: SovereignFS) -> None:
505
+ """/memories lists the three layer subdirs."""
506
+ entries = sovereign_fs.readdir("/memories", fh=0)
507
+ assert "short" in entries
508
+ assert "mid" in entries
509
+ assert "long" in entries
510
+
511
+ def test_readdir_memories_short_with_files(
512
+ self,
513
+ sovereign_fs: SovereignFS,
514
+ memory_dir: Path,
515
+ sample_memory: Dict[str, Any],
516
+ ) -> None:
517
+ """/memories/short lists .md files for each memory."""
518
+ layer_dir = memory_dir / "short-term"
519
+ (layer_dir / "mem001.json").write_text(
520
+ json.dumps(sample_memory), encoding="utf-8"
521
+ )
522
+ entries = sovereign_fs.readdir("/memories/short", fh=0)
523
+ assert "mem001.md" in entries
524
+
525
+ def test_readdir_identity(self, sovereign_fs: SovereignFS) -> None:
526
+ """/identity lists card.json and fingerprint.txt."""
527
+ entries = sovereign_fs.readdir("/identity", fh=0)
528
+ assert "card.json" in entries
529
+ assert "fingerprint.txt" in entries
530
+
531
+ def test_readdir_inbox(self, sovereign_fs: SovereignFS, agent_home: Path) -> None:
532
+ """/inbox lists files in the comms/inbox directory."""
533
+ inbox = agent_home / "comms" / "inbox"
534
+ inbox.mkdir(parents=True)
535
+ (inbox / "msg_from_ava.json").write_text("{}")
536
+ entries = sovereign_fs.readdir("/inbox", fh=0)
537
+ assert "msg_from_ava.json" in entries
538
+
539
+ def test_readdir_outbox_shows_buffered(self, sovereign_fs: SovereignFS) -> None:
540
+ """/outbox lists files that are in the outbox write buffer."""
541
+ sovereign_fs._outbox_buffers["/outbox/lumina.msg"] = b"pending"
542
+ entries = sovereign_fs.readdir("/outbox", fh=0)
543
+ assert "lumina.msg" in entries
544
+
545
+ def test_readdir_documents(self, sovereign_fs: SovereignFS, agent_home: Path) -> None:
546
+ """/documents lists files in the documents directory."""
547
+ docs = agent_home / "documents"
548
+ docs.mkdir()
549
+ (docs / "signed.pdf").write_bytes(b"data")
550
+ entries = sovereign_fs.readdir("/documents", fh=0)
551
+ assert "signed.pdf" in entries
552
+
553
+ def test_readdir_coordination(self, sovereign_fs: SovereignFS, agent_home: Path) -> None:
554
+ """/coordination lists task JSON files."""
555
+ tasks = agent_home / "coordination" / "tasks"
556
+ tasks.mkdir(parents=True)
557
+ (tasks / "task_abc.json").write_text("{}")
558
+ entries = sovereign_fs.readdir("/coordination", fh=0)
559
+ assert "task_abc.json" in entries
560
+
561
+ def test_readdir_invalid_path(self, sovereign_fs: SovereignFS) -> None:
562
+ """readdir on a non-directory raises ENOENT."""
563
+ with pytest.raises(OSError) as exc_info:
564
+ sovereign_fs.readdir("/bogus_dir", fh=0)
565
+ assert exc_info.value.errno == errno.ENOENT
566
+
567
+ # -- read --------------------------------------------------------------
568
+
569
+ def test_read_memory_content(
570
+ self,
571
+ sovereign_fs: SovereignFS,
572
+ memory_dir: Path,
573
+ sample_memory: Dict[str, Any],
574
+ ) -> None:
575
+ """Reading a memory file returns its Markdown content."""
576
+ layer_dir = memory_dir / "short-term"
577
+ (layer_dir / "abc123.json").write_text(
578
+ json.dumps(sample_memory), encoding="utf-8"
579
+ )
580
+ content = sovereign_fs.read("/memories/short/abc123.md", size=4096, offset=0, fh=0)
581
+ assert b"# Memory: abc123" in content
582
+
583
+ def test_read_with_offset_and_size(
584
+ self,
585
+ sovereign_fs: SovereignFS,
586
+ memory_dir: Path,
587
+ sample_memory: Dict[str, Any],
588
+ ) -> None:
589
+ """Read respects offset and size arguments."""
590
+ layer_dir = memory_dir / "short-term"
591
+ (layer_dir / "abc123.json").write_text(
592
+ json.dumps(sample_memory), encoding="utf-8"
593
+ )
594
+ full = sovereign_fs.read("/memories/short/abc123.md", size=99999, offset=0, fh=0)
595
+ partial = sovereign_fs.read("/memories/short/abc123.md", size=5, offset=2, fh=0)
596
+ assert partial == full[2:7]
597
+
598
+ def test_read_enoent(self, sovereign_fs: SovereignFS) -> None:
599
+ """Reading a nonexistent file raises ENOENT."""
600
+ with pytest.raises(OSError) as exc_info:
601
+ sovereign_fs.read("/memories/short/ghost.md", size=4096, offset=0, fh=0)
602
+ assert exc_info.value.errno == errno.ENOENT
603
+
604
+ def test_read_identity_fingerprint(self, sovereign_fs: SovereignFS) -> None:
605
+ """/identity/fingerprint.txt is readable."""
606
+ content = sovereign_fs.read("/identity/fingerprint.txt", size=4096, offset=0, fh=0)
607
+ assert isinstance(content, bytes)
608
+ assert len(content) > 0
609
+
610
+ # -- open --------------------------------------------------------------
611
+
612
+ def test_open_readonly_file(
613
+ self,
614
+ sovereign_fs: SovereignFS,
615
+ ) -> None:
616
+ """Opening a readable file with O_RDONLY succeeds."""
617
+ fh = sovereign_fs.open("/identity/fingerprint.txt", flags=os.O_RDONLY)
618
+ assert fh == 0
619
+
620
+ def test_open_write_to_nonoutbox_raises(self, sovereign_fs: SovereignFS) -> None:
621
+ """Writing to a non-outbox path raises EACCES."""
622
+ with pytest.raises(OSError) as exc_info:
623
+ sovereign_fs.open("/identity/card.json", flags=os.O_WRONLY)
624
+ assert exc_info.value.errno == errno.EACCES
625
+
626
+ def test_open_write_outbox_initialises_buffer(
627
+ self, sovereign_fs: SovereignFS
628
+ ) -> None:
629
+ """Opening an outbox file for write initialises the buffer."""
630
+ sovereign_fs.open("/outbox/ava.msg", flags=os.O_WRONLY)
631
+ assert "/outbox/ava.msg" in sovereign_fs._outbox_buffers
632
+ assert sovereign_fs._outbox_buffers["/outbox/ava.msg"] == b""
633
+
634
+ def test_open_nonexistent_readonly_raises(self, sovereign_fs: SovereignFS) -> None:
635
+ """Opening a nonexistent file for read raises ENOENT."""
636
+ with pytest.raises(OSError) as exc_info:
637
+ sovereign_fs.open("/memories/short/ghost.md", flags=os.O_RDONLY)
638
+ assert exc_info.value.errno == errno.ENOENT
639
+
640
+ # -- write and flush ---------------------------------------------------
641
+
642
+ def test_write_to_outbox(self, sovereign_fs: SovereignFS) -> None:
643
+ """Writing data to an outbox path buffers the bytes."""
644
+ sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] = b""
645
+ n = sovereign_fs.write("/outbox/jarvis.msg", b"Hello!", offset=0, fh=0)
646
+ assert n == 6
647
+ assert sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] == b"Hello!"
648
+
649
+ def test_write_appends_at_offset(self, sovereign_fs: SovereignFS) -> None:
650
+ """Subsequent writes at a nonzero offset append to the buffer."""
651
+ sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] = b"Hello"
652
+ sovereign_fs.write("/outbox/jarvis.msg", b" World", offset=5, fh=0)
653
+ assert sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] == b"Hello World"
654
+
655
+ def test_write_to_non_outbox_raises(self, sovereign_fs: SovereignFS) -> None:
656
+ """Writing outside /outbox/ raises EACCES."""
657
+ with pytest.raises(OSError) as exc_info:
658
+ sovereign_fs.write("/memories/short/x.md", b"data", offset=0, fh=0)
659
+ assert exc_info.value.errno == errno.EACCES
660
+
661
+ def test_flush_sends_via_skcomm(
662
+ self, sovereign_fs: SovereignFS
663
+ ) -> None:
664
+ """Flushing an outbox file invokes _send_via_skcomm with correct args."""
665
+ sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] = b"Test message"
666
+ with patch("skcapstone.fuse_mount._send_via_skcomm", return_value=True) as mock_send:
667
+ sovereign_fs.flush("/outbox/jarvis.msg", fh=0)
668
+ mock_send.assert_called_once_with(
669
+ sovereign_fs._home, "jarvis", "Test message"
670
+ )
671
+ # Buffer should be cleared after flush
672
+ assert "/outbox/jarvis.msg" not in sovereign_fs._outbox_buffers
673
+
674
+ def test_flush_strips_msg_suffix(self, sovereign_fs: SovereignFS) -> None:
675
+ """Flush extracts the recipient by stripping the .msg suffix."""
676
+ sovereign_fs._outbox_buffers["/outbox/lumina.msg"] = b"Hi"
677
+ with patch("skcapstone.fuse_mount._send_via_skcomm", return_value=True) as mock_send:
678
+ sovereign_fs.flush("/outbox/lumina.msg", fh=0)
679
+ assert mock_send.call_args[0][1] == "lumina"
680
+
681
+ def test_flush_empty_buffer_no_send(self, sovereign_fs: SovereignFS) -> None:
682
+ """Flushing an empty buffer does not call _send_via_skcomm."""
683
+ sovereign_fs._outbox_buffers["/outbox/ava.msg"] = b""
684
+ with patch("skcapstone.fuse_mount._send_via_skcomm") as mock_send:
685
+ sovereign_fs.flush("/outbox/ava.msg", fh=0)
686
+ mock_send.assert_not_called()
687
+
688
+ def test_flush_non_outbox_is_noop(self, sovereign_fs: SovereignFS) -> None:
689
+ """Flushing a non-outbox path is a no-op returning 0."""
690
+ result = sovereign_fs.flush("/memories/short/x.md", fh=0)
691
+ assert result == 0
692
+
693
+ # -- create ------------------------------------------------------------
694
+
695
+ def test_create_outbox_file(self, sovereign_fs: SovereignFS) -> None:
696
+ """Creating a file under /outbox/ initialises the buffer."""
697
+ fh = sovereign_fs.create("/outbox/opus.msg", mode=0o644)
698
+ assert fh == 0
699
+ assert sovereign_fs._outbox_buffers["/outbox/opus.msg"] == b""
700
+
701
+ def test_create_non_outbox_raises(self, sovereign_fs: SovereignFS) -> None:
702
+ """Creating a file outside /outbox/ raises EACCES."""
703
+ with pytest.raises(OSError) as exc_info:
704
+ sovereign_fs.create("/inbox/hacker.msg", mode=0o644)
705
+ assert exc_info.value.errno == errno.EACCES
706
+
707
+ # -- truncate ----------------------------------------------------------
708
+
709
+ def test_truncate_outbox(self, sovereign_fs: SovereignFS) -> None:
710
+ """Truncating an outbox buffer shortens it."""
711
+ sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] = b"Hello World"
712
+ sovereign_fs.truncate("/outbox/jarvis.msg", length=5)
713
+ assert sovereign_fs._outbox_buffers["/outbox/jarvis.msg"] == b"Hello"
714
+
715
+ def test_truncate_non_outbox_raises(self, sovereign_fs: SovereignFS) -> None:
716
+ """Truncating a non-outbox path raises EACCES."""
717
+ with pytest.raises(OSError) as exc_info:
718
+ sovereign_fs.truncate("/identity/card.json", length=0)
719
+ assert exc_info.value.errno == errno.EACCES
720
+
721
+ # -- release -----------------------------------------------------------
722
+
723
+ def test_release_flushes_outbox(self, sovereign_fs: SovereignFS) -> None:
724
+ """Release calls flush, which sends the outbox buffer."""
725
+ sovereign_fs._outbox_buffers["/outbox/ava.msg"] = b"Goodbye"
726
+ with patch("skcapstone.fuse_mount._send_via_skcomm", return_value=True) as mock_send:
727
+ sovereign_fs.release("/outbox/ava.msg", fh=0)
728
+ mock_send.assert_called_once()
729
+
730
+ # -- pass-through stubs ------------------------------------------------
731
+
732
+ def test_chmod_noop(self, sovereign_fs: SovereignFS) -> None:
733
+ """chmod returns 0 (no-op)."""
734
+ assert sovereign_fs.chmod("/inbox", mode=0o777) == 0
735
+
736
+ def test_chown_noop(self, sovereign_fs: SovereignFS) -> None:
737
+ """chown returns 0 (no-op)."""
738
+ assert sovereign_fs.chown("/inbox", uid=0, gid=0) == 0
739
+
740
+ def test_utimens_noop(self, sovereign_fs: SovereignFS) -> None:
741
+ """utimens returns 0 (no-op)."""
742
+ assert sovereign_fs.utimens("/inbox") == 0
743
+
744
+
745
+ # ---------------------------------------------------------------------------
746
+ # TestFUSEDaemon
747
+ # ---------------------------------------------------------------------------
748
+
749
+
750
+ class TestFUSEDaemon:
751
+ """Tests for FUSEDaemon lifecycle manager (no real FUSE mounts)."""
752
+
753
+ def test_status_when_no_state(self, tmp_path: Path) -> None:
754
+ """Status returns a dict with mounted=False when no state exists."""
755
+ daemon = FUSEDaemon(
756
+ mount_point=tmp_path / "mount",
757
+ agent_home=tmp_path / "home",
758
+ )
759
+ with patch.object(daemon, "_is_mounted", return_value=False):
760
+ status = daemon.status()
761
+ assert status["mounted"] is False
762
+ assert status["pid"] is None
763
+ assert status["updated_at"] is None
764
+
765
+ def test_write_and_read_state(self, tmp_path: Path) -> None:
766
+ """State persists to disk and is readable."""
767
+ home = tmp_path / "home"
768
+ home.mkdir()
769
+ daemon = FUSEDaemon(mount_point=tmp_path / "mount", agent_home=home)
770
+ daemon._write_state(mounted=True, pid=12345)
771
+ state = daemon._read_state()
772
+ assert state is not None
773
+ assert state["mounted"] is True
774
+ assert state["pid"] == 12345
775
+ assert "updated_at" in state
776
+
777
+ def test_read_state_missing(self, tmp_path: Path) -> None:
778
+ """Reading state when no file exists returns None."""
779
+ daemon = FUSEDaemon(
780
+ mount_point=tmp_path / "mount",
781
+ agent_home=tmp_path / "home",
782
+ )
783
+ assert daemon._read_state() is None
784
+
785
+ def test_read_state_corrupt_json(self, tmp_path: Path) -> None:
786
+ """Corrupt state file returns None."""
787
+ home = tmp_path / "home"
788
+ home.mkdir()
789
+ daemon = FUSEDaemon(mount_point=tmp_path / "mount", agent_home=home)
790
+ fuse_dir = home / "fuse"
791
+ fuse_dir.mkdir(parents=True)
792
+ (fuse_dir / "fuse_state.json").write_text("{broken", encoding="utf-8")
793
+ assert daemon._read_state() is None
794
+
795
+ def test_status_includes_mount_point(self, tmp_path: Path) -> None:
796
+ """Status dict includes the configured mount point."""
797
+ mnt = tmp_path / "mymount"
798
+ daemon = FUSEDaemon(mount_point=mnt, agent_home=tmp_path / "home")
799
+ with patch.object(daemon, "_is_mounted", return_value=False):
800
+ status = daemon.status()
801
+ assert status["mount_point"] == str(mnt)
802
+
803
+ def test_status_includes_agent_home(self, tmp_path: Path) -> None:
804
+ """Status dict includes the configured agent home."""
805
+ home = tmp_path / "home"
806
+ daemon = FUSEDaemon(mount_point=tmp_path / "mnt", agent_home=home)
807
+ with patch.object(daemon, "_is_mounted", return_value=False):
808
+ status = daemon.status()
809
+ assert status["agent_home"] == str(home)
810
+
811
+ def test_state_file_path(self, tmp_path: Path) -> None:
812
+ """The state file is at <agent_home>/fuse/fuse_state.json."""
813
+ home = tmp_path / "home"
814
+ daemon = FUSEDaemon(mount_point=tmp_path / "mnt", agent_home=home)
815
+ assert daemon._state_file() == home / "fuse" / "fuse_state.json"
816
+
817
+ def test_pid_file_path(self, tmp_path: Path) -> None:
818
+ """The PID file is at <agent_home>/fuse/fuse.pid."""
819
+ home = tmp_path / "home"
820
+ daemon = FUSEDaemon(mount_point=tmp_path / "mnt", agent_home=home)
821
+ assert daemon._pid_file() == home / "fuse" / "fuse.pid"
822
+
823
+ def test_status_reads_pid_from_state(self, tmp_path: Path) -> None:
824
+ """Status returns pid from persisted state."""
825
+ home = tmp_path / "home"
826
+ home.mkdir()
827
+ daemon = FUSEDaemon(mount_point=tmp_path / "mnt", agent_home=home)
828
+ daemon._write_state(mounted=True, pid=9999)
829
+ with patch.object(daemon, "_is_mounted", return_value=True):
830
+ status = daemon.status()
831
+ assert status["pid"] == 9999
832
+ assert status["mounted"] is True