shadow-claw 1.23.3 → 1.24.0

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 (286) hide show
  1. package/README.md +42 -5
  2. package/bin/assert-version-bump.mjs +60 -38
  3. package/bin/assert-version-bump.test.mjs +74 -11
  4. package/bin/cli.mjs +166 -0
  5. package/bin/cli.test.mjs +60 -0
  6. package/bin/commands/backup.mjs +70 -0
  7. package/bin/commands/clients.mjs +41 -0
  8. package/bin/commands/peer-id.mjs +76 -0
  9. package/bin/commands/peer-id.test.mjs +104 -0
  10. package/bin/commands/send.mjs +51 -0
  11. package/bin/commands/tasks.mjs +59 -0
  12. package/bin/commands/webrtc-listen.mjs +254 -0
  13. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  14. package/bin/rolldown.config.test.mjs +401 -8
  15. package/bin/utils/control-client.mjs +243 -0
  16. package/bin/utils/control-client.test.mjs +83 -0
  17. package/bin/utils/ulid.mjs +45 -0
  18. package/bin/utils/webrtc-control-client.mjs +699 -0
  19. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  20. package/dist/electron/main.cjs +1 -1
  21. package/dist/public/AGENTS.md +5 -3
  22. package/dist/public/README.md +42 -5
  23. package/dist/public/agent.worker.js +6 -6
  24. package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
  25. package/dist/public/backup-controller-C_s9in2t.js +1 -0
  26. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  27. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  28. package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
  29. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  30. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  31. package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
  32. package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
  33. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  34. package/dist/public/config-CrHchneq.js +1 -0
  35. package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-CxpZeDJJ.js} +1 -1
  36. package/dist/public/{connections-xd_66YVU.js → connections-DvbgdkaB.js} +1 -1
  37. package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-NCqQ5lmC.js} +1 -1
  38. package/dist/public/docs/README.md +3 -1
  39. package/dist/public/docs/architecture/overview.md +12 -12
  40. package/dist/public/docs/architecture/storage.md +5 -2
  41. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  42. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +6 -3
  43. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  44. package/dist/public/docs/example/article/index.html +2 -2
  45. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  46. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  47. package/dist/public/docs/publishing/index.html +19 -76
  48. package/dist/public/docs/skill-creator/index.html +2 -2
  49. package/dist/public/docs/subsystems/backup.md +56 -0
  50. package/dist/public/docs/subsystems/cli.md +124 -0
  51. package/dist/public/docs/subsystems/control-plane.md +158 -0
  52. package/dist/public/docs/subsystems/notifications.md +54 -2
  53. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  54. package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
  55. package/dist/public/e2e/README.md +5 -1
  56. package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-IGWsRl4i.js} +1 -1
  57. package/dist/public/file-viewer-D0eflgt6.js +1 -0
  58. package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
  59. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  60. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  61. package/dist/public/{git-BkjrsRXX.js → git-7Vq_SJSp.js} +2 -2
  62. package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  63. package/dist/public/index.html +2 -2
  64. package/dist/public/index.js +1 -1
  65. package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  66. package/dist/public/initControlPlane-C9jpnWev.js +1 -0
  67. package/dist/public/llms.txt +1 -1
  68. package/dist/public/main/index.html +2 -2
  69. package/dist/public/main/memory/index.html +2 -2
  70. package/dist/public/{markdown-6MBrIo-S.js → markdown-BnpKxSxl.js} +1 -1
  71. package/dist/public/mcp-reconnect-9emsqGRe.js +1 -0
  72. package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-C0KvLNUp.js} +1 -1
  73. package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-3h9xxHUm.js} +676 -676
  74. package/dist/public/orchestrator-nJj59RqD.js +15 -0
  75. package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
  76. package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-BP4laMXj.js} +1 -1
  77. package/dist/public/{push-client-Dqq06wbB.js → push-client-CrjEXK3U.js} +1 -1
  78. package/dist/public/service-worker/fetch-proxy.js +1 -1
  79. package/dist/public/service-worker/push-handler.js +1 -1
  80. package/dist/public/service-worker.js +1 -1
  81. package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-CbwETBWf.js} +2 -2
  82. package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-B4sBTQEj.js} +3 -3
  83. package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
  84. package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
  85. package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
  86. package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
  87. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  88. package/dist/public/shadow-claw-file-viewer-Blf6yGVD.js +577 -0
  89. package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
  90. package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-v8S0JjuR.js} +3 -3
  91. package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  92. package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
  93. package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  94. package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-qHbP95z9.js} +1 -1
  95. package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
  96. package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  97. package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DFZHhzFZ.js} +3 -3
  98. package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
  99. package/dist/public/shadow-claw-storage-BFynbP0j.js +219 -0
  100. package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
  101. package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Caxc9atL.js} +4 -4
  102. package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
  103. package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-BfJx3A9W.js} +2 -2
  104. package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
  105. package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
  106. package/dist/public/static-main-manifest.json +1 -1
  107. package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
  108. package/dist/server.js +286 -189
  109. package/package.json +2 -1
  110. package/src/__mocks__/jszip.cjs +15 -5
  111. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  112. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  113. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  114. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  115. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  116. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  117. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  118. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  119. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  120. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  121. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  122. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  123. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  124. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
  125. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +120 -0
  126. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
  127. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  128. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  129. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  130. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  131. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  132. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  133. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  134. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  135. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  136. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  137. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  138. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  139. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  140. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +378 -0
  141. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
  142. package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
  143. package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
  144. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +627 -1
  145. package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
  146. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
  147. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
  148. package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
  149. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  150. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  151. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  152. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  153. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  154. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  155. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  156. package/src/config/config.test.ts +9 -0
  157. package/src/config/config.ts +15 -0
  158. package/src/core/app-routes.test.ts +56 -0
  159. package/src/core/app-routes.ts +8 -1
  160. package/src/core/backup-controller.test.ts +101 -0
  161. package/src/core/backup-controller.ts +137 -0
  162. package/src/core/control-plane-client.test.ts +408 -0
  163. package/src/core/control-plane-client.ts +425 -0
  164. package/src/core/orchestrator/orchestrator.ts +4 -0
  165. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  166. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  167. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  168. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  169. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  170. package/src/core/utils/initControlPlane.test.ts +276 -0
  171. package/src/core/utils/initControlPlane.ts +437 -0
  172. package/src/core/utils/initializeApp.ts +18 -0
  173. package/src/db/getSession.test.ts +13 -1
  174. package/src/db/openDatabase.test.ts +10 -1
  175. package/src/db/openDatabase.ts +3 -1
  176. package/src/server/app.test.ts +12 -0
  177. package/src/server/app.ts +18 -8
  178. package/src/server/client-registry.test.ts +250 -0
  179. package/src/server/client-registry.ts +477 -0
  180. package/src/server/config.ts +15 -0
  181. package/src/server/control-plane-types.ts +120 -0
  182. package/src/server/control-plane.test.ts +472 -0
  183. package/src/server/control-plane.ts +581 -0
  184. package/src/server/routes/backup.test.ts +263 -0
  185. package/src/server/routes/backup.ts +262 -0
  186. package/src/server/routes/integrations-email.test.ts +532 -0
  187. package/src/server/routes/llamafile.test.ts +172 -48
  188. package/src/server/routes/mesh-llm.test.ts +192 -0
  189. package/src/server/routes/ollama.test.ts +49 -0
  190. package/src/server/server-peer.test.ts +86 -0
  191. package/src/server/server-peer.ts +330 -0
  192. package/src/server/server.ts +35 -0
  193. package/src/server/services/llamafile-manager.test.ts +230 -6
  194. package/src/server/services/transformers-runtime.test.ts +186 -18
  195. package/src/server/webrtc.test.ts +21 -0
  196. package/src/server/webrtc.ts +72 -0
  197. package/src/service-worker/fetch-proxy.test.ts +64 -1
  198. package/src/service-worker/push-handler.ts +39 -0
  199. package/src/shell/vm.test.ts +744 -12
  200. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  201. package/src/storage/getGroupFile.test.ts +66 -0
  202. package/src/storage/memoryStorage.test.ts +82 -2
  203. package/src/storage/migrateLegacyOpfs.test.ts +327 -0
  204. package/src/storage/migrateLegacyOpfs.ts +117 -0
  205. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  206. package/src/stores/chat-ui.test.ts +20 -0
  207. package/src/stores/orchestrator.test.ts +134 -0
  208. package/src/stores/toast.test.ts +50 -0
  209. package/src/stores/tools.test.ts +147 -10
  210. package/src/subsystems/channels/imessage.test.ts +59 -0
  211. package/src/subsystems/channels/peerjs.test.ts +500 -62
  212. package/src/subsystems/channels/peerjs.ts +71 -4
  213. package/src/subsystems/channels/telegram.test.ts +177 -0
  214. package/src/subsystems/email/catalog.test.ts +1 -0
  215. package/src/subsystems/email/connections.test.ts +39 -1
  216. package/src/subsystems/git/credentials.test.ts +160 -825
  217. package/src/subsystems/git/git.test.ts +463 -0
  218. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  219. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  220. package/src/subsystems/notifications/push-routes.ts +23 -0
  221. package/src/subsystems/notifications/push-store.ts +1 -1
  222. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  223. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  224. package/src/subsystems/providers/model-registry.test.ts +115 -0
  225. package/src/subsystems/providers/providers.test.ts +94 -1
  226. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  227. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  228. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  229. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  230. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  231. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  232. package/src/subsystems/tools/cron.test.ts +64 -0
  233. package/src/subsystems/tools/declarative.test.ts +255 -89
  234. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  235. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  236. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  237. package/src/testing/jest-setup.ts +328 -40
  238. package/src/testing/mock-v86.mjs +49 -0
  239. package/src/ui/a2ui/types.ts +4 -3
  240. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  241. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  242. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  243. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  244. package/src/worker/tools/email/email.test.ts +193 -8
  245. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  246. package/src/worker/tools/git/git.test.ts +214 -3
  247. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  248. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  249. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  250. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  251. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  252. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  253. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  254. package/src/worker/tools/ui/javascript.test.ts +116 -0
  255. package/src/worker/tools/ui/list-components.test.ts +10 -0
  256. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  257. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  258. package/src/worker/tools/ui/render-component.test.ts +346 -0
  259. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  260. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  261. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  262. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  263. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  264. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  265. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  266. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  267. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  268. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  269. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  270. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  271. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  272. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  273. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  274. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  275. package/src/worker/utils/rate-limit.test.ts +199 -0
  276. package/src/worker/utils/tool-state.test.ts +29 -0
  277. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  278. package/dist/public/config-AL77QAx4.js +0 -1
  279. package/dist/public/downloadGroupFile-BABv_1mJ.js +0 -1
  280. package/dist/public/file-viewer-CCSABMsE.js +0 -1
  281. package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
  282. package/dist/public/orchestrator-4su7iyWj.js +0 -15
  283. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  284. package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
  285. package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
  286. package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
@@ -0,0 +1,40 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ const mockIsVMReady = jest.fn() as any;
4
+
5
+ jest.unstable_mockModule("../../../../shell/vm.js", () => ({
6
+ isVMReady: mockIsVMReady,
7
+ }));
8
+
9
+ const { waitForVMReady } = await import("./waitForVMReady.js");
10
+
11
+ describe("waitForVMReady", () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ it("returns true immediately if VM is already ready", async () => {
17
+ mockIsVMReady.mockReturnValue(true);
18
+
19
+ const result = await waitForVMReady(1000);
20
+ expect(result).toBe(true);
21
+ expect(mockIsVMReady).toHaveBeenCalledTimes(1);
22
+ });
23
+
24
+ it("polls and returns true once VM becomes ready", async () => {
25
+ mockIsVMReady
26
+ .mockReturnValueOnce(false)
27
+ .mockReturnValueOnce(false)
28
+ .mockReturnValue(true);
29
+
30
+ const result = await waitForVMReady(500);
31
+ expect(result).toBe(true);
32
+ });
33
+
34
+ it("times out and returns false if VM never becomes ready", async () => {
35
+ mockIsVMReady.mockReturnValue(false);
36
+
37
+ const result = await waitForVMReady(50);
38
+ expect(result).toBe(false);
39
+ });
40
+ });
@@ -30,9 +30,14 @@ describe("worker/tools/email", () => {
30
30
  id: "imap",
31
31
  name: "IMAP",
32
32
  protocol: "imap",
33
- actions: ["messages.read", "messages.send"],
33
+ actions: [
34
+ "messages.read",
35
+ "messages.send",
36
+ "messages.mark_read",
37
+ "messages.delete",
38
+ ],
34
39
  authTypes: ["basic_userpass", "oauth"],
35
- configurableFields: [],
40
+ configurableFields: ["host", "port"],
36
41
  }
37
42
  : null,
38
43
  );
@@ -44,7 +49,7 @@ describe("worker/tools/email", () => {
44
49
  protocol: "imap",
45
50
  actions: ["messages.read", "messages.send"],
46
51
  authTypes: ["basic_userpass", "oauth"],
47
- configurableFields: [],
52
+ configurableFields: ["host", "port"],
48
53
  },
49
54
  ]);
50
55
 
@@ -119,10 +124,52 @@ describe("worker/tools/email", () => {
119
124
 
120
125
  it("returns validation error when action is missing", async () => {
121
126
  const result = await executeManageEmailTool({} as any, {}, "group-1");
122
-
123
127
  expect(result).toContain("requires action");
124
128
  });
125
129
 
130
+ it("lists plugins, connections and status", async () => {
131
+ const plugins = await executeManageEmailTool(
132
+ {} as any,
133
+ { action: "list_plugins" },
134
+ "group-1",
135
+ );
136
+ expect(plugins).toContain("IMAP");
137
+
138
+ mockListEmailConnections.mockResolvedValueOnce([
139
+ {
140
+ id: "conn-1",
141
+ label: "Office 365",
142
+ pluginId: "imap",
143
+ enabled: true,
144
+ config: { host: "outlook.office365.com" },
145
+ },
146
+ ]);
147
+
148
+ const connections = await executeManageEmailTool(
149
+ {} as any,
150
+ { action: "list_connections" },
151
+ "group-1",
152
+ );
153
+ expect(connections).toContain("conn-1 (Office 365)");
154
+
155
+ mockListEmailConnections.mockResolvedValueOnce([
156
+ {
157
+ id: "conn-1",
158
+ label: "Office 365",
159
+ pluginId: "imap",
160
+ enabled: true,
161
+ config: {},
162
+ },
163
+ ]);
164
+
165
+ const status = await executeManageEmailTool(
166
+ {} as any,
167
+ { action: "status" },
168
+ "group-1",
169
+ );
170
+ expect(status).toContain("Configured plugin connections: 1");
171
+ });
172
+
126
173
  it("connect stores encrypted basic credentials", async () => {
127
174
  mockUpsertEmailConnection.mockResolvedValueOnce({
128
175
  id: "conn-1",
@@ -156,6 +203,70 @@ describe("worker/tools/email", () => {
156
203
  expect(result).toContain("Email connection created: conn-1");
157
204
  });
158
205
 
206
+ it("configure, enable, disable, delete and test connections", async () => {
207
+ const existingConn = {
208
+ id: "conn-cfg",
209
+ label: "Old Label",
210
+ pluginId: "imap",
211
+ enabled: false,
212
+ config: { host: "old.host" },
213
+ credentialRef: {
214
+ username: "user@mail.com",
215
+ encryptedSecret: "enc:oldpw",
216
+ },
217
+ };
218
+
219
+ mockGetEmailConnection.mockResolvedValue(existingConn);
220
+ mockUpsertEmailConnection.mockImplementation(
221
+ async (_db: any, val: any) => val,
222
+ );
223
+
224
+ // configure
225
+ const cfgResult = await executeManageEmailTool(
226
+ {} as any,
227
+ {
228
+ action: "configure",
229
+ connection_id: "conn-cfg",
230
+ label: "New Label",
231
+ enabled: true,
232
+ },
233
+ "group-1",
234
+ );
235
+ expect(cfgResult).toContain("Integration connection updated: conn-cfg");
236
+
237
+ // enable
238
+ const enableResult = await executeManageEmailTool(
239
+ {} as any,
240
+ { action: "enable", connection_id: "conn-cfg" },
241
+ "group-1",
242
+ );
243
+ expect(enableResult).toContain("enabled");
244
+
245
+ // disable
246
+ const disableResult = await executeManageEmailTool(
247
+ {} as any,
248
+ { action: "disable", connection_id: "conn-cfg" },
249
+ "group-1",
250
+ );
251
+ expect(disableResult).toContain("disabled");
252
+
253
+ // test
254
+ const testResult = await executeManageEmailTool(
255
+ {} as any,
256
+ { action: "test", connection_id: "conn-cfg" },
257
+ "group-1",
258
+ );
259
+ expect(testResult).toContain("passed for conn-cfg");
260
+
261
+ // delete
262
+ const deleteResult = await executeManageEmailTool(
263
+ {} as any,
264
+ { action: "delete", connection_id: "conn-cfg" },
265
+ "group-1",
266
+ );
267
+ expect(deleteResult).toContain("Integration connection deleted: conn-cfg");
268
+ });
269
+
159
270
  it("read_messages retries once after oauth 401", async () => {
160
271
  mockGetEmailConnection.mockResolvedValueOnce({
161
272
  id: "conn-oauth",
@@ -294,8 +405,8 @@ describe("worker/tools/email", () => {
294
405
  expect(parsed.saved_paths).toEqual(["downloads/email/x.txt"]);
295
406
  });
296
407
 
297
- it("mark_as_read validates message_uids", async () => {
298
- mockGetEmailConnection.mockResolvedValueOnce({
408
+ it("mark_as_read and delete_messages execute flag modifications and delete requests", async () => {
409
+ mockGetEmailConnection.mockResolvedValue({
299
410
  id: "conn-mark",
300
411
  pluginId: "imap",
301
412
  enabled: true,
@@ -307,15 +418,89 @@ describe("worker/tools/email", () => {
307
418
  },
308
419
  });
309
420
 
310
- const result = await executeManageEmailTool(
421
+ // mark_as_read missing uids
422
+ const noUidResult = await executeManageEmailTool(
423
+ {} as any,
424
+ { action: "mark_as_read", connection_id: "conn-mark" },
425
+ "group-1",
426
+ );
427
+ expect(noUidResult).toContain("requires message_uids");
428
+
429
+ // mark_as_read success
430
+ (global.fetch as any).mockResolvedValueOnce({
431
+ ok: true,
432
+ status: 200,
433
+ json: async () => ({ updated: [101] }),
434
+ });
435
+
436
+ const markResult = await executeManageEmailTool(
311
437
  {} as any,
312
438
  {
313
439
  action: "mark_as_read",
314
440
  connection_id: "conn-mark",
441
+ message_uids: [101],
315
442
  },
316
443
  "group-1",
317
444
  );
445
+ expect(markResult).toContain('"updated": [');
446
+
447
+ // delete_messages success
448
+ (global.fetch as any).mockResolvedValueOnce({
449
+ ok: true,
450
+ status: 200,
451
+ json: async () => ({ deleted: [102] }),
452
+ });
453
+
454
+ const deleteMsgResult = await executeManageEmailTool(
455
+ {} as any,
456
+ {
457
+ action: "delete_messages",
458
+ connection_id: "conn-mark",
459
+ message_uids: [102],
460
+ },
461
+ "group-1",
462
+ );
463
+ expect(deleteMsgResult).toContain('"deleted": [');
464
+ });
465
+
466
+ it("parses email attachment inputs and guesses mime types correctly", async () => {
467
+ const { asEmailAttachmentInputs } =
468
+ await import("./utils/asEmailAttachmentInputs.js");
469
+ const { guessMimeTypeFromFilename } =
470
+ await import("./utils/guessMimeTypeFromFilename.js");
471
+
472
+ // asEmailAttachmentInputs
473
+ expect(asEmailAttachmentInputs(null)).toEqual([]);
474
+ expect(
475
+ asEmailAttachmentInputs([
476
+ "file1.txt",
477
+ " ",
478
+ {
479
+ path: "file2.pdf",
480
+ filename: "doc.pdf",
481
+ content_type: "application/pdf",
482
+ },
483
+ { path: "" },
484
+ 123,
485
+ ]),
486
+ ).toEqual([
487
+ { path: "file1.txt" },
488
+ {
489
+ path: "file2.pdf",
490
+ filename: "doc.pdf",
491
+ contentType: "application/pdf",
492
+ },
493
+ ]);
318
494
 
319
- expect(result).toContain("requires message_uids");
495
+ // guessMimeTypeFromFilename
496
+ expect(guessMimeTypeFromFilename("photo.png")).toBe("image/png");
497
+ expect(guessMimeTypeFromFilename("photo.jpg")).toBe("image/jpeg");
498
+ expect(guessMimeTypeFromFilename("photo.jpeg")).toBe("image/jpeg");
499
+ expect(guessMimeTypeFromFilename("anim.gif")).toBe("image/gif");
500
+ expect(guessMimeTypeFromFilename("pic.webp")).toBe("image/webp");
501
+ expect(guessMimeTypeFromFilename("doc.pdf")).toBe("application/pdf");
502
+ expect(guessMimeTypeFromFilename("notes.txt")).toBe("text/plain");
503
+ expect(guessMimeTypeFromFilename("readme.md")).toBe("text/plain");
504
+ expect(guessMimeTypeFromFilename("archive.zip")).toBeUndefined();
320
505
  });
321
506
  });
@@ -0,0 +1,47 @@
1
+ import { asEmailAttachmentInputs } from "./asEmailAttachmentInputs.js";
2
+
3
+ describe("asEmailAttachmentInputs", () => {
4
+ it("returns empty array for non-array inputs", () => {
5
+ expect(asEmailAttachmentInputs(null)).toEqual([]);
6
+ expect(asEmailAttachmentInputs(undefined)).toEqual([]);
7
+ expect(asEmailAttachmentInputs("file.txt")).toEqual([]);
8
+ expect(asEmailAttachmentInputs({ path: "file.txt" })).toEqual([]);
9
+ });
10
+
11
+ it("parses string attachment items", () => {
12
+ const input = ["file1.txt", " ", "folder/file2.pdf"];
13
+ const result = asEmailAttachmentInputs(input);
14
+ expect(result).toEqual([
15
+ { path: "file1.txt" },
16
+ { path: "folder/file2.pdf" },
17
+ ]);
18
+ });
19
+
20
+ it("parses object attachment items with filename and content_type", () => {
21
+ const input = [
22
+ {
23
+ path: "docs/spec.pdf",
24
+ filename: "specs.pdf",
25
+ content_type: "application/pdf",
26
+ },
27
+ { path: "images/logo.png" },
28
+ { path: " ", filename: "ignored.txt" },
29
+ null,
30
+ 123,
31
+ { notPath: "ignored" },
32
+ ];
33
+ const result = asEmailAttachmentInputs(input);
34
+ expect(result).toEqual([
35
+ {
36
+ path: "docs/spec.pdf",
37
+ filename: "specs.pdf",
38
+ contentType: "application/pdf",
39
+ },
40
+ {
41
+ path: "images/logo.png",
42
+ filename: undefined,
43
+ contentType: undefined,
44
+ },
45
+ ]);
46
+ });
47
+ });
@@ -6,7 +6,11 @@ function makeDeps(overrides: Record<string, unknown> = {}) {
6
6
  return {
7
7
  getConfig: jest.fn(async () => undefined),
8
8
  getProxyUrl: jest.fn(() => "https://proxy.local"),
9
- resolveGitCredentials: jest.fn(async () => ({ token: "tok" })),
9
+ resolveGitCredentials: jest.fn(async () => ({
10
+ token: "tok",
11
+ authorName: "Dev",
12
+ authorEmail: "dev@example.com",
13
+ })),
10
14
  gitClone: jest.fn(async () => "demo"),
11
15
  gitCheckout: jest.fn(async () => "checked out"),
12
16
  gitBranch: jest.fn(async () => "branched"),
@@ -22,6 +26,15 @@ function makeDeps(overrides: Record<string, unknown> = {}) {
22
26
  gitPush: jest.fn(async () => "pushed"),
23
27
  gitMerge: jest.fn(async () => "merged"),
24
28
  gitReset: jest.fn(async () => "reset"),
29
+ gitFetch: jest.fn(async () => "fetched"),
30
+ gitInit: jest.fn(async () => "initialized"),
31
+ gitTag: jest.fn(async () => "tagged"),
32
+ gitDeleteBranch: jest.fn(async () => "deleted branch"),
33
+ gitRemote: jest.fn(async () => "remote info"),
34
+ gitConfig: jest.fn(async () => "config val"),
35
+ gitUnstage: jest.fn(async () => "unstaged"),
36
+ gitShow: jest.fn(async () => "commit info"),
37
+ gitReadFileAtRef: jest.fn(async () => "file content"),
25
38
  getRemoteUrl: jest.fn(async () => "https://example.com/repo.git"),
26
39
  readGroupFile: jest.fn(async () => ""),
27
40
  getGroupDir: jest.fn(async () => ({}) as any),
@@ -131,7 +144,7 @@ describe("worker/tools/git", () => {
131
144
  expect(result).toBe("M file.ts");
132
145
  });
133
146
 
134
- it("calls gitCommit directly", async () => {
147
+ it("calls gitCommit directly and passes author info", async () => {
135
148
  const deps = makeDeps({
136
149
  gitCommit: jest.fn(async () => "Committed abc1234: msg"),
137
150
  });
@@ -145,9 +158,16 @@ describe("worker/tools/git", () => {
145
158
  );
146
159
 
147
160
  expect(result).toContain("Committed");
161
+ expect(deps.gitCommit).toHaveBeenCalledWith(
162
+ expect.objectContaining({
163
+ authorName: "Dev",
164
+ authorEmail: "dev@example.com",
165
+ message: "msg",
166
+ }),
167
+ );
148
168
  });
149
169
 
150
- it("returns missing-credentials message for git_push", async () => {
170
+ it("returns missing-credentials message for git_push when credentials missing", async () => {
151
171
  const deps = makeDeps({
152
172
  resolveGitCredentials: jest.fn(async () => ({})),
153
173
  });
@@ -163,6 +183,23 @@ describe("worker/tools/git", () => {
163
183
  expect(result).toContain("No git credentials configured");
164
184
  });
165
185
 
186
+ it("calls git_push with valid credentials", async () => {
187
+ const deps = makeDeps({
188
+ gitPush: jest.fn(async () => "Pushed main successfully"),
189
+ });
190
+
191
+ const result = await executeGitTool(
192
+ {} as any,
193
+ "git_push",
194
+ { repo: "demo", branch: "main" },
195
+ "group-1",
196
+ deps,
197
+ );
198
+
199
+ expect(result).toBe("Pushed main successfully");
200
+ expect(deps.gitPush).toHaveBeenCalled();
201
+ });
202
+
166
203
  it("formats merge conflicts with inline regions", async () => {
167
204
  const deps = makeDeps({
168
205
  gitMerge: jest.fn(async () => {
@@ -197,4 +234,178 @@ describe("worker/tools/git", () => {
197
234
  expect(result).toContain("Resolution steps:");
198
235
  expect(result).toContain("<<<<<<< ours");
199
236
  });
237
+
238
+ it("executes remaining git tool operations (add, branch, branches, checkout, config, etc.)", async () => {
239
+ const deps = makeDeps();
240
+
241
+ // git_add
242
+ await executeGitTool(
243
+ {} as any,
244
+ "git_add",
245
+ { repo: "demo", filepath: "file.txt" },
246
+ "group-1",
247
+ deps,
248
+ );
249
+ expect(deps.gitAdd).toHaveBeenCalled();
250
+
251
+ // git_branch
252
+ await executeGitTool(
253
+ {} as any,
254
+ "git_branch",
255
+ { repo: "demo", name: "feat" },
256
+ "group-1",
257
+ deps,
258
+ );
259
+ expect(deps.gitBranch).toHaveBeenCalled();
260
+
261
+ // git_branches
262
+ await executeGitTool(
263
+ {} as any,
264
+ "git_branches",
265
+ { repo: "demo" },
266
+ "group-1",
267
+ deps,
268
+ );
269
+ expect(deps.gitListBranches).toHaveBeenCalled();
270
+
271
+ // git_checkout
272
+ await executeGitTool(
273
+ {} as any,
274
+ "git_checkout",
275
+ { repo: "demo", ref: "main" },
276
+ "group-1",
277
+ deps,
278
+ );
279
+ expect(deps.gitCheckout).toHaveBeenCalled();
280
+
281
+ // git_config
282
+ await executeGitTool(
283
+ {} as any,
284
+ "git_config",
285
+ { repo: "demo", command: "get", key: "user.name" },
286
+ "group-1",
287
+ deps,
288
+ );
289
+ expect(deps.gitConfig).toHaveBeenCalled();
290
+
291
+ // git_delete_branch
292
+ await executeGitTool(
293
+ {} as any,
294
+ "git_delete_branch",
295
+ { repo: "demo", name: "old" },
296
+ "group-1",
297
+ deps,
298
+ );
299
+ expect(deps.gitDeleteBranch).toHaveBeenCalled();
300
+
301
+ // git_diff
302
+ await executeGitTool(
303
+ {} as any,
304
+ "git_diff",
305
+ { repo: "demo" },
306
+ "group-1",
307
+ deps,
308
+ );
309
+ expect(deps.gitDiff).toHaveBeenCalled();
310
+
311
+ // git_fetch
312
+ await executeGitTool(
313
+ {} as any,
314
+ "git_fetch",
315
+ { repo: "demo" },
316
+ "group-1",
317
+ deps,
318
+ );
319
+ expect(deps.gitFetch).toHaveBeenCalled();
320
+
321
+ // git_init
322
+ await executeGitTool(
323
+ {} as any,
324
+ "git_init",
325
+ { repo: "demo" },
326
+ "group-1",
327
+ deps,
328
+ );
329
+ expect(deps.gitInit).toHaveBeenCalled();
330
+
331
+ // git_log
332
+ await executeGitTool(
333
+ {} as any,
334
+ "git_log",
335
+ { repo: "demo" },
336
+ "group-1",
337
+ deps,
338
+ );
339
+ expect(deps.gitLog).toHaveBeenCalled();
340
+
341
+ // git_pull
342
+ await executeGitTool(
343
+ {} as any,
344
+ "git_pull",
345
+ { repo: "demo" },
346
+ "group-1",
347
+ deps,
348
+ );
349
+ expect(deps.gitPull).toHaveBeenCalled();
350
+
351
+ // git_read_file_at_ref
352
+ await executeGitTool(
353
+ {} as any,
354
+ "git_read_file_at_ref",
355
+ { repo: "demo", ref: "main", filepath: "a.ts" },
356
+ "group-1",
357
+ deps,
358
+ );
359
+ expect(deps.gitReadFileAtRef).toHaveBeenCalled();
360
+
361
+ // git_remote
362
+ await executeGitTool(
363
+ {} as any,
364
+ "git_remote",
365
+ { repo: "demo", command: "list" },
366
+ "group-1",
367
+ deps,
368
+ );
369
+ expect(deps.gitRemote).toHaveBeenCalled();
370
+
371
+ // git_reset
372
+ await executeGitTool(
373
+ {} as any,
374
+ "git_reset",
375
+ { repo: "demo", ref: "HEAD~1" },
376
+ "group-1",
377
+ deps,
378
+ );
379
+ expect(deps.gitReset).toHaveBeenCalled();
380
+
381
+ // git_show
382
+ await executeGitTool(
383
+ {} as any,
384
+ "git_show",
385
+ { repo: "demo", ref: "HEAD" },
386
+ "group-1",
387
+ deps,
388
+ );
389
+ expect(deps.gitShow).toHaveBeenCalled();
390
+
391
+ // git_tag
392
+ await executeGitTool(
393
+ {} as any,
394
+ "git_tag",
395
+ { repo: "demo", tag: "v1.0" },
396
+ "group-1",
397
+ deps,
398
+ );
399
+ expect(deps.gitTag).toHaveBeenCalled();
400
+
401
+ // git_unstage
402
+ await executeGitTool(
403
+ {} as any,
404
+ "git_unstage",
405
+ { repo: "demo", filepath: "a.ts" },
406
+ "group-1",
407
+ deps,
408
+ );
409
+ expect(deps.gitUnstage).toHaveBeenCalled();
410
+ });
200
411
  });
@@ -0,0 +1,54 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("../../utils/post.js", () => ({
4
+ post: jest.fn(),
5
+ }));
6
+
7
+ const { executeInviteToRoom } = await import("./invite-to-room.js");
8
+ const { post } = await import("../../utils/post.js");
9
+
10
+ describe("executeInviteToRoom", () => {
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ });
14
+
15
+ it("returns error when peer_id is missing", () => {
16
+ const result = executeInviteToRoom({}, "br:room-123");
17
+ expect(result).toBe("Error: peer_id is required to invite a participant.");
18
+ expect(post).not.toHaveBeenCalled();
19
+ });
20
+
21
+ it("returns error when room_id is missing and groupId is not a room", () => {
22
+ const result = executeInviteToRoom({ peer_id: "peer-abc" }, "default");
23
+ expect(result).toBe(
24
+ "Error: no room_id provided and the current conversation is not a room.",
25
+ );
26
+ expect(post).not.toHaveBeenCalled();
27
+ });
28
+
29
+ it("invites peer using explicit room_id", () => {
30
+ const result = executeInviteToRoom(
31
+ { peer_id: "peer-abc", room_id: "custom-room" },
32
+ "default",
33
+ );
34
+
35
+ expect(result).toBe("Invited peer peer-abc to room custom-room.");
36
+ expect(post).toHaveBeenCalledWith({
37
+ payload: { action: "invite", roomId: "custom-room", peerId: "peer-abc" },
38
+ type: "room-action",
39
+ });
40
+ });
41
+
42
+ it("invites peer inferring room from room groupId", () => {
43
+ const result = executeInviteToRoom(
44
+ { peer_id: "peer-xyz" },
45
+ "room:active-room",
46
+ );
47
+
48
+ expect(result).toBe("Invited peer peer-xyz to room active-room.");
49
+ expect(post).toHaveBeenCalledWith({
50
+ payload: { action: "invite", roomId: "active-room", peerId: "peer-xyz" },
51
+ type: "room-action",
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,40 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("../../utils/post.js", () => ({
4
+ post: jest.fn(),
5
+ }));
6
+
7
+ const { executeLeaveRoom } = await import("./leave-room.js");
8
+ const { post } = await import("../../utils/post.js");
9
+
10
+ describe("executeLeaveRoom", () => {
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ });
14
+
15
+ it("returns error when room_id is missing and groupId is not a room", () => {
16
+ const result = executeLeaveRoom({}, "main-group");
17
+ expect(result).toBe(
18
+ "Error: no room_id provided and the current conversation is not a room.",
19
+ );
20
+ expect(post).not.toHaveBeenCalled();
21
+ });
22
+
23
+ it("leaves room with explicit room_id", () => {
24
+ const result = executeLeaveRoom({ room_id: "room-abc" }, "main-group");
25
+ expect(result).toBe("Leaving room room-abc.");
26
+ expect(post).toHaveBeenCalledWith({
27
+ payload: { action: "leave", roomId: "room-abc" },
28
+ type: "room-action",
29
+ });
30
+ });
31
+
32
+ it("leaves room inferring room from room groupId", () => {
33
+ const result = executeLeaveRoom({}, "room:current-room");
34
+ expect(result).toBe("Leaving room current-room.");
35
+ expect(post).toHaveBeenCalledWith({
36
+ payload: { action: "leave", roomId: "current-room" },
37
+ type: "room-action",
38
+ });
39
+ });
40
+ });