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,346 @@
1
+ import { jest } from "@jest/globals";
2
+ import { A2UI_BASIC_CATALOG_ID } from "../../../ui/a2ui/types.js";
3
+
4
+ jest.unstable_mockModule("../../utils/post.js", () => ({
5
+ post: jest.fn(),
6
+ }));
7
+
8
+ const { post } = await import("../../utils/post.js");
9
+ const { executeRenderComponent } = await import("./render-component.js");
10
+
11
+ describe("executeRenderComponent", () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ it("returns error when surfaceId is missing or not a string", () => {
17
+ const res1 = executeRenderComponent({ action: "createSurface" }, "g1");
18
+ expect(res1).toBe("Error: render_component requires a surfaceId string.");
19
+
20
+ const res2 = executeRenderComponent(
21
+ { action: "createSurface", surfaceId: 123 },
22
+ "g1",
23
+ );
24
+ expect(res2).toBe("Error: render_component requires a surfaceId string.");
25
+ });
26
+
27
+ it("returns error on unknown action", () => {
28
+ const res = executeRenderComponent(
29
+ { surfaceId: "s1", action: "invalidAction" },
30
+ "g1",
31
+ );
32
+ expect(res).toContain("Error: Unknown render_component action");
33
+ });
34
+
35
+ describe("createSurface", () => {
36
+ it("returns error if components is missing", () => {
37
+ const res = executeRenderComponent(
38
+ { surfaceId: "s1", action: "createSurface" },
39
+ "g1",
40
+ );
41
+ expect(res).toBe(
42
+ "Error: createSurface requires a components array (or legacy map).",
43
+ );
44
+ });
45
+
46
+ it("returns error if components is primitive or null", () => {
47
+ const res = executeRenderComponent(
48
+ {
49
+ surfaceId: "s1",
50
+ action: "createSurface",
51
+ components: 123 as any,
52
+ },
53
+ "g1",
54
+ );
55
+ expect(res).toContain(
56
+ 'Error: createSurface requires a component with id "root".',
57
+ );
58
+ });
59
+
60
+ it("returns error if no component with id root exists", () => {
61
+ const res = executeRenderComponent(
62
+ {
63
+ surfaceId: "s1",
64
+ action: "createSurface",
65
+ components: [{ id: "btn", component: "Button" }],
66
+ },
67
+ "g1",
68
+ );
69
+ expect(res).toContain(
70
+ 'Error: createSurface requires a component with id "root".',
71
+ );
72
+ });
73
+
74
+ it("successfully posts createSurface with array components and flattens properties", () => {
75
+ const res = executeRenderComponent(
76
+ {
77
+ surfaceId: "s1",
78
+ action: "createSurface",
79
+ components: [
80
+ { id: "root", component: "Column", properties: { spacing: "md" } },
81
+ { id: "btn", component: "Button", text: "Click" },
82
+ ],
83
+ dataModel: { count: 0 },
84
+ sendDataModel: true,
85
+ surfaceProperties: { theme: "dark" },
86
+ },
87
+ "group-123",
88
+ );
89
+
90
+ expect(res).toBe('A2UI surface "s1" rendered (action: createSurface).');
91
+ expect(post).toHaveBeenCalledWith({
92
+ type: "render-component",
93
+ payload: {
94
+ groupId: "group-123",
95
+ envelope: {
96
+ version: "v1.0",
97
+ type: "createSurface",
98
+ surfaceId: "s1",
99
+ catalogId: A2UI_BASIC_CATALOG_ID,
100
+ components: [
101
+ { id: "root", component: "Column", spacing: "md" },
102
+ { id: "btn", component: "Button", text: "Click" },
103
+ ],
104
+ dataModel: { count: 0 },
105
+ sendDataModel: true,
106
+ surfaceProperties: { theme: "dark" },
107
+ },
108
+ },
109
+ });
110
+ });
111
+
112
+ it("successfully converts legacy map format to components array", () => {
113
+ const res = executeRenderComponent(
114
+ {
115
+ surfaceId: "s1",
116
+ action: "createSurface",
117
+ components: {
118
+ root: { component: "Column", properties: { padding: 4 } },
119
+ item1: { component: "Text", properties: { text: "Hello" } },
120
+ },
121
+ },
122
+ "group-123",
123
+ );
124
+
125
+ expect(res).toBe('A2UI surface "s1" rendered (action: createSurface).');
126
+ expect(post).toHaveBeenCalledWith({
127
+ type: "render-component",
128
+ payload: {
129
+ groupId: "group-123",
130
+ envelope: {
131
+ version: "v1.0",
132
+ type: "createSurface",
133
+ surfaceId: "s1",
134
+ catalogId: A2UI_BASIC_CATALOG_ID,
135
+ components: [
136
+ { id: "root", component: "Column", padding: 4 },
137
+ { id: "item1", component: "Text", text: "Hello" },
138
+ ],
139
+ dataModel: undefined,
140
+ sendDataModel: undefined,
141
+ surfaceProperties: undefined,
142
+ },
143
+ },
144
+ });
145
+ });
146
+ });
147
+
148
+ describe("updateComponents", () => {
149
+ it("returns error if components is missing", () => {
150
+ const res = executeRenderComponent(
151
+ { surfaceId: "s1", action: "updateComponents" },
152
+ "g1",
153
+ );
154
+ expect(res).toBe(
155
+ "Error: updateComponents requires a components array (or legacy map).",
156
+ );
157
+ });
158
+
159
+ it("successfully posts updateComponents envelope", () => {
160
+ const res = executeRenderComponent(
161
+ {
162
+ surfaceId: "s1",
163
+ action: "updateComponents",
164
+ components: [
165
+ {
166
+ id: "btn",
167
+ component: "Button",
168
+ properties: { label: "Updated" },
169
+ },
170
+ ],
171
+ },
172
+ "group-123",
173
+ );
174
+
175
+ expect(res).toBe(
176
+ 'A2UI surface "s1" rendered (action: updateComponents).',
177
+ );
178
+ expect(post).toHaveBeenCalledWith({
179
+ type: "render-component",
180
+ payload: {
181
+ groupId: "group-123",
182
+ envelope: {
183
+ version: "v1.0",
184
+ type: "updateComponents",
185
+ surfaceId: "s1",
186
+ components: [{ id: "btn", component: "Button", label: "Updated" }],
187
+ },
188
+ },
189
+ });
190
+ });
191
+ });
192
+
193
+ describe("updateDataModel", () => {
194
+ it("handles spec-compliant path and value", () => {
195
+ const res = executeRenderComponent(
196
+ {
197
+ surfaceId: "s1",
198
+ action: "updateDataModel",
199
+ path: "/user/name",
200
+ value: "Alice",
201
+ },
202
+ "group-123",
203
+ );
204
+
205
+ expect(res).toBe('A2UI surface "s1" rendered (action: updateDataModel).');
206
+ expect(post).toHaveBeenCalledWith({
207
+ type: "render-component",
208
+ payload: {
209
+ groupId: "group-123",
210
+ envelope: {
211
+ version: "v1.0",
212
+ type: "updateDataModel",
213
+ surfaceId: "s1",
214
+ path: "/user/name",
215
+ value: "Alice",
216
+ },
217
+ },
218
+ });
219
+ });
220
+
221
+ it("handles deprecated patches map with single key", () => {
222
+ const res = executeRenderComponent(
223
+ {
224
+ surfaceId: "s1",
225
+ action: "updateDataModel",
226
+ patches: {
227
+ "/user/age": 30,
228
+ },
229
+ },
230
+ "group-123",
231
+ );
232
+
233
+ expect(res).toBe('A2UI surface "s1" rendered (action: updateDataModel).');
234
+ expect(post).toHaveBeenCalledWith({
235
+ type: "render-component",
236
+ payload: {
237
+ groupId: "group-123",
238
+ envelope: {
239
+ version: "v1.0",
240
+ type: "updateDataModel",
241
+ surfaceId: "s1",
242
+ path: "/user/age",
243
+ value: 30,
244
+ },
245
+ },
246
+ });
247
+ });
248
+
249
+ it("handles deprecated patches map with multiple keys posting separate envelopes", () => {
250
+ const res = executeRenderComponent(
251
+ {
252
+ surfaceId: "s1",
253
+ action: "updateDataModel",
254
+ patches: {
255
+ "/user/k1": "v1",
256
+ "/user/k2": "v2",
257
+ "/user/k3": "v3",
258
+ },
259
+ },
260
+ "group-123",
261
+ );
262
+
263
+ expect(res).toBe('A2UI surface "s1" rendered (action: updateDataModel).');
264
+ // Total 3 post calls (2 in loop, 1 final)
265
+ expect(post).toHaveBeenCalledTimes(3);
266
+ expect(post).toHaveBeenNthCalledWith(1, {
267
+ type: "render-component",
268
+ payload: {
269
+ groupId: "group-123",
270
+ envelope: {
271
+ version: "v1.0",
272
+ type: "updateDataModel",
273
+ surfaceId: "s1",
274
+ path: "/user/k2",
275
+ value: "v2",
276
+ },
277
+ },
278
+ });
279
+ expect(post).toHaveBeenNthCalledWith(2, {
280
+ type: "render-component",
281
+ payload: {
282
+ groupId: "group-123",
283
+ envelope: {
284
+ version: "v1.0",
285
+ type: "updateDataModel",
286
+ surfaceId: "s1",
287
+ path: "/user/k3",
288
+ value: "v3",
289
+ },
290
+ },
291
+ });
292
+ expect(post).toHaveBeenNthCalledWith(3, {
293
+ type: "render-component",
294
+ payload: {
295
+ groupId: "group-123",
296
+ envelope: {
297
+ version: "v1.0",
298
+ type: "updateDataModel",
299
+ surfaceId: "s1",
300
+ path: "/user/k1",
301
+ value: "v1",
302
+ },
303
+ },
304
+ });
305
+ });
306
+
307
+ it("returns error on empty patches object", () => {
308
+ const res = executeRenderComponent(
309
+ {
310
+ surfaceId: "s1",
311
+ action: "updateDataModel",
312
+ patches: {},
313
+ },
314
+ "group-123",
315
+ );
316
+
317
+ expect(res).toBe("Error: updateDataModel patches object is empty.");
318
+ expect(post).not.toHaveBeenCalled();
319
+ });
320
+ });
321
+
322
+ describe("deleteSurface", () => {
323
+ it("successfully posts deleteSurface envelope", () => {
324
+ const res = executeRenderComponent(
325
+ {
326
+ surfaceId: "s1",
327
+ action: "deleteSurface",
328
+ },
329
+ "group-123",
330
+ );
331
+
332
+ expect(res).toBe('A2UI surface "s1" rendered (action: deleteSurface).');
333
+ expect(post).toHaveBeenCalledWith({
334
+ type: "render-component",
335
+ payload: {
336
+ groupId: "group-123",
337
+ envelope: {
338
+ version: "v1.0",
339
+ type: "deleteSurface",
340
+ surfaceId: "s1",
341
+ },
342
+ },
343
+ });
344
+ });
345
+ });
346
+ });
@@ -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 { executeSendNotification } = await import("./send-notification.js");
8
+ const { post } = await import("../../utils/post.js");
9
+
10
+ describe("executeSendNotification", () => {
11
+ it("posts send-notification event and returns confirmation", () => {
12
+ const result = executeSendNotification(
13
+ { body: "Task completed successfully", title: "Build Notification" },
14
+ "group-789",
15
+ );
16
+
17
+ expect(result).toBe("Push notification sent: Task completed successfully");
18
+ expect(post).toHaveBeenCalledWith({
19
+ type: "send-notification",
20
+ payload: {
21
+ body: "Task completed successfully",
22
+ groupId: "group-789",
23
+ title: "Build Notification",
24
+ },
25
+ });
26
+ });
27
+
28
+ it("uses default title when title is omitted", () => {
29
+ executeSendNotification({ body: "Ping" }, "group-789");
30
+
31
+ expect(post).toHaveBeenCalledWith({
32
+ type: "send-notification",
33
+ payload: {
34
+ body: "Ping",
35
+ groupId: "group-789",
36
+ title: "ShadowClaw",
37
+ },
38
+ });
39
+ });
40
+ });
@@ -0,0 +1,31 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("../../../../db/getConfig.js", () => ({
4
+ getConfig: jest.fn(),
5
+ }));
6
+
7
+ const { getAllowFullInternetAccess } =
8
+ await import("./getAllowFullInternetAccess.js");
9
+ const { getConfig } = await import("../../../../db/getConfig.js");
10
+
11
+ describe("getAllowFullInternetAccess", () => {
12
+ const mockDb: any = {};
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ it("returns true when config is 'true'", async () => {
19
+ (getConfig as jest.Mock<any>).mockResolvedValue("true");
20
+ const result = await getAllowFullInternetAccess(mockDb);
21
+ expect(result).toBe(true);
22
+ });
23
+
24
+ it("returns false when config is false or missing", async () => {
25
+ (getConfig as jest.Mock<any>).mockResolvedValue("false");
26
+ expect(await getAllowFullInternetAccess(mockDb)).toBe(false);
27
+
28
+ (getConfig as jest.Mock<any>).mockResolvedValue(null);
29
+ expect(await getAllowFullInternetAccess(mockDb)).toBe(false);
30
+ });
31
+ });
@@ -0,0 +1,25 @@
1
+ import { parseBooleanConfig } from "./parseBooleanConfig.js";
2
+
3
+ describe("parseBooleanConfig", () => {
4
+ it("parses truthy boolean strings", () => {
5
+ expect(parseBooleanConfig("true")).toBe(true);
6
+ expect(parseBooleanConfig("1")).toBe(true);
7
+ expect(parseBooleanConfig("yes")).toBe(true);
8
+ expect(parseBooleanConfig("on")).toBe(true);
9
+ expect(parseBooleanConfig(" TRUE ")).toBe(true);
10
+ });
11
+
12
+ it("parses falsy boolean strings", () => {
13
+ expect(parseBooleanConfig("false")).toBe(false);
14
+ expect(parseBooleanConfig("0")).toBe(false);
15
+ expect(parseBooleanConfig("no")).toBe(false);
16
+ expect(parseBooleanConfig("off")).toBe(false);
17
+ expect(parseBooleanConfig(" FALSE ")).toBe(false);
18
+ });
19
+
20
+ it("returns null for null, undefined, or unrecognized strings", () => {
21
+ expect(parseBooleanConfig(null)).toBeNull();
22
+ expect(parseBooleanConfig(undefined)).toBeNull();
23
+ expect(parseBooleanConfig("other")).toBeNull();
24
+ });
25
+ });
@@ -0,0 +1,84 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("../../../storage/readGroupFile.js", () => ({
4
+ readGroupFile: jest.fn(),
5
+ }));
6
+
7
+ const { executeAttachFile } = await import("./attach-file.js");
8
+ const { readGroupFile } = await import("../../../storage/readGroupFile.js");
9
+
10
+ describe("executeAttachFile", () => {
11
+ const mockDb: any = {};
12
+ const groupId = "test-group";
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ it("returns error if path is missing or not a string", async () => {
19
+ const result1 = await executeAttachFile(mockDb, {}, groupId);
20
+ expect(result1).toBe(
21
+ "Error: attach_file_to_chat requires a valid path string.",
22
+ );
23
+
24
+ const result2 = await executeAttachFile(mockDb, { path: 42 }, groupId);
25
+ expect(result2).toBe(
26
+ "Error: attach_file_to_chat requires a valid path string.",
27
+ );
28
+ });
29
+
30
+ it("returns error if path is empty", async () => {
31
+ const result = await executeAttachFile(mockDb, { path: " " }, groupId);
32
+ expect(result).toBe(
33
+ "Error: attach_file_to_chat received an empty file path.",
34
+ );
35
+ });
36
+
37
+ it("returns error if path contains traversal '..' segments", async () => {
38
+ const result = await executeAttachFile(
39
+ mockDb,
40
+ { path: "../secret.txt" },
41
+ groupId,
42
+ );
43
+ expect(result).toBe(
44
+ "Error: attach_file_to_chat path cannot contain '..' segments.",
45
+ );
46
+ });
47
+
48
+ it("returns error when file does not exist", async () => {
49
+ (readGroupFile as jest.Mock<any>).mockRejectedValue(new Error("Not found"));
50
+
51
+ const result = await executeAttachFile(
52
+ mockDb,
53
+ { path: "missing.md" },
54
+ groupId,
55
+ );
56
+ expect(result).toContain("could not find missing.md: Not found");
57
+ });
58
+
59
+ it("prepares standard markdown link for regular file", async () => {
60
+ (readGroupFile as jest.Mock<any>).mockResolvedValue("file content");
61
+
62
+ const result = await executeAttachFile(
63
+ mockDb,
64
+ { path: "docs/guide.md", alt: "User Guide" },
65
+ groupId,
66
+ );
67
+
68
+ expect(result).toContain("Attachment prepared: docs/guide.md");
69
+ expect(result).toContain("[User Guide](docs/guide.md)");
70
+ });
71
+
72
+ it("prepares image markdown syntax for image file", async () => {
73
+ (readGroupFile as jest.Mock<any>).mockResolvedValue("image data");
74
+
75
+ const result = await executeAttachFile(
76
+ mockDb,
77
+ { path: "images/banner.png" },
78
+ groupId,
79
+ );
80
+
81
+ expect(result).toContain("Attachment prepared: images/banner.png");
82
+ expect(result).toContain("![banner.png](images/banner.png)");
83
+ });
84
+ });
@@ -0,0 +1,69 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("../../../storage/copyGroupEntry.js", () => ({
4
+ copyGroupEntry: jest.fn(),
5
+ }));
6
+
7
+ const { executeCopyFile } = await import("./copy-file.js");
8
+ const { copyGroupEntry } = await import("../../../storage/copyGroupEntry.js");
9
+
10
+ describe("executeCopyFile", () => {
11
+ const mockDb: any = {};
12
+ const groupId = "test-group";
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ it("throws error when source_path or target_path is missing", async () => {
19
+ await expect(
20
+ executeCopyFile(mockDb, { source_path: "a" }, groupId),
21
+ ).rejects.toThrow("Missing source_path or target_path parameter");
22
+ await expect(
23
+ executeCopyFile(mockDb, { target_path: "b" }, groupId),
24
+ ).rejects.toThrow("Missing source_path or target_path parameter");
25
+ });
26
+
27
+ it("copies entry within same group", async () => {
28
+ (copyGroupEntry as jest.Mock<any>).mockResolvedValue(undefined);
29
+
30
+ const result = await executeCopyFile(
31
+ mockDb,
32
+ { source_path: "docs/a.md", target_path: "docs/b.md" },
33
+ groupId,
34
+ );
35
+
36
+ expect(result).toBe("Copied docs/a.md to docs/b.md");
37
+ expect(copyGroupEntry).toHaveBeenCalledWith(
38
+ mockDb,
39
+ groupId,
40
+ groupId,
41
+ "docs/a.md",
42
+ "docs/b.md",
43
+ );
44
+ });
45
+
46
+ it("copies entry across different groups", async () => {
47
+ (copyGroupEntry as jest.Mock<any>).mockResolvedValue(undefined);
48
+
49
+ const result = await executeCopyFile(
50
+ mockDb,
51
+ {
52
+ source_path: "a.md",
53
+ target_path: "b.md",
54
+ source_group_id: "group-1",
55
+ target_group_id: "group-2",
56
+ },
57
+ groupId,
58
+ );
59
+
60
+ expect(result).toBe("Copied a.md to b.md (from group-1 to group-2)");
61
+ expect(copyGroupEntry).toHaveBeenCalledWith(
62
+ mockDb,
63
+ "group-1",
64
+ "group-2",
65
+ "a.md",
66
+ "b.md",
67
+ );
68
+ });
69
+ });
@@ -0,0 +1,40 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("../../../storage/createGroupDirectory.js", () => ({
4
+ createGroupDirectory: jest.fn(),
5
+ }));
6
+
7
+ const { executeCreateDirectory } = await import("./create-directory.js");
8
+ const { createGroupDirectory } =
9
+ await import("../../../storage/createGroupDirectory.js");
10
+
11
+ describe("executeCreateDirectory", () => {
12
+ const mockDb: any = {};
13
+ const groupId = "test-group";
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ });
18
+
19
+ it("throws error when path parameter is missing", async () => {
20
+ await expect(executeCreateDirectory(mockDb, {}, groupId)).rejects.toThrow(
21
+ "Missing path parameter",
22
+ );
23
+ });
24
+
25
+ it("creates directory successfully", async () => {
26
+ (createGroupDirectory as jest.Mock<any>).mockResolvedValue(undefined);
27
+
28
+ const result = await executeCreateDirectory(
29
+ mockDb,
30
+ { path: "docs/images" },
31
+ groupId,
32
+ );
33
+ expect(result).toBe("Created directory docs/images");
34
+ expect(createGroupDirectory).toHaveBeenCalledWith(
35
+ mockDb,
36
+ groupId,
37
+ "docs/images",
38
+ );
39
+ });
40
+ });