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
@@ -45,6 +45,8 @@ jest.unstable_mockModule("../../stores/orchestrator.js", () => ({
45
45
  toggleTask: jest.fn(),
46
46
  getTasksByGroupId: jest.fn(() => []),
47
47
  getTaskServerTerminalInstance: jest.fn(() => null),
48
+ reorderTasks: jest.fn(),
49
+ restoreTasksFromBackup: jest.fn(),
48
50
  },
49
51
  }));
50
52
 
@@ -261,5 +263,410 @@ describe("shadow-claw-tasks", () => {
261
263
  }),
262
264
  );
263
265
  });
266
+
267
+ it("manages tools editor UI interactions (remove, modify, suppress)", () => {
268
+ const el = new ShadowClawTasks();
269
+ document.body.appendChild(el);
270
+
271
+ el.editingTools = [
272
+ {
273
+ name: "test_tool",
274
+ input: { a: 1 },
275
+ suppressOutput: false,
276
+ suppressToast: false,
277
+ },
278
+ ];
279
+
280
+ el.renderToolsEditor();
281
+ const toolsList = el.shadowRoot?.querySelector(
282
+ ".tasks__tools-list",
283
+ ) as HTMLElement;
284
+ expect(toolsList.querySelectorAll(".tasks__tool-item").length).toBe(1);
285
+
286
+ // Modify tool name
287
+ const nameInput = toolsList.querySelector(
288
+ ".tasks__form-input",
289
+ ) as HTMLInputElement;
290
+ nameInput.value = "updated_tool";
291
+ nameInput.dispatchEvent(new Event("input"));
292
+ expect(el.editingTools[0].name).toBe("updated_tool");
293
+
294
+ // Modify textarea params
295
+ const textarea = toolsList.querySelector(
296
+ ".tasks__form-textarea",
297
+ ) as HTMLTextAreaElement;
298
+ textarea.value = '{"foo": "bar"}';
299
+ textarea.dispatchEvent(new Event("change"));
300
+ expect(el.editingTools[0].input).toEqual({ foo: "bar" });
301
+
302
+ // Toggle suppress
303
+ const suppressCheckbox = toolsList.querySelector(
304
+ ".tasks__tool-suppress input",
305
+ ) as HTMLInputElement;
306
+ suppressCheckbox.checked = true;
307
+ suppressCheckbox.dispatchEvent(new Event("change"));
308
+ expect(el.editingTools[0].suppressOutput).toBe(true);
309
+
310
+ // Remove tool
311
+ const removeBtn = toolsList.querySelector(
312
+ ".tasks__tool-remove-btn",
313
+ ) as HTMLButtonElement;
314
+ removeBtn.click();
315
+ expect(el.editingTools.length).toBe(0);
316
+
317
+ document.body.removeChild(el);
318
+ });
319
+
320
+ it("renders tasks and binds action buttons (toggle, copy, run, edit, delete)", async () => {
321
+ const el = new ShadowClawTasks();
322
+ const container = document.createElement("div");
323
+ container.className = "tasks__list";
324
+ const countEl = document.createElement("span");
325
+ countEl.className = "tasks__count";
326
+ el.shadowRoot?.replaceChildren(countEl, container);
327
+
328
+ const db = {} as any;
329
+ const tasks = [
330
+ {
331
+ id: "task-1",
332
+ name: "Daily Job",
333
+ schedule: "0 0 * * *",
334
+ prompt: "check status",
335
+ enabled: true,
336
+ type: "prompt",
337
+ freshContext: true,
338
+ },
339
+ ];
340
+ (orchestratorStore as any).tasks = tasks;
341
+
342
+ await el.updateTaskList(db);
343
+ expect(container.querySelectorAll(".tasks__item").length).toBe(1);
344
+
345
+ const item = container.querySelector(".tasks__item") as HTMLElement;
346
+ const toggleInput = item.querySelector(
347
+ ".tasks__toggle-input",
348
+ ) as HTMLInputElement;
349
+ toggleInput.checked = false;
350
+ toggleInput.dispatchEvent(new Event("change"));
351
+ expect(orchestratorStore.toggleTask).toHaveBeenCalledWith(
352
+ db,
353
+ tasks[0],
354
+ false,
355
+ );
356
+
357
+ const runBtn = item.querySelector(".tasks__run-btn") as HTMLButtonElement;
358
+ runBtn.click();
359
+ expect(orchestratorStore.runTask).toHaveBeenCalledWith(tasks[0], true);
360
+ });
361
+
362
+ it("handles restore with valid and invalid files", async () => {
363
+ const el = new ShadowClawTasks();
364
+ const db = {} as any;
365
+
366
+ // 1. No file selected
367
+ const emptyInput = document.createElement("input");
368
+ emptyInput.type = "file";
369
+ await el.handleRestore(db, emptyInput);
370
+
371
+ // 2. Valid JSON file
372
+ const backupData = [
373
+ {
374
+ id: "restored-1",
375
+ schedule: "0 0 * * *",
376
+ prompt: "daily report",
377
+ enabled: true,
378
+ },
379
+ ];
380
+ const validFile = new File([JSON.stringify(backupData)], "backup.json", {
381
+ type: "application/json",
382
+ });
383
+ const fileInput = document.createElement("input");
384
+ fileInput.type = "file";
385
+ Object.defineProperty(fileInput, "files", { value: [validFile] });
386
+
387
+ await el.handleRestore(db, fileInput);
388
+ expect(orchestratorStore.upsertTask).toHaveBeenCalled();
389
+ });
390
+
391
+ it("handles preview link clicks and opens in file viewer", async () => {
392
+ const el = new ShadowClawTasks();
393
+ const db = {} as any;
394
+
395
+ const link = document.createElement("a");
396
+ link.setAttribute("href", "docs/spec.md");
397
+ const targetDiv = document.createElement("div");
398
+ targetDiv.appendChild(link);
399
+
400
+ const event = new MouseEvent("click", {
401
+ bubbles: true,
402
+ cancelable: true,
403
+ });
404
+ Object.defineProperty(event, "target", { value: link });
405
+ Object.defineProperty(event, "button", { value: 0 });
406
+
407
+ const fileViewerMod = await import("../../stores/file-viewer.js");
408
+ await el.handlePreviewLinkClick(event, db);
409
+ expect(fileViewerMod.fileViewerStore.openFile).toHaveBeenCalledWith(
410
+ db,
411
+ "docs/spec.md",
412
+ "default",
413
+ );
414
+ });
415
+
416
+ it("cleans up on disconnectedCallback", () => {
417
+ const el = new ShadowClawTasks();
418
+ const cleanupMock = jest.fn();
419
+ el.cleanup = cleanupMock;
420
+ el.disconnectedCallback();
421
+ expect(cleanupMock).toHaveBeenCalledTimes(1);
422
+ });
423
+
424
+ it("renders tools preview for tool task chains", () => {
425
+ const el = new ShadowClawTasks();
426
+ const tools = [
427
+ { name: "read_file", input: { path: "hello.txt" } },
428
+ { name: "write_file", input: { path: "out.txt", content: "$pipe" } },
429
+ ];
430
+ const previewHtml = el.renderToolsPreview(tools);
431
+ expect(previewHtml).toContain("read_file");
432
+ expect(previewHtml).toContain("write_file");
433
+ expect(previewHtml).toContain("hello.txt");
434
+ });
435
+
436
+ it("handles clear all tasks when confirmed", async () => {
437
+ const el = new ShadowClawTasks();
438
+ const db = {} as any;
439
+ window.confirm = jest.fn(() => true);
440
+
441
+ await el.handleClearAll(db);
442
+ expect(orchestratorStore.clearAllTasks).toHaveBeenCalledWith(db);
443
+
444
+ window.confirm = jest.fn(() => false);
445
+ (orchestratorStore.clearAllTasks as any).mockClear();
446
+ await el.handleClearAll(db);
447
+ expect(orchestratorStore.clearAllTasks).not.toHaveBeenCalled();
448
+ });
449
+
450
+ it("handles edit submission for prompt and tool tasks", async () => {
451
+ const el = new ShadowClawTasks();
452
+ const db = {} as any;
453
+
454
+ const form = document.createElement("form");
455
+ const scheduleInput = document.createElement("input");
456
+ scheduleInput.name = "schedule";
457
+ scheduleInput.value = "*/15 * * * *";
458
+
459
+ const promptInput = document.createElement("textarea");
460
+ promptInput.name = "prompt";
461
+ promptInput.value = "Run automated scan";
462
+
463
+ const freshContextInput = document.createElement("input");
464
+ freshContextInput.name = "freshContext";
465
+ freshContextInput.type = "checkbox";
466
+ freshContextInput.checked = true;
467
+
468
+ const subagentInput = document.createElement("input");
469
+ subagentInput.name = "subagent";
470
+ subagentInput.type = "checkbox";
471
+ subagentInput.checked = false;
472
+
473
+ const promptRadio = document.createElement("input");
474
+ promptRadio.name = "taskType";
475
+ promptRadio.value = "prompt";
476
+ promptRadio.checked = true;
477
+
478
+ form.append(
479
+ scheduleInput,
480
+ promptInput,
481
+ freshContextInput,
482
+ subagentInput,
483
+ promptRadio,
484
+ );
485
+
486
+ const dialog = document.createElement("dialog");
487
+ dialog.close = jest.fn();
488
+ el.shadowRoot?.appendChild(dialog);
489
+
490
+ await el.handleEditSubmit(db, form);
491
+ expect(orchestratorStore.upsertTask).toHaveBeenCalledWith(
492
+ db,
493
+ expect.objectContaining({
494
+ schedule: "*/15 * * * *",
495
+ prompt: "Run automated scan",
496
+ freshContext: true,
497
+ type: "prompt",
498
+ }),
499
+ );
500
+ });
501
+
502
+ it("handles copyId, delete, reorder, restore, and renderPreview", async () => {
503
+ const el = new ShadowClawTasks() as any;
504
+ const db = {} as any;
505
+
506
+ // 1. Copy ID
507
+ const mockClipboard = {
508
+ writeText: jest.fn<any>().mockResolvedValue(undefined),
509
+ };
510
+ Object.assign(navigator, { clipboard: mockClipboard });
511
+ await el.handleCopyId("task-123");
512
+ expect(mockClipboard.writeText).toHaveBeenCalledWith("task-123");
513
+
514
+ // 2. Delete task with confirmation
515
+ el.requestConfirmation = jest.fn<any>().mockResolvedValue(true);
516
+ await el.handleDelete(db, "task-123");
517
+ expect(orchestratorStore.deleteTask).toHaveBeenCalledWith(db, "task-123");
518
+
519
+ // 3. Reorder tasks
520
+ await el.handleReorder("t1", "t2", ["t2", "t1"]);
521
+ expect(orchestratorStore.reorderTasks).toHaveBeenCalledWith(
522
+ expect.anything(),
523
+ "default",
524
+ ["t2", "t1"],
525
+ );
526
+
527
+ // 4. Restore tasks from JSON
528
+ const restoreFile = {
529
+ name: "tasks-backup.json",
530
+ text: jest
531
+ .fn<() => Promise<string>>()
532
+ .mockResolvedValue(JSON.stringify([{ id: "t1" }])),
533
+ };
534
+ const restoreInput = {
535
+ files: [restoreFile],
536
+ value: "tasks-backup.json",
537
+ } as any;
538
+ await el.handleRestore(db, restoreInput);
539
+ expect(orchestratorStore.restoreTasksFromBackup).toHaveBeenCalledWith(
540
+ db,
541
+ [{ id: "t1" }],
542
+ );
543
+
544
+ // 5. Render preview
545
+ const emptyPreview = await el.renderPreview("");
546
+ expect(emptyPreview).toContain("No content");
547
+
548
+ const shortPreview = await el.renderPreview("Short prompt");
549
+ expect(shortPreview).toContain("tasks__prompt");
550
+
551
+ const longPreview = await el.renderPreview(
552
+ "Long\nmulti-line\nprompt",
553
+ true,
554
+ );
555
+ expect(longPreview).toContain("tasks__content-details");
556
+ });
557
+
558
+ it("handles keyboard navigation, announcement, and autoscroll", async () => {
559
+ const el = new ShadowClawTasks() as any;
560
+ document.body.appendChild(el);
561
+
562
+ // 1. _announce
563
+ const liveRegion = document.createElement("div");
564
+ liveRegion.id = "live-region";
565
+ el.shadowRoot?.appendChild(liveRegion);
566
+ el._announce("Task test announcement");
567
+
568
+ // 2. Keyboard grab and navigation
569
+ (orchestratorStore as any).tasks = [
570
+ { id: "t1", name: "Task 1", prompt: "Hello 1" },
571
+ { id: "t2", name: "Task 2", prompt: "Hello 2" },
572
+ ];
573
+
574
+ // Grab with 'm'
575
+ const keyM = new KeyboardEvent("keydown", { key: "m", cancelable: true });
576
+ el._handleKeyboard(keyM, "t1", "Task 1");
577
+ expect(el._keyboardGrabbedId).toBe("t1");
578
+
579
+ // Move down with ArrowDown
580
+ const keyDown = new KeyboardEvent("keydown", {
581
+ key: "ArrowDown",
582
+ cancelable: true,
583
+ });
584
+ el._handleKeyboard(keyDown, "t1", "Task 1");
585
+
586
+ // Drop with Space
587
+ const keySpace = new KeyboardEvent("keydown", {
588
+ key: " ",
589
+ cancelable: true,
590
+ });
591
+ el._handleKeyboard(keySpace, "t1", "Task 1");
592
+ expect(el._keyboardGrabbedId).toBeNull();
593
+
594
+ // Cancel with Escape
595
+ el._keyboardGrabbedId = "t1";
596
+ const keyEsc = new KeyboardEvent("keydown", {
597
+ key: "Escape",
598
+ cancelable: true,
599
+ });
600
+ el._handleKeyboard(keyEsc, "t1", "Task 1");
601
+ expect(el._keyboardGrabbedId).toBeNull();
602
+
603
+ // Navigation when not grabbed
604
+ const item1 = document.createElement("div");
605
+ item1.className = "tasks__item";
606
+ item1.setAttribute("tabindex", "0");
607
+ const item2 = document.createElement("div");
608
+ item2.className = "tasks__item";
609
+ item2.setAttribute("tabindex", "0");
610
+ el.shadowRoot?.appendChild(item1);
611
+ el.shadowRoot?.appendChild(item2);
612
+
613
+ const navDown = new KeyboardEvent("keydown", {
614
+ key: "ArrowDown",
615
+ cancelable: true,
616
+ });
617
+ Object.defineProperty(navDown, "target", { value: item1 });
618
+ el._handleKeyboard(navDown, "t1", "Task 1");
619
+
620
+ // 3. Autoscroll
621
+ let content = el.shadowRoot?.querySelector(
622
+ ".tasks__content",
623
+ ) as HTMLElement;
624
+ if (!content) {
625
+ content = document.createElement("div");
626
+ content.className = "tasks__content";
627
+ el.shadowRoot?.appendChild(content);
628
+ }
629
+ content.getBoundingClientRect = () =>
630
+ ({ top: 100, bottom: 500, height: 400 }) as any;
631
+
632
+ el._updateAutoScrollSpeed(110); // Near top
633
+ expect(el._autoScrollActive).toBe(true);
634
+ el._stopAutoScroll();
635
+ expect(el._autoScrollActive).toBe(false);
636
+
637
+ // 4. Focus navigation
638
+ el._focusNext(item1);
639
+ el._focusPrev(item2);
640
+ el._focusNextItem(item1);
641
+ el._focusPrevItem(item2);
642
+
643
+ // 5. Touch events and findTouch
644
+ if (el.shadowRoot) {
645
+ (el.shadowRoot as any).elementFromPoint = jest.fn(() => item1);
646
+ }
647
+ const listEl = document.createElement("div");
648
+ listEl.className = "tasks__list";
649
+ el.shadowRoot?.appendChild(listEl);
650
+ el._bindTouchListEvents(listEl);
651
+
652
+ el._touchId = 123;
653
+ el._touchDraggedTaskId = "t1";
654
+ const fakeTouch = { identifier: 123, clientX: 50, clientY: 150 };
655
+ const touchMoveEvt = new Event("touchmove") as any;
656
+ touchMoveEvt.touches = [fakeTouch];
657
+ touchMoveEvt.changedTouches = [fakeTouch];
658
+ listEl.dispatchEvent(touchMoveEvt);
659
+
660
+ const touchEndEvt = new Event("touchend") as any;
661
+ touchEndEvt.touches = [];
662
+ touchEndEvt.changedTouches = [fakeTouch];
663
+ listEl.dispatchEvent(touchEndEvt);
664
+
665
+ const touchCancelEvt = new Event("touchcancel") as any;
666
+ listEl.dispatchEvent(touchCancelEvt);
667
+
668
+ el.cleanup = jest.fn();
669
+ document.body.removeChild(el);
670
+ });
264
671
  });
265
672
  });
@@ -0,0 +1,58 @@
1
+ import { consumeEscapeSequence } from "./consumeEscapeSequence.js";
2
+
3
+ describe("consumeEscapeSequence", () => {
4
+ it("handles end of string right after escape character", () => {
5
+ const res = consumeEscapeSequence("\x1b", 0);
6
+ expect(res).toEqual({ action: "ignore", nextIndex: 1, incomplete: true });
7
+ });
8
+
9
+ describe("CSI sequences (\\x1b[)", () => {
10
+ it("handles clear-screen sequence ending with J (e.g. \\x1b[2J)", () => {
11
+ const res = consumeEscapeSequence("\x1b[2JHello", 0);
12
+ expect(res).toEqual({ action: "clear-screen", nextIndex: 4 });
13
+ });
14
+
15
+ it("handles clear-line sequence ending with K (e.g. \\x1b[K)", () => {
16
+ const res = consumeEscapeSequence("\x1b[KRest", 0);
17
+ expect(res).toEqual({ action: "clear-line", nextIndex: 3 });
18
+ });
19
+
20
+ it("handles other CSI sequences (e.g. color \\x1b[31m)", () => {
21
+ const res = consumeEscapeSequence("\x1b[31mRed text", 0);
22
+ expect(res).toEqual({ action: "ignore", nextIndex: 5 });
23
+ });
24
+
25
+ it("handles incomplete CSI sequence at end of buffer", () => {
26
+ const res = consumeEscapeSequence("\x1b[31", 0);
27
+ expect(res).toEqual({ action: "ignore", nextIndex: 4, incomplete: true });
28
+ });
29
+ });
30
+
31
+ describe("OSC sequences (\\x1b])", () => {
32
+ it("handles OSC sequence terminated by BEL (\\u0007)", () => {
33
+ const res = consumeEscapeSequence("\x1b]0;Title\u0007Remaining", 0);
34
+ expect(res).toEqual({ action: "ignore", nextIndex: 10 });
35
+ });
36
+
37
+ it("handles OSC sequence terminated by ST (\\u001b\\\\)", () => {
38
+ const res = consumeEscapeSequence("\x1b]0;Title\u001b\\Remaining", 0);
39
+ expect(res).toEqual({ action: "ignore", nextIndex: 11 });
40
+ });
41
+
42
+ it("handles incomplete OSC sequence at end of buffer", () => {
43
+ const res = consumeEscapeSequence("\x1b]0;Incomplete", 0);
44
+ expect(res).toEqual({
45
+ action: "ignore",
46
+ nextIndex: 14,
47
+ incomplete: true,
48
+ });
49
+ });
50
+ });
51
+
52
+ describe("Other escape sequences", () => {
53
+ it("handles simple 2-char escape sequence (e.g. \\x1bM)", () => {
54
+ const res = consumeEscapeSequence("\x1bMRest", 0);
55
+ expect(res).toEqual({ action: "ignore", nextIndex: 2 });
56
+ });
57
+ });
58
+ });
@@ -461,4 +461,84 @@ describe("shadow-claw-tools", () => {
461
461
 
462
462
  document.body.removeChild(el);
463
463
  });
464
+
465
+ it("handles handleAddTool, handleCloneTool, handleRestore, and handleSaveProfile", async () => {
466
+ const el = new ShadowClawTools() as any;
467
+ document.body.appendChild(el);
468
+ await el.connectedCallback();
469
+ const db = {} as any;
470
+
471
+ const dialog = el.shadowRoot?.querySelector(".tools__dialog");
472
+ if (dialog) (dialog as any).close = jest.fn();
473
+ const cloneDialog = el.shadowRoot?.querySelector(".tools__clone-dialog");
474
+ if (cloneDialog) (cloneDialog as any).close = jest.fn();
475
+ const profileDialog = el.shadowRoot?.querySelector(
476
+ ".tools__profile-dialog",
477
+ );
478
+ if (profileDialog) (profileDialog as any).close = jest.fn();
479
+
480
+ // 1. Add Tool validation
481
+ const form = document.createElement("form");
482
+ form.innerHTML = `
483
+ <input name="name" value="" />
484
+ <input name="description" value="" />
485
+ `;
486
+ await el.handleAddTool(db, form);
487
+ expect(toolsStore.addCustomTool).not.toHaveBeenCalled();
488
+
489
+ // 2. Add Tool success
490
+ form.innerHTML = `
491
+ <input name="name" value="unique_tool" />
492
+ <input name="description" value="A unique tool" />
493
+ <input name="input_schema" value='{"type":"object"}' />
494
+ `;
495
+ await el.handleAddTool(db, form);
496
+ expect(toolsStore.addCustomTool).toHaveBeenCalledWith(
497
+ db,
498
+ expect.objectContaining({ name: "unique_tool" }),
499
+ );
500
+
501
+ // 3. Clone Tool
502
+ const cloneForm = document.createElement("form");
503
+ cloneForm.innerHTML = `
504
+ <input name="source" value="bash" />
505
+ <input name="name" value="bash_copy" />
506
+ <input name="description" value="Cloned bash" />
507
+ `;
508
+ await el.handleCloneTool(db, cloneForm);
509
+ expect(toolsStore.cloneTool).toHaveBeenCalledWith(
510
+ db,
511
+ "bash",
512
+ "bash_copy",
513
+ "Cloned bash",
514
+ );
515
+
516
+ // 4. Restore backup
517
+ const restoreFile = {
518
+ text: jest
519
+ .fn<() => Promise<string>>()
520
+ .mockResolvedValue(JSON.stringify({ tools: [] })),
521
+ };
522
+ const restoreInput = {
523
+ files: [restoreFile],
524
+ value: "backup.json",
525
+ } as any;
526
+ await el.handleRestore(db, restoreInput);
527
+ expect(toolsStore.importBackup).toHaveBeenCalled();
528
+
529
+ // 5. Save Profile
530
+ const profileForm = document.createElement("form");
531
+ profileForm.innerHTML = `
532
+ <input name="name" value="Dev Profile" />
533
+ <input name="providerId" value="anthropic" />
534
+ <input name="model" value="claude-3" />
535
+ `;
536
+ await el.handleSaveProfile(db, profileForm);
537
+ expect(toolsStore.addProfile).toHaveBeenCalledWith(
538
+ db,
539
+ expect.objectContaining({ name: "Dev Profile", providerId: "anthropic" }),
540
+ );
541
+
542
+ document.body.removeChild(el);
543
+ });
464
544
  });
@@ -1035,6 +1035,10 @@ export const CONFIG_KEYS = {
1035
1035
  CHAT_SPLIT_VIEW_HEIGHT: "chat_split_view_height",
1036
1036
  COMPACTION_ENGINE_PREFERENCE: "compaction_engine_preference",
1037
1037
  CONTEXT_COMPRESSION_ENABLED: "context_compression_enabled",
1038
+ CONTROL_PLANE_CLIENT_ID: "control_plane_client_id",
1039
+ CONTROL_PLANE_ENABLED: "control_plane_enabled",
1040
+ CONTROL_PLANE_TRANSPORT: "control_plane_transport",
1041
+ CONTROL_PLANE_URL: "control_plane_url",
1038
1042
  CONVERSATIONS_HEIGHT: "conversations_height",
1039
1043
  CUSTOM_TOOLS: "custom_tools",
1040
1044
  DB_MIGRATED_FROM_LEGACY: "db_migrated_from_legacy",
@@ -1185,3 +1189,9 @@ export const DEFAULT_TASK_SERVER_ENABLED = false;
1185
1189
 
1186
1190
  /** Default Files Upload Append ULID */
1187
1191
  export const DEFAULT_FILES_UPLOAD_APPEND_ULID = false;
1192
+
1193
+ /** Default Control Plane Enabled (opt-in) */
1194
+ export const DEFAULT_CONTROL_PLANE_ENABLED = false;
1195
+
1196
+ /** Default Control Plane Transport ("sse" or "websocket") */
1197
+ export const DEFAULT_CONTROL_PLANE_TRANSPORT = "sse";
@@ -0,0 +1,101 @@
1
+ import { jest } from "@jest/globals";
2
+ import { BackupController } from "./backup-controller.js";
3
+
4
+ describe("backup-controller", () => {
5
+ let mockFetch: jest.Mock<any>;
6
+
7
+ beforeEach(() => {
8
+ mockFetch = jest.fn() as any;
9
+ (globalThis as any).fetch = mockFetch;
10
+ });
11
+
12
+ it("enumerates files, uploads each, and completes the backup", async () => {
13
+ const mockFiles: Record<string, string> = {
14
+ "MEMORY.md": "# Memory content",
15
+ "src/index.ts": "console.log('hello');",
16
+ };
17
+
18
+ mockFetch.mockImplementation(async (url: string, init: any) => {
19
+ const urlStr = url.toString();
20
+ if (urlStr.includes("/api/backup/upload")) {
21
+ return {
22
+ ok: true,
23
+ status: 200,
24
+ json: async () => ({ success: true }),
25
+ };
26
+ }
27
+ if (urlStr.includes("/api/backup/complete")) {
28
+ const body = JSON.parse(init.body);
29
+ return {
30
+ ok: true,
31
+ status: 200,
32
+ json: async () => ({
33
+ success: true,
34
+ backupId: body.backupId,
35
+ fileCount: body.fileCount,
36
+ totalBytes: body.totalBytes,
37
+ }),
38
+ };
39
+ }
40
+ return { ok: false, status: 404 };
41
+ });
42
+
43
+ const progressUpdates: any[] = [];
44
+ const controller = new BackupController({
45
+ clientId: "test-client-backup",
46
+ token: "secret-token",
47
+ serverBaseUrl: "http://127.0.0.1:8888",
48
+ fileEnumerator: async () => Object.keys(mockFiles),
49
+ fileReader: async (path: string) => mockFiles[path] || null,
50
+ onProgress: (p) => progressUpdates.push(p),
51
+ });
52
+
53
+ const result = await controller.initiate();
54
+
55
+ expect(result.success).toBe(true);
56
+ expect(result.fileCount).toBe(2);
57
+ expect(result.totalBytes).toBeGreaterThan(0);
58
+ expect(result.backupId).toBeTruthy();
59
+
60
+ // Verify upload calls
61
+ expect(mockFetch).toHaveBeenCalledWith(
62
+ expect.stringContaining("/api/backup/upload"),
63
+ expect.objectContaining({
64
+ method: "POST",
65
+ headers: expect.objectContaining({
66
+ "x-control-token": "secret-token",
67
+ }),
68
+ }),
69
+ );
70
+
71
+ // Verify complete call
72
+ expect(mockFetch).toHaveBeenCalledWith(
73
+ expect.stringContaining("/api/backup/complete"),
74
+ expect.objectContaining({
75
+ method: "POST",
76
+ headers: expect.objectContaining({
77
+ "x-control-token": "secret-token",
78
+ }),
79
+ }),
80
+ );
81
+
82
+ expect(progressUpdates.length).toBe(2);
83
+ });
84
+
85
+ it("handles upload failure gracefully", async () => {
86
+ mockFetch.mockResolvedValueOnce({
87
+ ok: false,
88
+ status: 500,
89
+ text: async () => "Internal Server Error",
90
+ });
91
+
92
+ const controller = new BackupController({
93
+ clientId: "test-err-client",
94
+ serverBaseUrl: "http://127.0.0.1:8888",
95
+ fileEnumerator: async () => ["file1.txt"],
96
+ fileReader: async () => "content",
97
+ });
98
+
99
+ await expect(controller.initiate()).rejects.toThrow(/Upload failed/i);
100
+ });
101
+ });