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
@@ -74,21 +74,6 @@ class MockWorker {
74
74
  terminate() {}
75
75
  }
76
76
 
77
- // Add generic Web APIs for JSDOM from Node globals
78
- globalThis.Response = global.Response;
79
- globalThis.Request = global.Request;
80
- globalThis.Headers = global.Headers;
81
- (globalThis as any).__PRERENDER_MAIN_MEMORY__ = false;
82
-
83
- if (typeof (globalThis as any).Worker === "undefined") {
84
- (globalThis as any).Worker = MockWorker;
85
- }
86
-
87
- if (typeof globalThis.structuredClone !== "function") {
88
- (globalThis as any).structuredClone = <T>(value: T): T =>
89
- deserialize(serialize(value));
90
- }
91
-
92
77
  // Patch TextEncoder
93
78
  if (typeof globalThis.TextEncoder === "undefined") {
94
79
  (globalThis as any).TextEncoder = class TextEncoder {
@@ -101,10 +86,236 @@ if (typeof globalThis.TextEncoder === "undefined") {
101
86
  // Patch TextDecoder
102
87
  if (typeof globalThis.TextDecoder === "undefined") {
103
88
  (globalThis as any).TextDecoder = class TextDecoder {
104
- decode(buf: Uint8Array): string {
105
- return Buffer.from(buf).toString("utf-8");
89
+ decode(buf?: Uint8Array | ArrayBuffer): string {
90
+ if (!buf) return "";
91
+ if (buf instanceof Uint8Array) {
92
+ return Buffer.from(buf).toString("utf-8");
93
+ }
94
+ return Buffer.from(new Uint8Array(buf)).toString("utf-8");
95
+ }
96
+ };
97
+ }
98
+
99
+ // Polyfill HTMLDialogElement in JSDOM
100
+ if (typeof globalThis.HTMLDialogElement !== "undefined") {
101
+ if (!globalThis.HTMLDialogElement.prototype.showModal) {
102
+ globalThis.HTMLDialogElement.prototype.showModal = function () {
103
+ this.setAttribute("open", "");
104
+ };
105
+ }
106
+ if (!globalThis.HTMLDialogElement.prototype.close) {
107
+ globalThis.HTMLDialogElement.prototype.close = function (
108
+ returnValue?: string,
109
+ ) {
110
+ this.removeAttribute("open");
111
+ if (returnValue !== undefined) {
112
+ this.returnValue = returnValue;
113
+ }
114
+ };
115
+ }
116
+ }
117
+
118
+ class MockHeaders {
119
+ private map = new Map<string, string>();
120
+ constructor(init?: any) {
121
+ if (init) {
122
+ if (typeof init.forEach === "function") {
123
+ init.forEach((v: string, k: string) =>
124
+ this.map.set(k.toLowerCase(), v),
125
+ );
126
+ } else if (Array.isArray(init)) {
127
+ for (const [k, v] of init) this.map.set(k.toLowerCase(), String(v));
128
+ } else if (typeof init === "object") {
129
+ for (const [k, v] of Object.entries(init)) {
130
+ if (v != null) this.map.set(k.toLowerCase(), String(v));
131
+ }
132
+ }
133
+ }
134
+ }
135
+ get(name: string) {
136
+ return this.map.get(name.toLowerCase()) ?? null;
137
+ }
138
+ set(name: string, value: string) {
139
+ this.map.set(name.toLowerCase(), String(value));
140
+ }
141
+ has(name: string) {
142
+ return this.map.has(name.toLowerCase());
143
+ }
144
+ delete(name: string) {
145
+ this.map.delete(name.toLowerCase());
146
+ }
147
+ forEach(callback: (value: string, key: string) => void) {
148
+ this.map.forEach((v, k) => callback(v, k));
149
+ }
150
+ entries() {
151
+ return this.map.entries();
152
+ }
153
+ keys() {
154
+ return this.map.keys();
155
+ }
156
+ values() {
157
+ return this.map.values();
158
+ }
159
+ [Symbol.iterator]() {
160
+ return this.map.entries();
161
+ }
162
+ }
163
+
164
+ class MockBlob {
165
+ private parts: any[];
166
+ public size: number;
167
+ public type: string;
168
+ constructor(parts?: any[], options?: { type?: string }) {
169
+ this.parts = parts || [];
170
+ this.type = options?.type || "";
171
+ this.size = this.parts.reduce(
172
+ (acc, p) =>
173
+ acc +
174
+ (p?.byteLength ||
175
+ p?.length ||
176
+ (typeof p === "string" ? Buffer.byteLength(p) : 0)),
177
+ 0,
178
+ );
179
+ }
180
+ async arrayBuffer(): Promise<ArrayBuffer> {
181
+ const combined = new Uint8Array(this.size);
182
+ let offset = 0;
183
+ for (const p of this.parts) {
184
+ if (p instanceof Uint8Array) {
185
+ combined.set(p, offset);
186
+ offset += p.byteLength;
187
+ } else if (p instanceof ArrayBuffer) {
188
+ combined.set(new Uint8Array(p), offset);
189
+ offset += p.byteLength;
190
+ } else if (typeof p === "string") {
191
+ const encoded = new (globalThis as any).TextEncoder().encode(p);
192
+ combined.set(encoded, offset);
193
+ offset += encoded.byteLength;
194
+ }
195
+ }
196
+ return combined.buffer;
197
+ }
198
+ async text(): Promise<string> {
199
+ const ab = await this.arrayBuffer();
200
+ return new (globalThis as any).TextDecoder().decode(ab);
201
+ }
202
+ stream() {
203
+ let resolved = false;
204
+ let bytes: Uint8Array | null = null;
205
+ const blobPromise = this.arrayBuffer().then((ab) => {
206
+ bytes = new Uint8Array(ab);
207
+ resolved = true;
208
+ });
209
+ return new ReadableStream<Uint8Array>({
210
+ async start(controller) {
211
+ if (!resolved) await blobPromise;
212
+ if (bytes && bytes.byteLength > 0) controller.enqueue(bytes);
213
+ controller.close();
214
+ },
215
+ });
216
+ }
217
+ }
218
+
219
+ class MockResponse {
220
+ body: any;
221
+ headers: any;
222
+ status: number;
223
+ statusText: string;
224
+ ok: boolean;
225
+ constructor(body?: any, init?: any) {
226
+ this.body = body;
227
+ this.status = init?.status ?? 200;
228
+ this.statusText = init?.statusText ?? (this.status === 200 ? "OK" : "");
229
+ this.ok = this.status >= 200 && this.status < 300;
230
+ const HeadersClass = (globalThis as any).Headers || MockHeaders;
231
+ this.headers =
232
+ init?.headers instanceof HeadersClass
233
+ ? init.headers
234
+ : new HeadersClass(init?.headers);
235
+ }
236
+ async json() {
237
+ if (typeof this.body === "string") return JSON.parse(this.body);
238
+ if (this.body instanceof Uint8Array) {
239
+ return JSON.parse(Buffer.from(this.body).toString("utf-8"));
240
+ }
241
+ return this.body;
242
+ }
243
+ async text() {
244
+ if (typeof this.body === "string") return this.body;
245
+ if (this.body instanceof Uint8Array) {
246
+ return Buffer.from(this.body).toString("utf-8");
247
+ }
248
+ if (this.body && typeof this.body.text === "function") {
249
+ return this.body.text();
250
+ }
251
+ return String(this.body ?? "");
252
+ }
253
+ async arrayBuffer() {
254
+ if (this.body instanceof Uint8Array) {
255
+ return this.body.buffer.slice(
256
+ this.body.byteOffset,
257
+ this.body.byteOffset + this.body.byteLength,
258
+ );
259
+ }
260
+ if (this.body instanceof ArrayBuffer) return this.body;
261
+ if (this.body && typeof this.body.arrayBuffer === "function") {
262
+ return this.body.arrayBuffer();
263
+ }
264
+ if (typeof this.body === "string") {
265
+ return new (globalThis as any).TextEncoder().encode(this.body).buffer;
266
+ }
267
+ return new ArrayBuffer(0);
268
+ }
269
+ async blob() {
270
+ if (this.body instanceof (globalThis as any).Blob) return this.body;
271
+ const ab = await this.arrayBuffer();
272
+ const BlobClass = (globalThis as any).Blob || MockBlob;
273
+ return new BlobClass([ab]);
274
+ }
275
+ }
276
+
277
+ if (typeof (globalThis as any).Headers === "undefined") {
278
+ (globalThis as any).Headers = MockHeaders;
279
+ (global as any).Headers = MockHeaders;
280
+ }
281
+
282
+ if (typeof (globalThis as any).Blob === "undefined") {
283
+ (globalThis as any).Blob = MockBlob;
284
+ (global as any).Blob = MockBlob;
285
+ }
286
+
287
+ if (typeof (globalThis as any).Response === "undefined") {
288
+ (globalThis as any).Response = MockResponse;
289
+ (global as any).Response = MockResponse;
290
+ }
291
+
292
+ if (typeof (globalThis as any).Request === "undefined") {
293
+ (globalThis as any).Request = class MockRequest {
294
+ url: string;
295
+ method: string;
296
+ headers: any;
297
+ constructor(input: any, init?: any) {
298
+ this.url = typeof input === "string" ? input : input?.url || "";
299
+ this.method = init?.method || "GET";
300
+ const HeadersClass = (globalThis as any).Headers || MockHeaders;
301
+ this.headers =
302
+ init?.headers instanceof HeadersClass
303
+ ? init.headers
304
+ : new HeadersClass(init?.headers);
106
305
  }
107
306
  };
307
+ (global as any).Request = (globalThis as any).Request;
308
+ }
309
+
310
+ (globalThis as any).__PRERENDER_MAIN_MEMORY__ = false;
311
+
312
+ if (typeof (globalThis as any).Worker === "undefined") {
313
+ (globalThis as any).Worker = MockWorker;
314
+ }
315
+
316
+ if (typeof globalThis.structuredClone !== "function") {
317
+ (globalThis as any).structuredClone = <T>(value: T): T =>
318
+ deserialize(serialize(value));
108
319
  }
109
320
 
110
321
  // Implement a filesystem-aware fetch for component templates/styles
@@ -117,7 +328,7 @@ globalThis.fetch = jest.fn((url: string | URL | Request) => {
117
328
  statusText: "OK",
118
329
  text: () => Promise.resolve(""),
119
330
  json: () => Promise.resolve({}),
120
- blob: () => Promise.resolve(new Blob()),
331
+ blob: () => Promise.resolve(new (globalThis as any).Blob()),
121
332
  arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
122
333
  } as Response);
123
334
  }
@@ -163,7 +374,7 @@ globalThis.fetch = jest.fn((url: string | URL | Request) => {
163
374
  ok: true,
164
375
  text: () => Promise.resolve(content),
165
376
  json: () => Promise.resolve(JSON.parse(content)),
166
- blob: () => Promise.resolve(new Blob([content])),
377
+ blob: () => Promise.resolve(new (globalThis as any).Blob([content])),
167
378
  arrayBuffer: () => Promise.resolve(Buffer.from(content).buffer),
168
379
  } as Response);
169
380
  } catch (err) {
@@ -178,6 +389,66 @@ globalThis.fetch = jest.fn((url: string | URL | Request) => {
178
389
  (globalThis as any).WritableStream = WritableStream;
179
390
  (globalThis as any).AudioContext = MockAudioContext;
180
391
 
392
+ // Polyfill ResizeObserver for JSDOM
393
+ if (typeof globalThis.ResizeObserver === "undefined") {
394
+ (globalThis as any).ResizeObserver = class MockResizeObserver {
395
+ callback: ResizeObserverCallback;
396
+ constructor(callback: ResizeObserverCallback) {
397
+ this.callback = callback;
398
+ }
399
+ observe() {}
400
+ unobserve() {}
401
+ disconnect() {}
402
+ };
403
+ }
404
+
405
+ const BaseMouseEvent: any =
406
+ typeof globalThis.MouseEvent !== "undefined"
407
+ ? globalThis.MouseEvent
408
+ : typeof globalThis.Event !== "undefined"
409
+ ? globalThis.Event
410
+ : class MockEvent {
411
+ type: string;
412
+ bubbles: boolean;
413
+ cancelable: boolean;
414
+ defaultPrevented = false;
415
+ constructor(type: string, init?: any) {
416
+ this.type = type;
417
+ this.bubbles = init?.bubbles ?? false;
418
+ this.cancelable = init?.cancelable ?? false;
419
+ }
420
+ preventDefault() {
421
+ this.defaultPrevented = true;
422
+ }
423
+ stopPropagation() {}
424
+ };
425
+
426
+ if (typeof globalThis.PointerEvent === "undefined") {
427
+ (globalThis as any).PointerEvent = class PointerEvent extends BaseMouseEvent {
428
+ pointerId: number;
429
+ pointerType: string;
430
+ clientX: number;
431
+ clientY: number;
432
+ constructor(type: string, init?: any) {
433
+ super(type, init);
434
+ this.pointerId = init?.pointerId ?? 0;
435
+ this.pointerType = init?.pointerType ?? "mouse";
436
+ this.clientX = init?.clientX ?? 0;
437
+ this.clientY = init?.clientY ?? 0;
438
+ }
439
+ };
440
+ }
441
+
442
+ if (typeof globalThis.DragEvent === "undefined") {
443
+ (globalThis as any).DragEvent = class DragEvent extends BaseMouseEvent {
444
+ dataTransfer: any;
445
+ constructor(type: string, init?: any) {
446
+ super(type, init);
447
+ this.dataTransfer = init?.dataTransfer ?? null;
448
+ }
449
+ };
450
+ }
451
+
181
452
  // Polyfill CSSStyleSheet and adoptedStyleSheets for JSDOM
182
453
  if (typeof globalThis.CSSStyleSheet === "undefined") {
183
454
  (globalThis as any).CSSStyleSheet = class MockCSSStyleSheet {
@@ -201,24 +472,26 @@ if (typeof globalThis.CSSStyleSheet === "undefined") {
201
472
  }
202
473
  }
203
474
 
204
- Object.defineProperty(ShadowRoot.prototype, "adoptedStyleSheets", {
205
- configurable: true,
206
- set(sheets: any[]) {
207
- (this as any)._adoptedStyleSheets = sheets;
208
- if (Array.isArray(sheets)) {
209
- sheets.forEach((sheet) => {
210
- if (sheet._css) {
211
- const style = document.createElement("style");
212
- style.textContent = sheet._css;
213
- this.appendChild(style);
214
- }
215
- });
216
- }
217
- },
218
- get() {
219
- return (this as any)._adoptedStyleSheets || [];
220
- },
221
- });
475
+ if (typeof ShadowRoot !== "undefined") {
476
+ Object.defineProperty(ShadowRoot.prototype, "adoptedStyleSheets", {
477
+ configurable: true,
478
+ set(sheets: any[]) {
479
+ (this as any)._adoptedStyleSheets = sheets;
480
+ if (Array.isArray(sheets)) {
481
+ sheets.forEach((sheet) => {
482
+ if (sheet._css) {
483
+ const style = document.createElement("style");
484
+ style.textContent = sheet._css;
485
+ this.appendChild(style);
486
+ }
487
+ });
488
+ }
489
+ },
490
+ get() {
491
+ return (this as any)._adoptedStyleSheets || [];
492
+ },
493
+ });
494
+ }
222
495
 
223
496
  const originalConsoleError = console.error;
224
497
  const originalConsoleWarn = console.warn;
@@ -243,11 +516,14 @@ const expectedLogs = [
243
516
  "PeerJsChannel: connected to signaling server",
244
517
  "PeerJsChannel: connection opened with",
245
518
  "PeerJsChannel: rejecting connection from untrusted peer:",
519
+ "PeerJsChannel: connection closed with",
246
520
  "[WebVM] Ignoring invalid VM boot host:",
247
521
  "[WebVM] Ignoring invalid VM relay URL:",
248
- "[WebVM:ui] Starting boot - checking assets...",
249
- "[WebVM boot] Mode",
250
- "[WebVM:ui] Assets not found.",
522
+ "[WebVM]",
523
+ "[WebVM:ui]",
524
+ "[WebVM boot]",
525
+ "[shadow-claw-a2ui] Failed to load workspace",
526
+ "[shadow-claw-a2ui] groupId not set, cannot resolve workspace files",
251
527
  "Failed to fetch Vertex AI models dynamically:",
252
528
  "Vertex AI proxy error:",
253
529
  "Error: Not implemented: navigation (except hash changes)",
@@ -276,6 +552,18 @@ const expectedLogs = [
276
552
  "[Security] Registration blocked:",
277
553
  "[Security] Removing unapproved custom element:",
278
554
  "Failed to retrieve local storage handle from DB:",
555
+ "fetch proxy: config updated",
556
+ "Error reaching huggingface.co:",
557
+ "Error reaching hf-mirror.com:",
558
+ "Hugging Face main domain unreachable",
559
+ "writePartialMeta error:",
560
+ "flushChunkToCache error:",
561
+ "[Proxy] Llamafile runtime:",
562
+ "[Proxy] Mesh LLM request for model:",
563
+ "[Proxy] Fetching Mesh LLM catalog...",
564
+ "Ollama invoke error:",
565
+ "Ollama models discovery error:",
566
+ "Failed to check Prompt API onboarding:",
279
567
  ];
280
568
 
281
569
  function isExpectedLog(...args: any[]) {
@@ -0,0 +1,49 @@
1
+ export class V86 {
2
+ constructor(options) {
3
+ this.options = options;
4
+ this.listeners = new Map();
5
+ this.serial0_send = (text) => {
6
+ if (text === "root\n") {
7
+ setTimeout(() => {
8
+ this.emitSerial("# ");
9
+ }, 10);
10
+ }
11
+ };
12
+ this.destroy = () => Promise.resolve();
13
+ this.fs9p = {
14
+ read_dir: () => [],
15
+ SearchPath: () => ({ id: 1 }),
16
+ IsDirectory: () => true,
17
+ };
18
+ this.create_file = () => Promise.resolve();
19
+ this.read_file = () => Promise.resolve(new Uint8Array([1, 2, 3]));
20
+
21
+ // Auto-emit boot sequence
22
+ setTimeout(() => {
23
+ this.emitSerial("Booting Linux...\nlogin: ");
24
+ }, 10);
25
+ }
26
+
27
+ add_listener(event, listener) {
28
+ if (!this.listeners.has(event)) {
29
+ this.listeners.set(event, new Set());
30
+ }
31
+ this.listeners.get(event).add(listener);
32
+ }
33
+
34
+ remove_listener(event, listener) {
35
+ this.listeners.get(event)?.delete(listener);
36
+ }
37
+
38
+ emitSerial(text) {
39
+ const serialListeners = this.listeners.get("serial0-output-byte");
40
+ if (!serialListeners) return;
41
+ for (const ch of text) {
42
+ for (const listener of serialListeners) {
43
+ listener(ch.charCodeAt(0));
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ export default V86;
@@ -423,7 +423,7 @@ export interface ColumnSpec extends ComponentCommon {
423
423
  align?: AlignValue;
424
424
  }
425
425
 
426
- export type ButtonVariant = "default" | "primary" | "borderless";
426
+ export type ButtonVariant = "default" | "primary" | "secondary" | "borderless";
427
427
 
428
428
  /**
429
429
  * A2UI v1.0 spec §actions: server action event sent when a button is clicked.
@@ -571,6 +571,7 @@ export interface CheckBoxSpec extends ComponentCommon, CheckableComponent {
571
571
  component: "CheckBox";
572
572
  label: DynamicString;
573
573
  value: DynamicBoolean;
574
+ action?: A2UIActionDescriptor;
574
575
  }
575
576
 
576
577
  export interface ChoicePickerOption {
@@ -592,14 +593,14 @@ export interface SliderSpec extends ComponentCommon, CheckableComponent {
592
593
  component: "Slider";
593
594
  label?: DynamicString;
594
595
  min?: number;
595
- max: number;
596
+ max?: number;
596
597
  value: DynamicNumber;
597
598
  steps?: number;
598
599
  }
599
600
 
600
601
  export interface DateTimeInputSpec extends ComponentCommon, CheckableComponent {
601
602
  component: "DateTimeInput";
602
- value: DynamicString;
603
+ value?: DynamicString;
603
604
  enableDate?: boolean;
604
605
  enableTime?: boolean;
605
606
  min?: DynamicString;
@@ -0,0 +1,102 @@
1
+ import { jest } from "@jest/globals";
2
+ import { normaliseComponentsToMap } from "./normaliseComponentsToMap.js";
3
+
4
+ describe("normaliseComponentsToMap", () => {
5
+ it("normalises a flat list of component specs", () => {
6
+ const specs = [
7
+ { id: "root", component: "Column", children: ["c1", "c2"] },
8
+ { id: "c1", component: "Text", text: "Hello" },
9
+ { id: "c2", component: "Button", text: "Submit" },
10
+ ] as any;
11
+
12
+ const map = normaliseComponentsToMap(specs);
13
+ expect(map["root"]).toEqual(specs[0]);
14
+ expect(map["c1"]).toEqual(specs[1]);
15
+ expect(map["c2"]).toEqual(specs[2]);
16
+ });
17
+
18
+ it("walks nested children array objects", () => {
19
+ const specs = [
20
+ {
21
+ id: "root",
22
+ component: "Column",
23
+ children: [
24
+ { id: "nested1", component: "Text", text: "Nested 1" },
25
+ { id: "nested2", component: "Text", text: "Nested 2" },
26
+ ],
27
+ },
28
+ ] as any;
29
+
30
+ const map = normaliseComponentsToMap(specs);
31
+ expect(map["root"]).toBeDefined();
32
+ expect(map["nested1"]).toEqual({
33
+ id: "nested1",
34
+ component: "Text",
35
+ text: "Nested 1",
36
+ });
37
+ expect(map["nested2"]).toEqual({
38
+ id: "nested2",
39
+ component: "Text",
40
+ text: "Nested 2",
41
+ });
42
+ });
43
+
44
+ it("does not walk children if children[0] is a template specification", () => {
45
+ const specs = [
46
+ {
47
+ id: "root",
48
+ component: "Column",
49
+ children: [{ path: "/items", componentId: "tmpl" }],
50
+ },
51
+ ] as any;
52
+
53
+ const map = normaliseComponentsToMap(specs);
54
+ expect(map["root"]).toBeDefined();
55
+ expect(Object.keys(map)).toEqual(["root"]);
56
+ });
57
+
58
+ it("walks nested child object and nested tabs", () => {
59
+ const specs = [
60
+ {
61
+ id: "card1",
62
+ component: "Card",
63
+ child: { id: "cardInner", component: "Text", text: "Inside Card" },
64
+ },
65
+ {
66
+ id: "tabs1",
67
+ component: "Tabs",
68
+ tabs: [
69
+ { title: "Tab 1", child: { id: "tabContent1", component: "Text" } },
70
+ { title: "Tab 2", child: { id: "tabContent2", component: "Text" } },
71
+ ],
72
+ },
73
+ ] as any;
74
+
75
+ const map = normaliseComponentsToMap(specs);
76
+ expect(map["card1"]).toBeDefined();
77
+ expect(map["cardInner"]).toBeDefined();
78
+ expect(map["tabs1"]).toBeDefined();
79
+ expect(map["tabContent1"]).toBeDefined();
80
+ expect(map["tabContent2"]).toBeDefined();
81
+ });
82
+
83
+ it("warns and skips components missing id field", () => {
84
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
85
+ const specs = [
86
+ null,
87
+ "invalid-primitive" as any,
88
+ { component: "Text", text: "No ID" },
89
+ { customField: 123 },
90
+ ] as any;
91
+
92
+ const map = normaliseComponentsToMap(specs);
93
+ expect(map).toEqual({});
94
+ expect(warnSpy).toHaveBeenCalledWith(
95
+ expect.stringContaining(
96
+ "Component missing required 'id' field; skipped.",
97
+ ),
98
+ expect.anything(),
99
+ );
100
+ warnSpy.mockRestore();
101
+ });
102
+ });
@@ -0,0 +1,63 @@
1
+ import { jest } from "@jest/globals";
2
+ import { resolveChildIds } from "./resolveChildIds.js";
3
+
4
+ describe("resolveChildIds", () => {
5
+ it("returns fixed string array as-is", () => {
6
+ const children = ["child1", "child2", "child3"];
7
+ expect(resolveChildIds(children, {})).toEqual([
8
+ "child1",
9
+ "child2",
10
+ "child3",
11
+ ]);
12
+ });
13
+
14
+ it("handles template specification object and generates dynamic IDs", () => {
15
+ const template = { path: "/items", componentId: "item" };
16
+ const dataModel = { items: ["apple", "banana", "cherry"] };
17
+ expect(resolveChildIds(template, dataModel)).toEqual([
18
+ "item_0",
19
+ "item_1",
20
+ "item_2",
21
+ ]);
22
+ });
23
+
24
+ it("handles array containing a single template object", () => {
25
+ const children = [{ path: "/users", componentId: "user_card" }] as any;
26
+ const dataModel = { users: [{ name: "Alice" }, { name: "Bob" }] };
27
+ expect(resolveChildIds(children, dataModel)).toEqual([
28
+ "user_card_0",
29
+ "user_card_1",
30
+ ]);
31
+ });
32
+
33
+ it("handles array containing nested component objects with IDs", () => {
34
+ const children = [
35
+ { id: "comp1", component: "Text" },
36
+ { id: "comp2", component: "Button" },
37
+ ] as any;
38
+ expect(resolveChildIds(children, {})).toEqual(["comp1", "comp2"]);
39
+ });
40
+
41
+ it("returns empty array for nested component objects without IDs", () => {
42
+ const children = [{ component: "Text" }] as any;
43
+ expect(resolveChildIds(children, {})).toEqual([]);
44
+ });
45
+
46
+ it("warns and returns empty array if template path does not resolve to an array", () => {
47
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
48
+ const template = { path: "/notAnArray", componentId: "item" };
49
+ const dataModel = { notAnArray: "string value" };
50
+
51
+ expect(resolveChildIds(template, dataModel)).toEqual([]);
52
+ expect(warnSpy).toHaveBeenCalledWith(
53
+ expect.stringContaining(
54
+ 'ChildList template path "/notAnArray" did not resolve to an array.',
55
+ ),
56
+ );
57
+ warnSpy.mockRestore();
58
+ });
59
+
60
+ it("handles empty array gracefully", () => {
61
+ expect(resolveChildIds([], {})).toEqual([]);
62
+ });
63
+ });
@@ -9,7 +9,19 @@ describe("resolveDynamicNumber", () => {
9
9
  expect(resolveDynamicNumber({ path: "/n" }, { n: 7 })).toBe(7);
10
10
  });
11
11
 
12
- it("returns 0 for null", () => {
12
+ it("resolves a $dataModel reference", () => {
13
+ expect(resolveDynamicNumber({ $dataModel: "/count" }, { count: 99 })).toBe(
14
+ 99,
15
+ );
16
+ });
17
+
18
+ it("returns 0 for null or undefined", () => {
13
19
  expect(resolveDynamicNumber(null, {})).toBe(0);
20
+ expect(resolveDynamicNumber(undefined, {})).toBe(0);
21
+ });
22
+
23
+ it("returns 0 for invalid objects without path or $dataModel", () => {
24
+ expect(resolveDynamicNumber({} as any, {})).toBe(0);
25
+ expect(resolveDynamicNumber("not-a-number" as any, {})).toBe(0);
14
26
  });
15
27
  });