shadow-claw 1.23.4 → 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 (255) hide show
  1. package/README.md +42 -5
  2. package/bin/cli.mjs +166 -0
  3. package/bin/cli.test.mjs +60 -0
  4. package/bin/commands/backup.mjs +70 -0
  5. package/bin/commands/clients.mjs +41 -0
  6. package/bin/commands/peer-id.mjs +76 -0
  7. package/bin/commands/peer-id.test.mjs +104 -0
  8. package/bin/commands/send.mjs +51 -0
  9. package/bin/commands/tasks.mjs +59 -0
  10. package/bin/commands/webrtc-listen.mjs +254 -0
  11. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  12. package/bin/rolldown.config.test.mjs +401 -8
  13. package/bin/utils/control-client.mjs +243 -0
  14. package/bin/utils/control-client.test.mjs +83 -0
  15. package/bin/utils/ulid.mjs +45 -0
  16. package/bin/utils/webrtc-control-client.mjs +699 -0
  17. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  18. package/dist/electron/main.cjs +1 -1
  19. package/dist/public/AGENTS.md +5 -3
  20. package/dist/public/README.md +42 -5
  21. package/dist/public/agent.worker.js +1 -1
  22. package/dist/public/backup-controller-C_s9in2t.js +1 -0
  23. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  24. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  25. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  26. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  27. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  28. package/dist/public/config-CrHchneq.js +1 -0
  29. package/dist/public/{configurePeerJs-B4Ym9VMj.js → configurePeerJs-CxpZeDJJ.js} +1 -1
  30. package/dist/public/{connections-BEn6JYdu.js → connections-DvbgdkaB.js} +1 -1
  31. package/dist/public/{custom-element-security-DdYlUozq.js → custom-element-security-NCqQ5lmC.js} +1 -1
  32. package/dist/public/docs/README.md +3 -1
  33. package/dist/public/docs/architecture/overview.md +12 -12
  34. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  35. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +2 -2
  36. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  37. package/dist/public/docs/example/article/index.html +1 -1
  38. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  39. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  40. package/dist/public/docs/publishing/index.html +1 -1
  41. package/dist/public/docs/skill-creator/index.html +1 -1
  42. package/dist/public/docs/subsystems/backup.md +56 -0
  43. package/dist/public/docs/subsystems/cli.md +124 -0
  44. package/dist/public/docs/subsystems/control-plane.md +158 -0
  45. package/dist/public/docs/subsystems/notifications.md +54 -2
  46. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  47. package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
  48. package/dist/public/e2e/README.md +5 -1
  49. package/dist/public/{e2e-bridge-BQ4KzUt5.js → e2e-bridge-IGWsRl4i.js} +1 -1
  50. package/dist/public/file-viewer-D0eflgt6.js +1 -0
  51. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  52. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  53. package/dist/public/{git-DEUvQTUA.js → git-7Vq_SJSp.js} +2 -2
  54. package/dist/public/{iframe-sanitizer-0NS4XLOd.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  55. package/dist/public/index.html +1 -1
  56. package/dist/public/index.js +1 -1
  57. package/dist/public/{initChatSplitResize-llGkSJXg.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  58. package/dist/public/initControlPlane-C9jpnWev.js +1 -0
  59. package/dist/public/llms.txt +1 -1
  60. package/dist/public/main/index.html +1 -1
  61. package/dist/public/main/memory/index.html +1 -1
  62. package/dist/public/{markdown-CyYR4App.js → markdown-BnpKxSxl.js} +1 -1
  63. package/dist/public/{mcp-reconnect-CSx4aMwy.js → mcp-reconnect-9emsqGRe.js} +1 -1
  64. package/dist/public/{memoryStorage-Dwg8NhAZ.js → memoryStorage-C0KvLNUp.js} +1 -1
  65. package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-3h9xxHUm.js} +676 -676
  66. package/dist/public/orchestrator-nJj59RqD.js +15 -0
  67. package/dist/public/{peerjs-CurBuKvk.js → peerjs-BP4laMXj.js} +1 -1
  68. package/dist/public/{push-client-Dii-vPEh.js → push-client-CrjEXK3U.js} +1 -1
  69. package/dist/public/service-worker/push-handler.js +1 -1
  70. package/dist/public/service-worker.js +1 -1
  71. package/dist/public/{shadow-claw-5IHaDDMJ.js → shadow-claw-CbwETBWf.js} +2 -2
  72. package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
  73. package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
  74. package/dist/public/{shadow-claw-chat-CCLuhWdI.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
  75. package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
  76. package/dist/public/{shadow-claw-conversations-DV-87UUh.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
  77. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  78. package/dist/public/{shadow-claw-file-viewer-Dk2dbAUd.js → shadow-claw-file-viewer-Blf6yGVD.js} +1 -1
  79. package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
  80. package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-v8S0JjuR.js} +2 -2
  81. package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  82. package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
  83. package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  84. package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-qHbP95z9.js} +1 -1
  85. package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
  86. package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  87. package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-DFZHhzFZ.js} +1 -1
  88. package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
  89. package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-BFynbP0j.js} +2 -2
  90. package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
  91. package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Caxc9atL.js} +1 -1
  92. package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
  93. package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-BfJx3A9W.js} +1 -1
  94. package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
  95. package/dist/public/{syncWebMcpRegistration-BqJxaMGR.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
  96. package/dist/server.js +286 -189
  97. package/package.json +2 -1
  98. package/src/__mocks__/jszip.cjs +15 -5
  99. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  100. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  101. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  102. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  103. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  104. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  105. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  106. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  107. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  108. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  109. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  110. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  111. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  112. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +76 -0
  113. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  114. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  115. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  116. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  117. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  118. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  119. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  120. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  121. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  122. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  123. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  124. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  125. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  126. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +273 -0
  127. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
  128. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  129. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  130. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  131. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  132. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  133. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  134. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  135. package/src/config/config.ts +10 -0
  136. package/src/core/backup-controller.test.ts +101 -0
  137. package/src/core/backup-controller.ts +137 -0
  138. package/src/core/control-plane-client.test.ts +408 -0
  139. package/src/core/control-plane-client.ts +425 -0
  140. package/src/core/orchestrator/orchestrator.ts +4 -0
  141. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  142. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  143. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  144. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  145. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  146. package/src/core/utils/initControlPlane.test.ts +276 -0
  147. package/src/core/utils/initControlPlane.ts +437 -0
  148. package/src/core/utils/initializeApp.ts +18 -0
  149. package/src/db/getSession.test.ts +13 -1
  150. package/src/server/app.test.ts +12 -0
  151. package/src/server/app.ts +18 -8
  152. package/src/server/client-registry.test.ts +250 -0
  153. package/src/server/client-registry.ts +477 -0
  154. package/src/server/config.ts +15 -0
  155. package/src/server/control-plane-types.ts +120 -0
  156. package/src/server/control-plane.test.ts +472 -0
  157. package/src/server/control-plane.ts +581 -0
  158. package/src/server/routes/backup.test.ts +263 -0
  159. package/src/server/routes/backup.ts +262 -0
  160. package/src/server/routes/integrations-email.test.ts +532 -0
  161. package/src/server/routes/llamafile.test.ts +172 -48
  162. package/src/server/routes/mesh-llm.test.ts +192 -0
  163. package/src/server/routes/ollama.test.ts +49 -0
  164. package/src/server/server-peer.test.ts +86 -0
  165. package/src/server/server-peer.ts +330 -0
  166. package/src/server/server.ts +35 -0
  167. package/src/server/services/llamafile-manager.test.ts +230 -6
  168. package/src/server/services/transformers-runtime.test.ts +186 -18
  169. package/src/server/webrtc.test.ts +21 -0
  170. package/src/server/webrtc.ts +72 -0
  171. package/src/service-worker/fetch-proxy.test.ts +64 -1
  172. package/src/service-worker/push-handler.ts +39 -0
  173. package/src/shell/vm.test.ts +744 -12
  174. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  175. package/src/storage/getGroupFile.test.ts +66 -0
  176. package/src/storage/memoryStorage.test.ts +82 -2
  177. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  178. package/src/stores/chat-ui.test.ts +20 -0
  179. package/src/stores/orchestrator.test.ts +134 -0
  180. package/src/stores/toast.test.ts +50 -0
  181. package/src/stores/tools.test.ts +147 -10
  182. package/src/subsystems/channels/imessage.test.ts +59 -0
  183. package/src/subsystems/channels/peerjs.test.ts +500 -62
  184. package/src/subsystems/channels/peerjs.ts +71 -4
  185. package/src/subsystems/channels/telegram.test.ts +177 -0
  186. package/src/subsystems/email/catalog.test.ts +1 -0
  187. package/src/subsystems/email/connections.test.ts +39 -1
  188. package/src/subsystems/git/credentials.test.ts +160 -825
  189. package/src/subsystems/git/git.test.ts +463 -0
  190. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  191. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  192. package/src/subsystems/notifications/push-routes.ts +23 -0
  193. package/src/subsystems/notifications/push-store.ts +1 -1
  194. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  195. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  196. package/src/subsystems/providers/model-registry.test.ts +115 -0
  197. package/src/subsystems/providers/providers.test.ts +94 -1
  198. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  199. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  200. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  201. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  202. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  203. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  204. package/src/subsystems/tools/cron.test.ts +64 -0
  205. package/src/subsystems/tools/declarative.test.ts +255 -89
  206. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  207. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  208. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  209. package/src/testing/jest-setup.ts +328 -40
  210. package/src/testing/mock-v86.mjs +49 -0
  211. package/src/ui/a2ui/types.ts +4 -3
  212. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  213. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  214. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  215. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  216. package/src/worker/tools/email/email.test.ts +193 -8
  217. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  218. package/src/worker/tools/git/git.test.ts +214 -3
  219. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  220. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  221. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  222. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  223. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  224. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  225. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  226. package/src/worker/tools/ui/javascript.test.ts +116 -0
  227. package/src/worker/tools/ui/list-components.test.ts +10 -0
  228. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  229. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  230. package/src/worker/tools/ui/render-component.test.ts +346 -0
  231. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  232. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  233. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  234. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  235. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  236. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  237. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  238. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  239. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  240. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  241. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  242. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  243. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  244. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  245. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  246. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  247. package/src/worker/utils/rate-limit.test.ts +199 -0
  248. package/src/worker/utils/tool-state.test.ts +29 -0
  249. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  250. package/dist/public/config-BYAuxx3j.js +0 -1
  251. package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
  252. package/dist/public/file-viewer-CcZYw-PS.js +0 -1
  253. package/dist/public/orchestrator-o9Fj67BN.js +0 -15
  254. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  255. package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
@@ -10,20 +10,18 @@ describe("llamafile-routes", () => {
10
10
  statusCode: 200,
11
11
  body: undefined,
12
12
  headers: {},
13
+ headersSent: false,
13
14
  status: jest.fn().mockImplementation((code: any) => {
14
15
  res.statusCode = code;
15
-
16
16
  return res;
17
17
  }),
18
18
  json: jest.fn().mockImplementation((payload: any) => {
19
19
  res.body = payload;
20
-
21
20
  return res;
22
21
  }),
23
22
  write: jest.fn(),
24
23
  end: jest.fn(),
25
24
  };
26
-
27
25
  return res;
28
26
  }
29
27
 
@@ -55,71 +53,197 @@ describe("llamafile-routes", () => {
55
53
  }),
56
54
  };
57
55
 
58
- registerLlamafileRoutes(app as any, service, { verbose: false });
56
+ registerLlamafileRoutes(app as any, service, { verbose: true });
59
57
  });
60
58
 
61
- it("lists local llamafile binaries", async () => {
62
- const handler = routes.get("GET /llamafile-proxy/models");
63
- service.listBinaries.mockResolvedValue([
64
- { id: "model.llamafile", fileName: "model.llamafile" },
65
- ]);
59
+ describe("GET /llamafile-proxy/models", () => {
60
+ it("lists local llamafile binaries", async () => {
61
+ const handler = routes.get("GET /llamafile-proxy/models");
62
+ service.listBinaries.mockResolvedValue([
63
+ { id: "model.llamafile", fileName: "model.llamafile" },
64
+ ]);
65
+
66
+ const res = createResponse();
67
+ await handler({}, res);
68
+
69
+ expect(res.json).toHaveBeenCalledWith({
70
+ data: [expect.objectContaining({ id: "model.llamafile" })],
71
+ });
72
+ });
73
+
74
+ it("handles failure listing binaries with 502 status", async () => {
75
+ const handler = routes.get("GET /llamafile-proxy/models");
76
+ service.listBinaries.mockRejectedValue(new Error("Disk access denied"));
66
77
 
67
- const res = createResponse();
68
- await handler({}, res);
78
+ const errSpy = jest.spyOn(console, "error").mockImplementation(() => {});
79
+ const res = createResponse();
80
+ await handler({}, res);
69
81
 
70
- expect(res.json).toHaveBeenCalledWith({
71
- data: [expect.objectContaining({ id: "model.llamafile" })],
82
+ expect(res.status).toHaveBeenCalledWith(502);
83
+ expect(res.body.error).toContain(
84
+ "Failed to list llamafile binaries: Disk access denied",
85
+ );
86
+ errSpy.mockRestore();
72
87
  });
73
88
  });
74
89
 
75
- it("cancels an active llamafile request", async () => {
76
- const handler = routes.get("POST /llamafile-proxy/cancel");
77
- service.getLlamafileRequestId.mockReturnValue("req-123");
78
- service.cancelRequest.mockReturnValue(true);
90
+ describe("POST /llamafile-proxy/cancel", () => {
91
+ it("returns 400 when request id is missing", async () => {
92
+ const handler = routes.get("POST /llamafile-proxy/cancel");
93
+ service.getLlamafileRequestId.mockReturnValue(undefined);
79
94
 
80
- const req = { body: { id: "req-123" } };
81
- const res = createResponse();
95
+ const res = createResponse();
96
+ await handler({ body: {} }, res);
82
97
 
83
- await handler(req, res);
98
+ expect(res.status).toHaveBeenCalledWith(400);
99
+ expect(res.body.error).toContain("Missing llamafile request id");
100
+ });
84
101
 
85
- expect(res.status).toHaveBeenCalledWith(202);
86
- expect(res.json).toHaveBeenCalledWith({ ok: true, cancelled: true });
87
- });
102
+ it("returns 200 when request was not found or already completed", async () => {
103
+ const handler = routes.get("POST /llamafile-proxy/cancel");
104
+ service.getLlamafileRequestId.mockReturnValue("req-123");
105
+ service.cancelRequest.mockReturnValue(false);
106
+
107
+ const res = createResponse();
108
+ await handler({ body: { id: "req-123" } }, res);
88
109
 
89
- it("invokes llamafile in CLI mode", async () => {
90
- const handler = routes.get("POST /llamafile-proxy/chat/completions");
91
- service.getLlamafileRuntimeOptions.mockReturnValue({
92
- mode: "cli",
93
- offline: true,
110
+ expect(res.status).toHaveBeenCalledWith(200);
111
+ expect(res.body).toEqual({ ok: true, cancelled: false });
94
112
  });
95
113
 
96
- const req = { body: { model: "model.llamafile" }, headers: {} };
97
- const res = createResponse();
114
+ it("cancels an active llamafile request", async () => {
115
+ const handler = routes.get("POST /llamafile-proxy/cancel");
116
+ service.getLlamafileRequestId.mockReturnValue("req-123");
117
+ service.cancelRequest.mockReturnValue(true);
98
118
 
99
- await handler(req, res);
119
+ const req = { body: { id: "req-123" } };
120
+ const res = createResponse();
100
121
 
101
- expect(service.invokeCli).toHaveBeenCalled();
122
+ await handler(req, res);
123
+
124
+ expect(res.status).toHaveBeenCalledWith(202);
125
+ expect(res.json).toHaveBeenCalledWith({ ok: true, cancelled: true });
126
+ });
102
127
  });
103
128
 
104
- it("invokes llamafile in SERVER mode", async () => {
105
- const handler = routes.get("POST /llamafile-proxy/chat/completions");
106
- service.getLlamafileRuntimeOptions.mockReturnValue({
107
- mode: "server",
108
- host: "127.0.0.1",
109
- port: 8080,
129
+ describe("POST /llamafile-proxy/chat/completions", () => {
130
+ it("returns 400 when body is missing or not an object", async () => {
131
+ const handler = routes.get("POST /llamafile-proxy/chat/completions");
132
+ const res = createResponse();
133
+ await handler({ body: null, headers: {} }, res);
134
+
135
+ expect(res.status).toHaveBeenCalledWith(400);
136
+ expect(res.body.error).toBe("Missing request body");
110
137
  });
111
138
 
112
- const req = { body: { model: "model.llamafile" }, headers: {} };
113
- const res = createResponse();
139
+ it("returns 400 in CLI mode when model is missing", async () => {
140
+ const handler = routes.get("POST /llamafile-proxy/chat/completions");
141
+ service.getLlamafileRuntimeOptions.mockReturnValue({
142
+ mode: "cli",
143
+ offline: true,
144
+ });
114
145
 
115
- await handler(req, res);
146
+ const res = createResponse();
147
+ await handler({ body: { model: " " }, headers: {} }, res);
116
148
 
117
- expect(invokeLlamafileServerMock).toHaveBeenCalledWith(
118
- req,
119
- res,
120
- req.body,
121
- expect.objectContaining({ host: "127.0.0.1", port: 8080 }),
122
- false,
123
- );
149
+ expect(res.status).toHaveBeenCalledWith(400);
150
+ expect(res.body.error).toContain(
151
+ "Missing or invalid 'model' parameter for CLI mode",
152
+ );
153
+ });
154
+
155
+ it("invokes llamafile in CLI mode", async () => {
156
+ const handler = routes.get("POST /llamafile-proxy/chat/completions");
157
+ service.getLlamafileRuntimeOptions.mockReturnValue({
158
+ mode: "cli",
159
+ offline: true,
160
+ });
161
+
162
+ const req = {
163
+ body: { model: "model.llamafile", llamafile: { mode: "cli" } },
164
+ headers: { "x-llamafile-mode": "cli" },
165
+ };
166
+ const res = createResponse();
167
+
168
+ await handler(req, res);
169
+
170
+ expect(service.invokeCli).toHaveBeenCalledWith(
171
+ req,
172
+ res,
173
+ req.body,
174
+ { model: "model.llamafile", offline: true },
175
+ true,
176
+ );
177
+ });
178
+
179
+ it("invokes llamafile in SERVER mode", async () => {
180
+ const handler = routes.get("POST /llamafile-proxy/chat/completions");
181
+ service.getLlamafileRuntimeOptions.mockReturnValue({
182
+ mode: "server",
183
+ host: "127.0.0.1",
184
+ port: 8080,
185
+ });
186
+
187
+ const req = { body: { model: "model.llamafile" }, headers: {} };
188
+ const res = createResponse();
189
+
190
+ await handler(req, res);
191
+
192
+ expect(invokeLlamafileServerMock).toHaveBeenCalledWith(
193
+ req,
194
+ res,
195
+ req.body,
196
+ expect.objectContaining({ host: "127.0.0.1", port: 8080 }),
197
+ true,
198
+ );
199
+ });
200
+
201
+ it("handles invocation failure before headers sent with 502", async () => {
202
+ const handler = routes.get("POST /llamafile-proxy/chat/completions");
203
+ service.getLlamafileRuntimeOptions.mockReturnValue({
204
+ mode: "cli",
205
+ });
206
+ service.invokeCli.mockRejectedValue(new Error("Process crashed"));
207
+
208
+ const errSpy = jest.spyOn(console, "error").mockImplementation(() => {});
209
+ const req = { body: { model: "model.llamafile" }, headers: {} };
210
+ const res = createResponse();
211
+
212
+ await handler(req, res);
213
+
214
+ expect(res.status).toHaveBeenCalledWith(502);
215
+ expect(res.body.error).toContain(
216
+ "Llamafile invocation failed: Process crashed",
217
+ );
218
+ errSpy.mockRestore();
219
+ });
220
+
221
+ it("handles invocation failure after headers sent by writing SSE error event", async () => {
222
+ const handler = routes.get("POST /llamafile-proxy/chat/completions");
223
+ service.getLlamafileRuntimeOptions.mockReturnValue({
224
+ mode: "server",
225
+ host: "127.0.0.1",
226
+ port: 8080,
227
+ });
228
+ invokeLlamafileServerMock.mockImplementation(
229
+ async (_req: any, res: any) => {
230
+ res.headersSent = true;
231
+ throw new Error("Stream broken midway");
232
+ },
233
+ );
234
+
235
+ const errSpy = jest.spyOn(console, "error").mockImplementation(() => {});
236
+ const req = { body: { model: "model.llamafile" }, headers: {} };
237
+ const res = createResponse();
238
+
239
+ await handler(req, res);
240
+
241
+ expect(res.write).toHaveBeenCalledWith(
242
+ expect.stringContaining('"message":"Stream broken midway"'),
243
+ );
244
+ expect(res.write).toHaveBeenCalledWith("data: [DONE]\n\n");
245
+ expect(res.end).toHaveBeenCalled();
246
+ errSpy.mockRestore();
247
+ });
124
248
  });
125
249
  });
@@ -0,0 +1,192 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ describe("mesh-llm routes", () => {
4
+ let routes: Map<string, any>;
5
+ let mockHandleProxyRequest: jest.Mock<any>;
6
+ let mockHandleStreamingProxyRequest: jest.Mock<any>;
7
+
8
+ function createResponse() {
9
+ const res: any = {
10
+ statusCode: 200,
11
+ body: undefined,
12
+ headers: {},
13
+ status: jest.fn().mockImplementation((code: any) => {
14
+ res.statusCode = code;
15
+ return res;
16
+ }),
17
+ json: jest.fn().mockImplementation((payload: any) => {
18
+ res.body = payload;
19
+ return res;
20
+ }),
21
+ };
22
+ return res;
23
+ }
24
+
25
+ beforeEach(async () => {
26
+ jest.resetModules();
27
+ routes = new Map();
28
+
29
+ mockHandleProxyRequest = jest.fn().mockResolvedValue(undefined as never);
30
+ mockHandleStreamingProxyRequest = jest
31
+ .fn()
32
+ .mockResolvedValue(undefined as never);
33
+
34
+ jest.unstable_mockModule("../utils/proxy-helpers.js", () => ({
35
+ handleProxyRequest: mockHandleProxyRequest,
36
+ handleStreamingProxyRequest: mockHandleStreamingProxyRequest,
37
+ }));
38
+
39
+ const { registerMeshLlmRoutes } = await import("./mesh-llm.js");
40
+
41
+ const app = {
42
+ get: jest.fn((path: string, handler: any) => {
43
+ routes.set(`GET ${path}`, handler);
44
+ }),
45
+ post: jest.fn((path: string, handler: any) => {
46
+ routes.set(`POST ${path}`, handler);
47
+ }),
48
+ };
49
+
50
+ registerMeshLlmRoutes(app as any, { verbose: true });
51
+ });
52
+
53
+ describe("POST /mesh-llm-proxy/chat/completions", () => {
54
+ const route = "POST /mesh-llm-proxy/chat/completions";
55
+
56
+ it("handles non-streaming chat completions proxying", async () => {
57
+ const handler = routes.get(route);
58
+ const req = {
59
+ headers: {
60
+ host: "localhost",
61
+ origin: "http://localhost",
62
+ referer: "http://localhost/chat",
63
+ authorization: "Bearer mesh-token",
64
+ "x-mesh-llm-host": "https://custom.mesh.io",
65
+ },
66
+ body: {
67
+ model: "mesh-70b",
68
+ messages: [{ role: "user", content: "hello" }],
69
+ stream: false,
70
+ },
71
+ };
72
+ const res = createResponse();
73
+
74
+ await handler(req, res);
75
+
76
+ expect(mockHandleProxyRequest).toHaveBeenCalledWith(
77
+ req,
78
+ res,
79
+ expect.objectContaining({
80
+ targetUrl: "https://custom.mesh.io/v1/chat/completions",
81
+ method: "POST",
82
+ headers: {
83
+ authorization: "Bearer mesh-token",
84
+ "x-mesh-llm-host": "https://custom.mesh.io",
85
+ },
86
+ body: JSON.stringify(req.body),
87
+ }),
88
+ );
89
+ });
90
+
91
+ it("handles streaming chat completions proxying with array header", async () => {
92
+ const handler = routes.get(route);
93
+ const req = {
94
+ headers: {
95
+ host: "localhost",
96
+ authorization: "Bearer mesh-token",
97
+ "x-mesh-llm-host": ["https://node1.mesh.io", "https://node2.mesh.io"],
98
+ },
99
+ body: {
100
+ model: "mesh-70b",
101
+ messages: [{ role: "user", content: "stream hello" }],
102
+ stream: true,
103
+ },
104
+ };
105
+ const res = createResponse();
106
+
107
+ await handler(req, res);
108
+
109
+ expect(mockHandleStreamingProxyRequest).toHaveBeenCalledWith(
110
+ req,
111
+ res,
112
+ expect.objectContaining({
113
+ targetUrl: "https://node1.mesh.io/v1/chat/completions",
114
+ headers: expect.not.objectContaining({ host: "localhost" }),
115
+ body: JSON.stringify(req.body),
116
+ }),
117
+ );
118
+ });
119
+ });
120
+
121
+ describe("GET /mesh-llm-proxy/models", () => {
122
+ const route = "GET /mesh-llm-proxy/models";
123
+
124
+ it("fetches and normalises models catalog", async () => {
125
+ const handler = routes.get(route);
126
+ const mockCatalog = {
127
+ data: [
128
+ {
129
+ id: "model-1",
130
+ metadata: {
131
+ context_length: 32768,
132
+ },
133
+ },
134
+ {
135
+ id: "model-2",
136
+ },
137
+ ],
138
+ };
139
+
140
+ (globalThis as any).fetch = (jest.fn() as any).mockResolvedValue({
141
+ ok: true,
142
+ json: async () => mockCatalog,
143
+ });
144
+
145
+ const req = { headers: {} };
146
+ const res = createResponse();
147
+
148
+ await handler(req, res);
149
+
150
+ expect(res.json).toHaveBeenCalledWith({
151
+ data: [
152
+ {
153
+ id: "model-1",
154
+ metadata: { context_length: 32768 },
155
+ context_length: 32768,
156
+ },
157
+ {
158
+ id: "model-2",
159
+ },
160
+ ],
161
+ });
162
+ });
163
+
164
+ it("falls back to default fallback model on fetch failure", async () => {
165
+ const handler = routes.get(route);
166
+ (globalThis as any).fetch = (jest.fn() as any).mockResolvedValue({
167
+ ok: false,
168
+ status: 500,
169
+ statusText: "Internal Server Error",
170
+ text: async () => "Upstream error",
171
+ });
172
+
173
+ const errSpy = jest.spyOn(console, "error").mockImplementation(() => {});
174
+ const req = { headers: {} };
175
+ const res = createResponse();
176
+
177
+ await handler(req, res);
178
+
179
+ expect(res.json).toHaveBeenCalledWith({
180
+ models: [
181
+ {
182
+ id: "mesh",
183
+ name: "mesh",
184
+ context_length: 8000,
185
+ max_completion_tokens: 4096,
186
+ },
187
+ ],
188
+ });
189
+ errSpy.mockRestore();
190
+ });
191
+ });
192
+ });
@@ -164,4 +164,53 @@ describe("ollama-routes", () => {
164
164
  expect(res.write).toHaveBeenCalledWith(expect.any(Buffer));
165
165
  expect(res.end).toHaveBeenCalled();
166
166
  });
167
+
168
+ it("handles non-streaming error responses and exceptions", async () => {
169
+ const handler = routes.get("POST /ollama-proxy/chat/completions");
170
+
171
+ // Upstream error response
172
+ globalFetchMock.mockResolvedValueOnce({
173
+ ok: false,
174
+ status: 400,
175
+ text: async () => "Bad request from Ollama",
176
+ });
177
+
178
+ const req1 = {
179
+ body: { model: "llama3", messages: [], stream: false },
180
+ headers: {},
181
+ };
182
+ const res1 = createResponse();
183
+ await handler(req1, res1);
184
+
185
+ expect(res1.status).toHaveBeenCalledWith(400);
186
+ expect(res1.json).toHaveBeenCalledWith({
187
+ error: "Bad request from Ollama",
188
+ });
189
+
190
+ // Exception throw
191
+ globalFetchMock.mockRejectedValueOnce(new Error("Connection refused"));
192
+
193
+ const req2 = {
194
+ body: { model: "llama3", messages: [], stream: false },
195
+ headers: {},
196
+ };
197
+ const res2 = createResponse();
198
+ await handler(req2, res2);
199
+
200
+ expect(res2.status).toHaveBeenCalledWith(502);
201
+ });
202
+
203
+ it("handles models route failure gracefully", async () => {
204
+ const handler = routes.get("GET /ollama-proxy/models");
205
+
206
+ globalFetchMock.mockResolvedValueOnce({
207
+ ok: false,
208
+ status: 500,
209
+ });
210
+
211
+ const res = createResponse();
212
+ await handler({}, res);
213
+
214
+ expect(res.status).toHaveBeenCalledWith(502);
215
+ });
167
216
  });
@@ -0,0 +1,86 @@
1
+ import { jest } from "@jest/globals";
2
+ import { ServerPeer } from "./server-peer.js";
3
+
4
+ describe("ServerPeer", () => {
5
+ it("initializes ServerPeer with options and handlers", () => {
6
+ const mockHandler = jest.fn();
7
+ const peer = new ServerPeer({
8
+ peerId: "test-server-peer",
9
+ host: "127.0.0.1",
10
+ port: 8888,
11
+ handlers: {
12
+ "custom-action": mockHandler,
13
+ },
14
+ });
15
+
16
+ expect(peer.peerId).toBe("test-server-peer");
17
+ expect(peer.isOpen).toBe(false);
18
+ expect(peer.getConnectedPeers()).toEqual([]);
19
+ });
20
+
21
+ it("handles incoming command:execute and command:result payloads", async () => {
22
+ const mockHandler = jest.fn(async (args: any) => ({
23
+ ok: true,
24
+ echo: args.text,
25
+ }));
26
+ const peer = new ServerPeer({
27
+ peerId: "test-server-peer",
28
+ handlers: {
29
+ "echo-test": mockHandler,
30
+ },
31
+ });
32
+
33
+ const sentMessages: any[] = [];
34
+ const mockConn = {
35
+ peer: "remote-client-peer",
36
+ send: (data: any) => sentMessages.push(data),
37
+ };
38
+
39
+ // Simulate incoming command:execute
40
+ await (peer as any)._handleData("remote-client-peer", mockConn, {
41
+ id: "req-1",
42
+ type: "command:execute",
43
+ payload: {
44
+ commandId: "cmd-1",
45
+ action: "echo-test",
46
+ args: { text: "hello peer" },
47
+ },
48
+ });
49
+
50
+ expect(mockHandler).toHaveBeenCalledWith({ text: "hello peer" });
51
+ expect(sentMessages).toHaveLength(1);
52
+ expect(sentMessages[0].type).toBe("command:result");
53
+ expect(sentMessages[0].payload.success).toBe(true);
54
+ expect(sentMessages[0].payload.data).toEqual({
55
+ ok: true,
56
+ echo: "hello peer",
57
+ });
58
+ });
59
+
60
+ it("handles unknown action error", async () => {
61
+ const peer = new ServerPeer({
62
+ peerId: "test-server-peer",
63
+ });
64
+
65
+ const sentMessages: any[] = [];
66
+ const mockConn = {
67
+ peer: "remote-client-peer",
68
+ send: (data: any) => sentMessages.push(data),
69
+ };
70
+
71
+ await (peer as any)._handleData("remote-client-peer", mockConn, {
72
+ id: "req-2",
73
+ type: "command:execute",
74
+ payload: {
75
+ commandId: "cmd-2",
76
+ action: "unregistered-action",
77
+ args: {},
78
+ },
79
+ });
80
+
81
+ expect(sentMessages).toHaveLength(1);
82
+ expect(sentMessages[0].type).toBe("command:result");
83
+ expect(sentMessages[0].payload.success).toBe(false);
84
+ expect(sentMessages[0].payload.error).toContain("Unknown action");
85
+ });
86
+ });