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
@@ -1,15 +1,41 @@
1
1
  import { jest } from "@jest/globals";
2
2
 
3
- import {
3
+ const mockCreateConversation = jest.fn<any>().mockResolvedValue({
4
+ sendMessage: jest.fn<any>().mockResolvedValue(undefined),
5
+ sendMessageStreaming: jest.fn<any>().mockImplementation(async function* () {
6
+ yield {
7
+ content: [
8
+ {
9
+ text: JSON.stringify({
10
+ type: "response",
11
+ response: "Hello from LiteRT!",
12
+ }),
13
+ },
14
+ ],
15
+ };
16
+ }),
17
+ });
18
+
19
+ jest.unstable_mockModule("@litert-lm/core", () => ({
20
+ Engine: {
21
+ create: jest.fn<any>().mockResolvedValue({
22
+ createConversation: mockCreateConversation,
23
+ delete: jest.fn<any>().mockResolvedValue(undefined),
24
+ }),
25
+ },
26
+ }));
27
+
28
+ const {
4
29
  isLiteRtLmSupported,
5
30
  loadLiteRtModelStream,
6
31
  parseModelSpecificToolCall,
7
32
  parseLiteRtStructured,
8
- } from "./litert-lm-provider.js";
33
+ invokeWithLiteRtLm,
34
+ } = await import("./litert-lm-provider.js");
9
35
 
10
36
  // jsdom does not implement Blob fully, so we mock it for the tests
11
- if (typeof global !== "undefined") {
12
- (global as any).Blob = class MockBlob {
37
+ if (typeof globalThis !== "undefined") {
38
+ (globalThis as any).Blob = class MockBlob {
13
39
  private parts: any[];
14
40
  public size: number;
15
41
  constructor(parts: any[]) {
@@ -64,8 +90,6 @@ if (typeof global !== "undefined") {
64
90
  };
65
91
  }
66
92
 
67
- // TextEncoder / TextDecoder are not available in every Jest environment;
68
- // fall back to Node's Buffer.
69
93
  function encodeText(s: string): Uint8Array {
70
94
  if (typeof TextEncoder !== "undefined") {
71
95
  return new TextEncoder().encode(s);
@@ -82,10 +106,6 @@ function decodeBytes(b: Uint8Array): string {
82
106
  return Buffer.from(b).toString("utf8");
83
107
  }
84
108
 
85
- // ---------------------------------------------------------------------------
86
- // Helpers
87
- // ---------------------------------------------------------------------------
88
-
89
109
  function makeBodyStream(data: Uint8Array): ReadableStream<Uint8Array> {
90
110
  return new ReadableStream<Uint8Array>({
91
111
  start(controller) {
@@ -114,14 +134,9 @@ function makeFetchResponse(
114
134
  };
115
135
  }
116
136
 
117
- // ---------------------------------------------------------------------------
118
- // isLiteRtLmSupported
119
- // ---------------------------------------------------------------------------
120
-
121
137
  describe("LiteRT-LM Provider", () => {
122
138
  describe("isLiteRtLmSupported", () => {
123
139
  let originalNavigator: any;
124
-
125
140
  let originalWebAssembly: any;
126
141
 
127
142
  beforeEach(() => {
@@ -204,13 +219,8 @@ describe("LiteRT-LM Provider", () => {
204
219
  });
205
220
  });
206
221
 
207
- // ---------------------------------------------------------------------------
208
- // loadLiteRtModelStream — full integration of cache + network
209
- // ---------------------------------------------------------------------------
210
-
211
222
  describe("loadLiteRtModelStream", () => {
212
223
  let mockFetch: jest.Mock<any>;
213
- // cache storage: key -> { body: Uint8Array | null, headers: Headers }
214
224
  const cacheStore = new Map<
215
225
  string,
216
226
  { body: Uint8Array | null; headers: Headers }
@@ -222,9 +232,6 @@ describe("LiteRT-LM Provider", () => {
222
232
  }
223
233
 
224
234
  function makeCacheApi() {
225
- // A single cache object backed by `cacheStore`; shared across all
226
- // `caches.open()` calls so that writes from one call site are visible
227
- // to reads from another.
228
235
  const cacheObject = {
229
236
  put: jest.fn<any>(async (key: string, response: any) => {
230
237
  const headers = response.init
@@ -310,11 +317,8 @@ describe("LiteRT-LM Provider", () => {
310
317
  }
311
318
  });
312
319
 
313
- // -- Complete meta hit (after a previous successful download) ---------------
314
-
315
320
  it("returns stream from cached chunks when meta is complete (no network)", async () => {
316
321
  const chunk0 = new Uint8Array([1, 2, 3, 4]);
317
- // Populate the cache as if a previous download completed
318
322
  cacheStore.set("http://model?__sc_meta=1", {
319
323
  body: encodeText(
320
324
  JSON.stringify({
@@ -335,11 +339,9 @@ describe("LiteRT-LM Provider", () => {
335
339
  const onProgress = jest.fn<any>();
336
340
  const stream = await loadLiteRtModelStream("http://model", onProgress);
337
341
 
338
- // Should read from cache — no network calls
339
342
  expect(mockFetch).not.toHaveBeenCalled();
340
343
  expect(onProgress).toHaveBeenCalledWith(4, 4, true);
341
344
 
342
- // Stream should yield the cached bytes
343
345
  const reader = stream.getReader();
344
346
  const collected: number[] = [];
345
347
  while (true) {
@@ -354,10 +356,7 @@ describe("LiteRT-LM Provider", () => {
354
356
  expect(collected).toEqual([1, 2, 3, 4]);
355
357
  });
356
358
 
357
- // -- Fresh download (no meta) -----------------------------------------------
358
-
359
359
  it("downloads model and writes chunks + meta to CacheStorage", async () => {
360
- // 3 bytes fits in a single chunk (way below 16MB flush threshold)
361
360
  const modelData = new Uint8Array([10, 20, 30]);
362
361
  mockFetch.mockResolvedValueOnce(makeFetchResponse(200, modelData) as any);
363
362
 
@@ -372,25 +371,19 @@ describe("LiteRT-LM Provider", () => {
372
371
  if (done) break;
373
372
  }
374
373
 
375
- // At least one chunk entry should have been written
376
374
  const chunkEntry = cacheStore.get("http://model?__sc_chunk=0");
377
375
  expect(chunkEntry).toBeDefined();
378
376
 
379
- // Meta should exist and be marked complete
380
377
  const metaRaw = cacheStore.get("http://model?__sc_meta=1");
381
378
  expect(metaRaw).toBeDefined();
382
379
  const meta = JSON.parse(decodeBytes(metaRaw!.body!));
383
380
  expect(meta.complete).toBe(true);
384
381
  expect(meta.received).toBe(3);
385
382
 
386
- // Progress callback should have fired
387
383
  expect(onProgress).toHaveBeenCalled();
388
384
  });
389
385
 
390
- // -- Resume after crash -----------------------------------------------------
391
-
392
386
  it("resumes download from partial meta offset after crash", async () => {
393
- // Partial state: 4 bytes already cached in chunk 0, 4 more bytes remain
394
387
  const partialBytes = new Uint8Array([1, 2, 3, 4]);
395
388
  cacheStore.set("http://model?__sc_meta=1", {
396
389
  body: encodeText(
@@ -409,7 +402,6 @@ describe("LiteRT-LM Provider", () => {
409
402
  headers: new Headers({ "content-length": "4" }),
410
403
  });
411
404
 
412
- // Server returns the remaining 4 bytes via 206
413
405
  const remaining = new Uint8Array([5, 6, 7, 8]);
414
406
  mockFetch.mockResolvedValueOnce({
415
407
  status: 206,
@@ -425,7 +417,6 @@ describe("LiteRT-LM Provider", () => {
425
417
  const onProgress = jest.fn<any>();
426
418
  const stream = await loadLiteRtModelStream("http://model", onProgress);
427
419
 
428
- // Must have issued a Range request
429
420
  expect(mockFetch).toHaveBeenCalledTimes(1);
430
421
  const [, fetchInit] = mockFetch.mock.calls[0] as any[];
431
422
  expect(fetchInit.headers["Range"]).toBe("bytes=4-");
@@ -436,17 +427,13 @@ describe("LiteRT-LM Provider", () => {
436
427
  if (done) break;
437
428
  }
438
429
 
439
- // Meta should now be complete
440
430
  const metaRaw = cacheStore.get("http://model?__sc_meta=1");
441
431
  const meta = JSON.parse(decodeBytes(metaRaw!.body!));
442
432
  expect(meta.complete).toBe(true);
443
433
  expect(meta.received).toBe(8);
444
434
  });
445
435
 
446
- // -- Resume when server ignores Range (returns 200) -------------------------
447
-
448
436
  it("restarts from scratch when server ignores Range request (200 instead of 206)", async () => {
449
- // Partial state exists in cache
450
437
  cacheStore.set("http://model?__sc_meta=1", {
451
438
  body: encodeText(
452
439
  JSON.stringify({
@@ -464,7 +451,6 @@ describe("LiteRT-LM Provider", () => {
464
451
  headers: new Headers({ "content-length": "4" }),
465
452
  });
466
453
 
467
- // Server returns 200 (ignores Range) with full data
468
454
  const fullData = new Uint8Array([10, 20, 30, 40, 50, 60, 70, 80]);
469
455
  mockFetch.mockResolvedValueOnce(makeFetchResponse(200, fullData) as any);
470
456
 
@@ -479,15 +465,12 @@ describe("LiteRT-LM Provider", () => {
479
465
  if (done) break;
480
466
  }
481
467
 
482
- // Final meta should reflect the full 8 bytes, not 4+4
483
468
  const metaRaw = cacheStore.get("http://model?__sc_meta=1");
484
469
  const meta = JSON.parse(decodeBytes(metaRaw!.body!));
485
470
  expect(meta.complete).toBe(true);
486
471
  expect(meta.received).toBe(8);
487
472
  });
488
473
 
489
- // -- Retry on 500 -----------------------------------------------------------
490
-
491
474
  it("retries on 500 error", async () => {
492
475
  const modelData = new Uint8Array([1, 2, 3]);
493
476
  mockFetch
@@ -511,8 +494,6 @@ describe("LiteRT-LM Provider", () => {
511
494
  expect(meta.complete).toBe(true);
512
495
  });
513
496
 
514
- // -- Abort ------------------------------------------------------------------
515
-
516
497
  it("aborts download if signal is aborted", async () => {
517
498
  mockFetch.mockRejectedValue(
518
499
  new DOMException("Aborted", "AbortError") as any,
@@ -530,6 +511,188 @@ describe("LiteRT-LM Provider", () => {
530
511
  ).rejects.toThrow("Aborted");
531
512
  });
532
513
  });
514
+
515
+ describe("invokeWithLiteRtLm", () => {
516
+ it("emits error when LiteRT is unsupported in current environment", async () => {
517
+ const emit: any = jest.fn();
518
+ await invokeWithLiteRtLm(
519
+ {} as any,
520
+ "g1",
521
+ "system",
522
+ [{ role: "user", content: "hello" }],
523
+ 1000,
524
+ emit,
525
+ undefined,
526
+ "litert-community/gemma-4-E2B-it-litert-lm",
527
+ );
528
+
529
+ expect(emit).toHaveBeenCalledWith({
530
+ type: "response",
531
+ payload: {
532
+ groupId: "g1",
533
+ text: expect.stringContaining(
534
+ "LiteRT-LM requires WebGPU and WebAssembly.Suspending",
535
+ ),
536
+ },
537
+ });
538
+ });
539
+
540
+ it("handles model failure when supported but initialization fails", async () => {
541
+ Object.defineProperty(global, "navigator", {
542
+ value: { gpu: {} },
543
+ configurable: true,
544
+ writable: true,
545
+ });
546
+ Object.defineProperty(global, "WebAssembly", {
547
+ value: { Suspending: class {} },
548
+ configurable: true,
549
+ writable: true,
550
+ });
551
+
552
+ const emitted: any[] = [];
553
+ const emit = jest.fn((msg: any) => {
554
+ emitted.push(msg);
555
+ });
556
+
557
+ // Pass an unsupported model ID
558
+ await invokeWithLiteRtLm(
559
+ {} as any,
560
+ "g1",
561
+ "system",
562
+ [{ role: "user", content: "hello" }],
563
+ 1000,
564
+ emit,
565
+ undefined,
566
+ "unsupported-model-id",
567
+ );
568
+
569
+ expect(
570
+ emitted.some(
571
+ (m) =>
572
+ m.type === "model-download-progress" &&
573
+ m.payload.status === "error",
574
+ ),
575
+ ).toBe(true);
576
+ expect(
577
+ emitted.some(
578
+ (m) =>
579
+ m.type === "response" &&
580
+ m.payload.text.includes("LiteRT-LM failed to initialize"),
581
+ ),
582
+ ).toBe(true);
583
+ });
584
+
585
+ it("handles aborted signal gracefully during model download", async () => {
586
+ Object.defineProperty(global, "navigator", {
587
+ value: { gpu: {} },
588
+ configurable: true,
589
+ writable: true,
590
+ });
591
+ Object.defineProperty(global, "WebAssembly", {
592
+ value: { Suspending: class {} },
593
+ configurable: true,
594
+ writable: true,
595
+ });
596
+
597
+ const abortController = new AbortController();
598
+ abortController.abort();
599
+
600
+ const emitted: any[] = [];
601
+ const emit = jest.fn((msg: any) => {
602
+ emitted.push(msg);
603
+ });
604
+
605
+ await invokeWithLiteRtLm(
606
+ {} as any,
607
+ "g1",
608
+ "system",
609
+ [{ role: "user", content: "hello" }],
610
+ 1000,
611
+ emit,
612
+ abortController.signal,
613
+ "litert-community/gemma-4-E2B-it-litert-lm",
614
+ );
615
+
616
+ expect(
617
+ emitted.some(
618
+ (m) =>
619
+ m.type === "model-download-progress" &&
620
+ m.payload.status === "error",
621
+ ),
622
+ ).toBe(true);
623
+ });
624
+
625
+ it("emits warning response when LiteRT-LM is not supported in browser", async () => {
626
+ Object.defineProperty(global, "navigator", {
627
+ value: {},
628
+ configurable: true,
629
+ writable: true,
630
+ });
631
+
632
+ const emitted: any[] = [];
633
+ const emit = jest.fn((msg: any) => {
634
+ emitted.push(msg);
635
+ });
636
+
637
+ await invokeWithLiteRtLm(
638
+ {} as any,
639
+ "g1",
640
+ "system",
641
+ [{ role: "user", content: "hello" }],
642
+ 1000,
643
+ emit,
644
+ undefined,
645
+ "litert-community/gemma-4-E2B-it-litert-lm",
646
+ );
647
+
648
+ expect(
649
+ emitted.some(
650
+ (m) =>
651
+ m.type === "response" &&
652
+ m.payload.text.includes("LiteRT-LM requires WebGPU"),
653
+ ),
654
+ ).toBe(true);
655
+ });
656
+
657
+ it("successfully creates conversation and streams response when supported", async () => {
658
+ Object.defineProperty(global, "navigator", {
659
+ value: { gpu: {} },
660
+ configurable: true,
661
+ writable: true,
662
+ });
663
+ Object.defineProperty(global, "WebAssembly", {
664
+ value: { Suspending: class {} },
665
+ configurable: true,
666
+ writable: true,
667
+ });
668
+
669
+ global.fetch = jest.fn<any>().mockResolvedValue({
670
+ ok: true,
671
+ status: 200,
672
+ headers: new Headers({ "content-length": "100" }),
673
+ body: makeBodyStream(new Uint8Array(100)),
674
+ });
675
+
676
+ const emitted: any[] = [];
677
+ const emit = jest.fn((msg: any) => {
678
+ emitted.push(msg);
679
+ });
680
+
681
+ await invokeWithLiteRtLm(
682
+ {} as any,
683
+ "g1",
684
+ "system",
685
+ [{ role: "user", content: "hello" }],
686
+ 1000,
687
+ emit,
688
+ undefined,
689
+ "litert-community/gemma-4-E2B-it-litert-lm",
690
+ );
691
+
692
+ expect(emitted.some((m) => m.type === "streaming-chunk")).toBe(true);
693
+ expect(emitted.some((m) => m.type === "streaming-done")).toBe(true);
694
+ });
695
+ });
533
696
  });
534
697
 
535
698
  // ---------------------------------------------------------------------------
@@ -547,7 +710,6 @@ describe("parseModelSpecificToolCall", () => {
547
710
  });
548
711
 
549
712
  it('parses a Gemma 4 native tool call with encoded quote tokens <|"|">', () => {
550
- // Standard inline output
551
713
  const rawInline =
552
714
  '<|tool_call>call:web_search{queries:[<|"|>Blueberries latest details<|"|>]}<tool_call|>';
553
715
  const resultInline = parseModelSpecificToolCall(rawInline);
@@ -555,7 +717,6 @@ describe("parseModelSpecificToolCall", () => {
555
717
  expect(resultInline!.name).toBe("web_search");
556
718
  expect(resultInline!.input.queries[0]).toBe("Blueberries latest details");
557
719
 
558
- // Real-world variant: The model frequently injects a newline before the closing tag
559
720
  const rawWithNewline =
560
721
  '<|tool_call>call:web_search{queries:[<|"|>Blueberries latest details<|"|>]}\n<tool_call|>';
561
722
  const resultWithNewline = parseModelSpecificToolCall(rawWithNewline);
@@ -1,3 +1,4 @@
1
+ import { jest } from "@jest/globals";
1
2
  import { modelRegistry } from "./model-registry.js";
2
3
 
3
4
  describe("ModelRegistry", () => {
@@ -259,4 +260,118 @@ describe("ModelRegistry", () => {
259
260
  (globalThis as any).fetch = originalFetch;
260
261
  }
261
262
  });
263
+
264
+ it("should clear registered models", () => {
265
+ modelRegistry.registerModelInfo("model-to-clear", {
266
+ contextWindow: 4096,
267
+ maxOutput: 1024,
268
+ });
269
+ expect(modelRegistry.getModelInfo("model-to-clear")).not.toBeNull();
270
+ modelRegistry.clear();
271
+ expect(modelRegistry.getModelInfo("model-to-clear")).toBeNull();
272
+ });
273
+
274
+ it("should do nothing when provider has no modelsUrl", async () => {
275
+ await expect(
276
+ modelRegistry.fetchModelInfo({ id: "no-url" } as any),
277
+ ).resolves.toBeUndefined();
278
+ });
279
+
280
+ it("should handle apiKeyHeader, apiKeyHeaderFormat, headers, and extraHeaders", async () => {
281
+ const originalFetch = (globalThis as any).fetch;
282
+ let sentHeaders: any = null;
283
+
284
+ (globalThis as any).fetch = async (_url: string, opts: any) => {
285
+ sentHeaders = opts.headers;
286
+ return {
287
+ ok: true,
288
+ json: async () => ({
289
+ data: [{ id: "test-auth-model", context_length: 16000 }],
290
+ }),
291
+ };
292
+ };
293
+
294
+ try {
295
+ await modelRegistry.fetchModelInfo(
296
+ {
297
+ id: "hf-provider",
298
+ modelsUrl: "https://api.hf.co/models",
299
+ headers: { "X-Custom": "CustomVal" },
300
+ apiKeyHeader: "Authorization",
301
+ apiKeyHeaderFormat: "Bearer {key}",
302
+ } as any,
303
+ "secret-key",
304
+ { "X-Extra": "ExtraVal" },
305
+ );
306
+
307
+ expect(sentHeaders.get("Authorization")).toBe("Bearer secret-key");
308
+ expect(sentHeaders.get("X-Custom")).toBe("CustomVal");
309
+ expect(sentHeaders.get("X-Extra")).toBe("ExtraVal");
310
+ expect(modelRegistry.getModelInfo("test-auth-model")).toEqual({
311
+ contextWindow: 16000,
312
+ maxOutput: null,
313
+ });
314
+ } finally {
315
+ (globalThis as any).fetch = originalFetch;
316
+ }
317
+ });
318
+
319
+ it("should handle error when fetch response is not ok", async () => {
320
+ const originalFetch = (globalThis as any).fetch;
321
+ const consoleSpy = jest
322
+ .spyOn(console, "error")
323
+ .mockImplementation(() => {});
324
+ (globalThis as any).fetch = async () => ({
325
+ ok: false,
326
+ status: 403,
327
+ statusText: "Forbidden",
328
+ });
329
+
330
+ try {
331
+ await modelRegistry.fetchModelInfo({
332
+ id: "fail-provider",
333
+ modelsUrl: "https://fail.com/models",
334
+ } as any);
335
+
336
+ expect(consoleSpy).toHaveBeenCalledWith(
337
+ "[ModelRegistry] Error fetching models for fail-provider:",
338
+ expect.any(Error),
339
+ );
340
+ expect(modelRegistry.loading).toBe(false);
341
+ } finally {
342
+ (globalThis as any).fetch = originalFetch;
343
+ consoleSpy.mockRestore();
344
+ }
345
+ });
346
+
347
+ it("should parse text->image architecture modality string", async () => {
348
+ const originalFetch = (globalThis as any).fetch;
349
+ (globalThis as any).fetch = async () => ({
350
+ ok: true,
351
+ json: async () => ({
352
+ data: [
353
+ {
354
+ id: "vision-model",
355
+ context_length: 32000,
356
+ architecture: {
357
+ modality: "text+image->text",
358
+ },
359
+ },
360
+ ],
361
+ }),
362
+ });
363
+
364
+ try {
365
+ await modelRegistry.fetchModelInfo({
366
+ id: "vision-provider",
367
+ modelsUrl: "https://vision.com/models",
368
+ } as any);
369
+
370
+ expect(
371
+ modelRegistry.getModelInfo("vision-model")?.supportsImageInput,
372
+ ).toBe(true);
373
+ } finally {
374
+ (globalThis as any).fetch = originalFetch;
375
+ }
376
+ });
262
377
  });
@@ -5,6 +5,7 @@ import {
5
5
  formatRequest,
6
6
  getAdapter,
7
7
  getContextLimit,
8
+ normalizeMeshLlmResult,
8
9
  parseResponse,
9
10
  } from "./providers.js";
10
11
 
@@ -64,10 +65,47 @@ describe("providers.js", () => {
64
65
  it("should return correct limit for Claude 3 models", () => {
65
66
  expect(getContextLimit("claude-3-opus")).toBe(200000);
66
67
  expect(getContextLimit("claude-3-haiku")).toBe(200000);
68
+ expect(getContextLimit("claude-opus-4")).toBe(1000000);
67
69
  });
68
70
 
69
- it("should return correct limit for GPT-4", () => {
71
+ it("should return correct limit for GPT-4 and reasoning models", () => {
70
72
  expect(getContextLimit("gpt-4")).toBe(8192);
73
+ expect(getContextLimit("o1-preview")).toBe(200000);
74
+ expect(getContextLimit("o3-mini")).toBe(200000);
75
+ });
76
+
77
+ it("should return correct limit for Gemini models", () => {
78
+ expect(getContextLimit("gemini-1.5-pro")).toBe(2097152);
79
+ expect(getContextLimit("gemini-2.0-flash")).toBe(1048576);
80
+ expect(getContextLimit("gemini-1.0-pro")).toBe(32768);
81
+ expect(getContextLimit("gemini-nano")).toBe(4096);
82
+ });
83
+
84
+ it("should return correct limit for Llama, Gemma, and Mistral models", () => {
85
+ expect(getContextLimit("llama-3.1-70b")).toBe(128000);
86
+ expect(getContextLimit("llama-3-8b")).toBe(8192);
87
+ expect(getContextLimit("gemma-4-12b")).toBe(256000);
88
+ expect(getContextLimit("gemma-4-e2b")).toBe(128000);
89
+ expect(getContextLimit("gemma-3-12b")).toBe(128000);
90
+ expect(getContextLimit("gemma-3-1b")).toBe(32000);
91
+ expect(getContextLimit("gemma-2-9b")).toBe(8192);
92
+ expect(getContextLimit("mistral-large")).toBe(128000);
93
+ expect(getContextLimit("mistral-small")).toBe(32000);
94
+ });
95
+
96
+ it("should return correct limit for Bedrock, Phi, Qwen, and Mesh models", () => {
97
+ expect(getContextLimit("amazon.nova-micro")).toBe(128000);
98
+ expect(getContextLimit("amazon.nova-pro")).toBe(300000);
99
+ expect(getContextLimit("amazon.titan-text")).toBe(8192);
100
+ expect(getContextLimit("cohere.command-r")).toBe(128000);
101
+ expect(getContextLimit("phi-4-mini")).toBe(128000);
102
+ expect(getContextLimit("phi-4")).toBe(16384);
103
+ expect(getContextLimit("phi-3.5")).toBe(128000);
104
+ expect(getContextLimit("phi-3")).toBe(4096);
105
+ expect(getContextLimit("qwen3.5-9b")).toBe(262144);
106
+ expect(getContextLimit("qwen3-0.6b")).toBe(40960);
107
+ expect(getContextLimit("qwen3-8b")).toBe(128000);
108
+ expect(getContextLimit("mesh")).toBe(242144);
71
109
  });
72
110
 
73
111
  it("should return default limit for unknown models", () => {
@@ -1157,4 +1195,59 @@ describe("providers.js", () => {
1157
1195
  expect(headers["X-Token"]).toBe("Token abc123");
1158
1196
  });
1159
1197
  });
1198
+
1199
+ describe("normalizeMeshLlmResult & MeshLlmAdapter", () => {
1200
+ it("returns unchanged for empty or invalid results", () => {
1201
+ expect(normalizeMeshLlmResult(null)).toBeNull();
1202
+ expect(normalizeMeshLlmResult(undefined)).toBeUndefined();
1203
+ expect(normalizeMeshLlmResult({ content: "not-an-array" })).toEqual({
1204
+ content: "not-an-array",
1205
+ });
1206
+ });
1207
+
1208
+ it("normalizes mesh-llm result stripping thought tags and parsing tool calls", () => {
1209
+ const result = {
1210
+ content: [
1211
+ {
1212
+ type: "text",
1213
+ text: '<|channel>Thinking deeply...<channel|>Hello user!<|tool_call>call:search{"q":"test"}<tool_call|>',
1214
+ },
1215
+ { type: "image", data: "123" },
1216
+ ],
1217
+ stop_reason: "end_turn",
1218
+ };
1219
+
1220
+ const normalized = normalizeMeshLlmResult(result);
1221
+ expect(normalized.content).toEqual([
1222
+ { type: "text", text: "Hello user!" },
1223
+ expect.objectContaining({
1224
+ type: "tool_use",
1225
+ name: "search",
1226
+ input: { q: "test" },
1227
+ }),
1228
+ { type: "image", data: "123" },
1229
+ ]);
1230
+ expect(normalized.stop_reason).toBe("tool_use");
1231
+ });
1232
+
1233
+ it("returns MeshLlmAdapter for mesh-llm provider format", () => {
1234
+ const provider: any = { format: "mesh-llm" };
1235
+ const adapter = getAdapter(provider);
1236
+ expect(adapter.constructor.name).toBe("MeshLlmAdapter");
1237
+
1238
+ const response = {
1239
+ choices: [
1240
+ {
1241
+ message: {
1242
+ content: "<|channel>reasoning<channel|>Result text",
1243
+ },
1244
+ finish_reason: "stop",
1245
+ },
1246
+ ],
1247
+ };
1248
+
1249
+ const parsed = parseResponse(provider, response);
1250
+ expect(parsed.content).toEqual([{ type: "text", text: "Result text" }]);
1251
+ });
1252
+ });
1160
1253
  });