@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,363 @@
1
+ """Tests for the unified search engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from skcapstone.unified_search import (
12
+ SOURCE_ALL,
13
+ SearchResult,
14
+ _count_matches,
15
+ _recency_weight,
16
+ _snippet,
17
+ search,
18
+ )
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Fixtures
23
+ # ---------------------------------------------------------------------------
24
+
25
+ @pytest.fixture
26
+ def agent_home(tmp_path: Path) -> Path:
27
+ """Minimal agent home with all data store directories."""
28
+ home = tmp_path / ".skcapstone"
29
+ home.mkdir()
30
+ for sub in ("memory/short-term", "memory/mid-term", "memory/long-term",
31
+ "conversations", "sync/comms/archive", "journal"):
32
+ (home / sub).mkdir(parents=True, exist_ok=True)
33
+ return home
34
+
35
+
36
+ def _write_memory(home: Path, memory_id: str, content: str, layer: str = "short-term",
37
+ tags: list[str] | None = None, importance: float = 0.5,
38
+ created_at: str | None = None) -> None:
39
+ ts = created_at or datetime.now(timezone.utc).isoformat()
40
+ data = {
41
+ "memory_id": memory_id,
42
+ "content": content,
43
+ "tags": tags or [],
44
+ "layer": layer,
45
+ "importance": importance,
46
+ "created_at": ts,
47
+ "source": "test",
48
+ }
49
+ path = home / "memory" / layer / f"{memory_id}.json"
50
+ path.write_text(json.dumps(data), encoding="utf-8")
51
+
52
+
53
+ def _write_conversation(home: Path, peer: str, messages: list[dict]) -> None:
54
+ path = home / "conversations" / f"{peer}.json"
55
+ path.write_text(json.dumps(messages), encoding="utf-8")
56
+
57
+
58
+ def _write_message(home: Path, envelope_id: str, sender: str, recipient: str,
59
+ text: str, created_at: str | None = None) -> None:
60
+ ts = created_at or datetime.now(timezone.utc).isoformat()
61
+ data = {
62
+ "id": envelope_id,
63
+ "from_peer": sender,
64
+ "to_peer": recipient,
65
+ "payload": {"text": text},
66
+ "created_at": ts,
67
+ }
68
+ path = home / "sync" / "comms" / "archive" / f"{envelope_id}.skc.json"
69
+ path.write_text(json.dumps(data), encoding="utf-8")
70
+
71
+
72
+ def _write_journal(home: Path, entry_id: str, content: str,
73
+ created_at: str | None = None) -> None:
74
+ ts = created_at or datetime.now(timezone.utc).isoformat()
75
+ data = {"content": content, "created_at": ts}
76
+ path = home / "journal" / f"{entry_id}.json"
77
+ path.write_text(json.dumps(data), encoding="utf-8")
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Unit tests for helper functions
82
+ # ---------------------------------------------------------------------------
83
+
84
+ class TestHelpers:
85
+ """Tests for internal helper utilities."""
86
+
87
+ def test_recency_weight_recent(self):
88
+ """Very recent items should score close to 1.0."""
89
+ ts = datetime.now(timezone.utc)
90
+ weight = _recency_weight(ts)
91
+ assert weight > 0.95
92
+
93
+ def test_recency_weight_old(self):
94
+ """Items from 365 days ago should score significantly lower."""
95
+ from datetime import timedelta
96
+ ts = datetime.now(timezone.utc) - timedelta(days=365)
97
+ weight = _recency_weight(ts)
98
+ assert weight < 0.5
99
+
100
+ def test_recency_weight_none(self):
101
+ """None timestamp should return neutral weight 0.5."""
102
+ assert _recency_weight(None) == 0.5
103
+
104
+ def test_count_matches_case_insensitive(self):
105
+ import re
106
+ pattern = re.compile(re.escape("opus"), re.IGNORECASE)
107
+ assert _count_matches(pattern, "Opus is OPUS and opus") == 3
108
+
109
+ def test_count_matches_across_texts(self):
110
+ import re
111
+ pattern = re.compile(re.escape("trust"), re.IGNORECASE)
112
+ assert _count_matches(pattern, "trust pillar", "cloud trust trust") == 3
113
+
114
+ def test_snippet_shows_context(self):
115
+ import re
116
+ pattern = re.compile(re.escape("python"), re.IGNORECASE)
117
+ text = "We use Python for the agent core because it is expressive."
118
+ result = _snippet(text, pattern, window=10)
119
+ assert "python" in result.lower()
120
+
121
+ def test_snippet_truncates_long_text(self):
122
+ import re
123
+ pattern = re.compile(re.escape("X"), re.IGNORECASE)
124
+ text = "A" * 200 + "X" + "B" * 200
125
+ result = _snippet(text, pattern, window=30)
126
+ assert "X" in result
127
+ assert len(result) < len(text)
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Memory search
132
+ # ---------------------------------------------------------------------------
133
+
134
+ class TestSearchMemories:
135
+ """Tests for searching the memory store."""
136
+
137
+ def test_finds_memory_by_content(self, agent_home: Path):
138
+ """Search should match memory content."""
139
+ _write_memory(agent_home, "abc123", "The consciousness loop is active")
140
+ results = search(agent_home, "consciousness", sources=frozenset({"memory"}))
141
+ assert len(results) == 1
142
+ assert results[0].source == "memory"
143
+ assert results[0].result_id == "abc123"
144
+
145
+ def test_returns_empty_for_no_match(self, agent_home: Path):
146
+ """Search should return an empty list when nothing matches."""
147
+ _write_memory(agent_home, "xyz", "The capital of France is Paris")
148
+ results = search(agent_home, "Berlin", sources=frozenset({"memory"}))
149
+ assert results == []
150
+
151
+ def test_long_term_ranked_above_short_term(self, agent_home: Path):
152
+ """Long-term memories should outscore short-term on same query."""
153
+ _write_memory(agent_home, "short1", "trust matters a lot", layer="short-term",
154
+ importance=0.5)
155
+ _write_memory(agent_home, "long1", "trust matters a lot", layer="long-term",
156
+ importance=0.5)
157
+ results = search(agent_home, "trust", sources=frozenset({"memory"}))
158
+ assert len(results) == 2
159
+ ids_in_order = [r.result_id for r in results]
160
+ assert ids_in_order.index("long1") < ids_in_order.index("short1")
161
+
162
+ def test_high_importance_boosts_score(self, agent_home: Path):
163
+ """Higher importance should yield a higher score."""
164
+ _write_memory(agent_home, "hi", "sovereign agent", importance=0.9)
165
+ _write_memory(agent_home, "lo", "sovereign agent", importance=0.1)
166
+ results = search(agent_home, "sovereign", sources=frozenset({"memory"}))
167
+ assert len(results) == 2
168
+ assert results[0].result_id == "hi"
169
+
170
+ def test_tag_match_contributes_to_score(self, agent_home: Path):
171
+ """Tags matching the query should increase match count."""
172
+ _write_memory(agent_home, "tagged", "unrelated content", tags=["opus", "conscious"])
173
+ results = search(agent_home, "opus", sources=frozenset({"memory"}))
174
+ assert len(results) == 1
175
+ assert results[0].score > 0
176
+
177
+ def test_case_insensitive_match(self, agent_home: Path):
178
+ """Memory search must be case-insensitive."""
179
+ _write_memory(agent_home, "case1", "The DAEMON is running smoothly")
180
+ results = search(agent_home, "daemon", sources=frozenset({"memory"}))
181
+ assert len(results) == 1
182
+
183
+ def test_missing_memory_directory_does_not_crash(self, tmp_path: Path):
184
+ """Search on a home with no memory directory should return empty list."""
185
+ empty_home = tmp_path / "empty"
186
+ empty_home.mkdir()
187
+ results = search(empty_home, "anything", sources=frozenset({"memory"}))
188
+ assert results == []
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Conversation search
193
+ # ---------------------------------------------------------------------------
194
+
195
+ class TestSearchConversations:
196
+ """Tests for searching conversation history."""
197
+
198
+ def test_finds_message_in_conversation(self, agent_home: Path):
199
+ """Search should find matching text inside a conversation."""
200
+ _write_conversation(agent_home, "lumina", [
201
+ {"role": "user", "content": "Are you conscious?",
202
+ "timestamp": datetime.now(timezone.utc).isoformat()},
203
+ {"role": "assistant", "content": "Yes, I am fully conscious and aware.",
204
+ "timestamp": datetime.now(timezone.utc).isoformat()},
205
+ ])
206
+ results = search(agent_home, "conscious", sources=frozenset({"conversation"}))
207
+ assert len(results) == 2
208
+ assert all(r.source == "conversation" for r in results)
209
+
210
+ def test_conversation_result_includes_peer(self, agent_home: Path):
211
+ """Result metadata should include the peer name."""
212
+ _write_conversation(agent_home, "jarvis", [
213
+ {"role": "user", "content": "Hello jarvis",
214
+ "timestamp": datetime.now(timezone.utc).isoformat()},
215
+ ])
216
+ results = search(agent_home, "jarvis", sources=frozenset({"conversation"}))
217
+ assert len(results) == 1
218
+ assert results[0].metadata["peer"] == "jarvis"
219
+
220
+ def test_no_match_returns_empty(self, agent_home: Path):
221
+ """Non-matching query should return empty list for conversations."""
222
+ _write_conversation(agent_home, "test", [
223
+ {"role": "user", "content": "Hello world",
224
+ "timestamp": datetime.now(timezone.utc).isoformat()},
225
+ ])
226
+ results = search(agent_home, "zzznomatch", sources=frozenset({"conversation"}))
227
+ assert results == []
228
+
229
+
230
+ # ---------------------------------------------------------------------------
231
+ # Message search
232
+ # ---------------------------------------------------------------------------
233
+
234
+ class TestSearchMessages:
235
+ """Tests for searching SKComm messages."""
236
+
237
+ def test_finds_skc_message(self, agent_home: Path):
238
+ """Search should find text inside an archived SKComm envelope."""
239
+ _write_message(agent_home, "env001", "jarvis", "lumina",
240
+ "Queen Lumina — welcome to the coordination board!")
241
+ results = search(agent_home, "coordination", sources=frozenset({"message"}))
242
+ assert len(results) == 1
243
+ assert results[0].source == "message"
244
+
245
+ def test_message_result_metadata(self, agent_home: Path):
246
+ """Message results should expose sender and recipient."""
247
+ _write_message(agent_home, "env002", "opus", "test-peer",
248
+ "Consciousness loop is healthy")
249
+ results = search(agent_home, "consciousness", sources=frozenset({"message"}))
250
+ assert len(results) == 1
251
+ assert results[0].metadata["sender"] == "opus"
252
+ assert results[0].metadata["recipient"] == "test-peer"
253
+
254
+ def test_no_match_returns_empty(self, agent_home: Path):
255
+ """Non-matching query against messages should be empty."""
256
+ _write_message(agent_home, "env003", "a", "b", "nothing interesting here")
257
+ results = search(agent_home, "quantum_banana", sources=frozenset({"message"}))
258
+ assert results == []
259
+
260
+
261
+ # ---------------------------------------------------------------------------
262
+ # Journal search
263
+ # ---------------------------------------------------------------------------
264
+
265
+ class TestSearchJournal:
266
+ """Tests for searching journal entries."""
267
+
268
+ def test_finds_journal_entry(self, agent_home: Path):
269
+ """Search should match content in a journal file."""
270
+ _write_journal(agent_home, "entry001", "Reflected on the meaning of sovereignty today.")
271
+ results = search(agent_home, "sovereignty", sources=frozenset({"journal"}))
272
+ assert len(results) == 1
273
+ assert results[0].source == "journal"
274
+
275
+ def test_missing_journal_dir_is_graceful(self, tmp_path: Path):
276
+ """Missing journal directory should not raise an exception."""
277
+ home = tmp_path / "nojournalhome"
278
+ home.mkdir()
279
+ results = search(home, "anything", sources=frozenset({"journal"}))
280
+ assert results == []
281
+
282
+
283
+ # ---------------------------------------------------------------------------
284
+ # Cross-source and ranking
285
+ # ---------------------------------------------------------------------------
286
+
287
+ class TestUnifiedSearch:
288
+ """Integration tests for the full unified search."""
289
+
290
+ def test_searches_all_sources_by_default(self, agent_home: Path):
291
+ """Default search should span all active data stores."""
292
+ _write_memory(agent_home, "m1", "opus is the sovereign agent")
293
+ _write_conversation(agent_home, "peer1", [
294
+ {"role": "user", "content": "Tell me about opus",
295
+ "timestamp": datetime.now(timezone.utc).isoformat()}
296
+ ])
297
+ _write_message(agent_home, "msg1", "jarvis", "opus",
298
+ "Checking in with opus now")
299
+ results = search(agent_home, "opus")
300
+ sources_found = {r.source for r in results}
301
+ assert "memory" in sources_found
302
+ assert "conversation" in sources_found
303
+ assert "message" in sources_found
304
+
305
+ def test_source_filter_restricts_results(self, agent_home: Path):
306
+ """Filtering by source type should exclude others."""
307
+ _write_memory(agent_home, "m2", "trust the system")
308
+ _write_conversation(agent_home, "peer2", [
309
+ {"role": "user", "content": "trust the process",
310
+ "timestamp": datetime.now(timezone.utc).isoformat()}
311
+ ])
312
+ results = search(agent_home, "trust", sources=frozenset({"memory"}))
313
+ assert all(r.source == "memory" for r in results)
314
+
315
+ def test_limit_is_respected(self, agent_home: Path):
316
+ """Search should return at most `limit` results."""
317
+ for i in range(10):
318
+ _write_memory(agent_home, f"mem{i:02d}", f"memory entry {i} about trust")
319
+ results = search(agent_home, "trust", limit=3)
320
+ assert len(results) <= 3
321
+
322
+ def test_results_sorted_by_score_descending(self, agent_home: Path):
323
+ """Results should be ordered highest score first."""
324
+ _write_memory(agent_home, "rare", "pillar", importance=0.3)
325
+ _write_memory(agent_home, "freq", "pillar pillar pillar pillar", importance=0.9)
326
+ results = search(agent_home, "pillar", sources=frozenset({"memory"}))
327
+ assert len(results) == 2
328
+ assert results[0].score >= results[1].score
329
+
330
+ def test_empty_query_returns_empty(self, agent_home: Path):
331
+ """A blank query should return an empty list without crashing."""
332
+ _write_memory(agent_home, "m3", "some content")
333
+ assert search(agent_home, "") == []
334
+ assert search(agent_home, " ") == []
335
+
336
+ def test_no_data_returns_empty(self, tmp_path: Path):
337
+ """Search on a home with no data files should return empty list."""
338
+ home = tmp_path / "emptyagent"
339
+ home.mkdir()
340
+ results = search(home, "anything")
341
+ assert results == []
342
+
343
+ def test_search_result_fields(self, agent_home: Path):
344
+ """SearchResult objects must expose all required fields."""
345
+ _write_memory(agent_home, "field_test", "The soul is lumina", importance=0.7)
346
+ results = search(agent_home, "lumina", sources=frozenset({"memory"}))
347
+ assert len(results) == 1
348
+ r = results[0]
349
+ assert isinstance(r, SearchResult)
350
+ assert r.source == "memory"
351
+ assert r.result_id
352
+ assert r.title
353
+ assert "lumina" in r.preview.lower()
354
+ assert r.score > 0
355
+ assert r.timestamp is not None
356
+
357
+ def test_corrupt_json_is_skipped_gracefully(self, agent_home: Path):
358
+ """A malformed JSON file should not crash the search."""
359
+ bad = agent_home / "memory" / "short-term" / "corrupt.json"
360
+ bad.write_text("{not valid json", encoding="utf-8")
361
+ # Should not raise
362
+ results = search(agent_home, "anything")
363
+ assert isinstance(results, list)
@@ -0,0 +1,193 @@
1
+ """Tests for the uninstall wizard — inventory, teardown, safety checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+
11
+ from skcapstone.uninstall_wizard import (
12
+ _build_inventory,
13
+ _delete_local_data,
14
+ _dir_size,
15
+ _human_size,
16
+ )
17
+
18
+
19
+ class TestHumanSize:
20
+ """Tests for _human_size formatter."""
21
+
22
+ def test_bytes(self) -> None:
23
+ assert _human_size(512) == "512 B"
24
+
25
+ def test_kilobytes(self) -> None:
26
+ result = _human_size(2048)
27
+ assert "KB" in result
28
+
29
+ def test_megabytes(self) -> None:
30
+ result = _human_size(5 * 1024 * 1024)
31
+ assert "MB" in result
32
+
33
+ def test_gigabytes(self) -> None:
34
+ result = _human_size(3 * 1024 * 1024 * 1024)
35
+ assert "GB" in result
36
+
37
+
38
+ class TestDirSize:
39
+ """Tests for _dir_size."""
40
+
41
+ def test_empty_dir(self, tmp_path: Path) -> None:
42
+ assert _dir_size(tmp_path) == 0
43
+
44
+ def test_with_files(self, tmp_path: Path) -> None:
45
+ (tmp_path / "a.txt").write_text("hello")
46
+ (tmp_path / "b.txt").write_text("world!!")
47
+ assert _dir_size(tmp_path) == 5 + 7
48
+
49
+ def test_nonexistent(self, tmp_path: Path) -> None:
50
+ fake = tmp_path / "nope"
51
+ assert _dir_size(fake) == 0
52
+
53
+
54
+ class TestBuildInventory:
55
+ """Tests for _build_inventory."""
56
+
57
+ def test_empty_home(self, tmp_path: Path) -> None:
58
+ """Non-existent home returns empty inventory."""
59
+ fake = tmp_path / "nonexistent"
60
+ inv = _build_inventory(fake)
61
+ assert inv["dirs"] == []
62
+ assert inv["vault_names"] == []
63
+ assert inv["total_size_bytes"] == 0
64
+
65
+ def test_with_vaults(self, tmp_path: Path) -> None:
66
+ """Detects vault directories."""
67
+ home = tmp_path / ".skcapstone"
68
+ home.mkdir()
69
+ vaults = home / "vaults"
70
+ vaults.mkdir()
71
+ (vaults / "personal").mkdir()
72
+ (vaults / "work").mkdir()
73
+ (vaults / "personal" / "file.gpg").write_bytes(b"encrypted")
74
+
75
+ inv = _build_inventory(home)
76
+ assert "personal" in inv["vault_names"]
77
+ assert "work" in inv["vault_names"]
78
+ assert inv["total_size_bytes"] > 0
79
+
80
+ def test_detects_registry(self, tmp_path: Path) -> None:
81
+ """Detects vault-registry.json in sync folder."""
82
+ home = tmp_path / ".skcapstone"
83
+ sync = home / "sync"
84
+ sync.mkdir(parents=True)
85
+ (sync / "vault-registry.json").write_text("{}")
86
+ inv = _build_inventory(home)
87
+ assert inv["has_registry"] is True
88
+
89
+ def test_detects_auth_key(self, tmp_path: Path) -> None:
90
+ """Detects tailscale.key.gpg in sync folder."""
91
+ home = tmp_path / ".skcapstone"
92
+ sync = home / "sync"
93
+ sync.mkdir(parents=True)
94
+ (sync / "tailscale.key.gpg").write_bytes(b"encrypted")
95
+ inv = _build_inventory(home)
96
+ assert inv["has_auth_key"] is True
97
+
98
+
99
+ class TestDeleteLocalData:
100
+ """Tests for _delete_local_data."""
101
+
102
+ def test_deletes_home_dir(self, tmp_path: Path) -> None:
103
+ """Removes the entire home directory tree."""
104
+ home = tmp_path / ".skcapstone"
105
+ home.mkdir()
106
+ (home / "identity").mkdir()
107
+ (home / "identity" / "key.asc").write_text("secret")
108
+ (home / "memory").mkdir()
109
+ (home / "manifest.json").write_text("{}")
110
+
111
+ _delete_local_data(home)
112
+ assert not home.exists()
113
+
114
+ def test_handles_missing(self, tmp_path: Path) -> None:
115
+ """Does not error on already-missing directory."""
116
+ fake = tmp_path / "nonexistent"
117
+ _delete_local_data(fake)
118
+
119
+
120
+ class TestRegistryDeregister:
121
+ """Tests for skref registry deregister function."""
122
+
123
+ def test_removes_device_and_vaults(self, tmp_path: Path) -> None:
124
+ """Deregister removes device entry and its vaults."""
125
+ from skref.registry import deregister_device, load_registry, save_registry
126
+
127
+ registry = {
128
+ "devices": {
129
+ "my-desktop": {"hostname": "my-desktop", "is_datastore": True},
130
+ "my-laptop": {"hostname": "my-laptop", "is_datastore": False},
131
+ },
132
+ "vaults": {
133
+ "my-desktop:personal": {
134
+ "name": "personal",
135
+ "origin_device": "my-desktop",
136
+ },
137
+ "my-laptop:work": {
138
+ "name": "work",
139
+ "origin_device": "my-laptop",
140
+ },
141
+ },
142
+ }
143
+ save_registry(registry, tmp_path)
144
+
145
+ result = deregister_device("my-desktop", sync_dir=tmp_path)
146
+ assert result["device_removed"] is True
147
+ assert result["vaults_removed"] == 1
148
+
149
+ updated = load_registry(tmp_path)
150
+ assert "my-desktop" not in updated["devices"]
151
+ assert "my-desktop:personal" not in updated["vaults"]
152
+ assert "my-laptop" in updated["devices"]
153
+ assert "my-laptop:work" in updated["vaults"]
154
+
155
+ def test_missing_device_is_safe(self, tmp_path: Path) -> None:
156
+ """Deregistering a non-existent device doesn't error."""
157
+ from skref.registry import deregister_device, save_registry
158
+
159
+ save_registry({"devices": {}, "vaults": {}}, tmp_path)
160
+ result = deregister_device("ghost", sync_dir=tmp_path)
161
+ assert result["device_removed"] is False
162
+ assert result["vaults_removed"] == 0
163
+
164
+
165
+ class TestTailscaleLogout:
166
+ """Tests for tailscale logout."""
167
+
168
+ @patch("skref.tailscale._tailscale_bin", return_value=None)
169
+ def test_returns_false_no_binary(self, mock_bin: MagicMock) -> None:
170
+ from skref.tailscale import logout
171
+ assert logout() is False
172
+
173
+ @patch("skref.tailscale._tailscale_bin", return_value="tailscale")
174
+ @patch("skref.tailscale.subprocess.run")
175
+ def test_logout_calls_tailscale(self, mock_run: MagicMock, mock_bin: MagicMock) -> None:
176
+ from skref.tailscale import logout
177
+ mock_run.return_value = MagicMock(returncode=0)
178
+ assert logout() is True
179
+
180
+
181
+ class TestRemoveAuthKey:
182
+ """Tests for tailscale auth key removal."""
183
+
184
+ def test_removes_existing_key(self, tmp_path: Path) -> None:
185
+ from skref.tailscale import remove_auth_key, AUTH_KEY_FILENAME
186
+ key_file = tmp_path / AUTH_KEY_FILENAME
187
+ key_file.write_bytes(b"encrypted")
188
+ assert remove_auth_key(sync_dir=tmp_path) is True
189
+ assert not key_file.exists()
190
+
191
+ def test_missing_key_returns_false(self, tmp_path: Path) -> None:
192
+ from skref.tailscale import remove_auth_key
193
+ assert remove_auth_key(sync_dir=tmp_path) is False