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
@@ -0,0 +1,153 @@
1
+ import { jest } from "@jest/globals";
2
+ import { DEFAULT_MAIN_GROUP_INDEX_CONTENT } from "./defaultIndexContent.mjs";
3
+
4
+ const mockGroupFileExists = jest.fn() as any;
5
+ const mockWriteGroupFile = jest.fn() as any;
6
+ const mockGetConfig = jest.fn() as any;
7
+ const mockSetConfig = jest.fn() as any;
8
+
9
+ jest.unstable_mockModule("./groupFileExists.js", () => ({
10
+ groupFileExists: mockGroupFileExists,
11
+ }));
12
+
13
+ jest.unstable_mockModule("./writeGroupFile.js", () => ({
14
+ writeGroupFile: mockWriteGroupFile,
15
+ }));
16
+
17
+ jest.unstable_mockModule("../db/getConfig.js", () => ({
18
+ getConfig: mockGetConfig,
19
+ }));
20
+
21
+ jest.unstable_mockModule("../db/setConfig.js", () => ({
22
+ setConfig: mockSetConfig,
23
+ }));
24
+
25
+ const {
26
+ ensureMainGroupIndex,
27
+ STATIC_MAIN_GROUP_INDEX_PATH,
28
+ resolveStaticMainGroupIndexUrl,
29
+ isMainGroupIndexSuppressed,
30
+ setMainGroupIndexSuppressed,
31
+ } = await import("./ensureMainGroupIndex.js");
32
+
33
+ const { DEFAULT_GROUP_ID } = await import("../config/config.js");
34
+
35
+ describe("ensureMainGroupIndex", () => {
36
+ beforeEach(() => {
37
+ jest.clearAllMocks();
38
+ (mockGroupFileExists as any).mockReset();
39
+ (mockWriteGroupFile as any).mockReset();
40
+ (mockGetConfig as any).mockReset();
41
+ (mockSetConfig as any).mockReset();
42
+ (mockGetConfig as any).mockResolvedValue(undefined);
43
+ (mockSetConfig as any).mockResolvedValue(undefined);
44
+ (globalThis as any).fetch = (jest.fn() as any).mockRejectedValue(
45
+ new Error("no static template"),
46
+ );
47
+ });
48
+
49
+ it("returns true immediately if index.html already exists", async () => {
50
+ mockGroupFileExists.mockResolvedValue(true);
51
+
52
+ const res = await ensureMainGroupIndex({} as any, "group-1");
53
+ expect(res).toBe(true);
54
+ expect(mockWriteGroupFile).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it("returns false if index.html is suppressed for DEFAULT_GROUP_ID", async () => {
58
+ mockGroupFileExists.mockResolvedValue(false);
59
+ mockGetConfig.mockResolvedValue(true);
60
+
61
+ const res = await ensureMainGroupIndex({} as any, DEFAULT_GROUP_ID);
62
+ expect(res).toBe(false);
63
+ expect(mockWriteGroupFile).not.toHaveBeenCalled();
64
+ });
65
+
66
+ it("creates index.html with custom content if supplied", async () => {
67
+ mockGroupFileExists.mockResolvedValue(false);
68
+ mockWriteGroupFile.mockResolvedValue(undefined);
69
+
70
+ const res = await ensureMainGroupIndex(
71
+ {} as any,
72
+ "custom-group",
73
+ "<h1>Custom Title</h1>",
74
+ );
75
+ expect(res).toBe(true);
76
+ expect(mockWriteGroupFile).toHaveBeenCalledWith(
77
+ {},
78
+ "custom-group",
79
+ "index.html",
80
+ "<h1>Custom Title</h1>",
81
+ );
82
+ });
83
+
84
+ it("fetches static template if available", async () => {
85
+ mockGroupFileExists.mockResolvedValue(false);
86
+ (globalThis as any).fetch = (jest.fn() as any).mockResolvedValue({
87
+ ok: true,
88
+ text: async () => "<html>Static Template</html>",
89
+ });
90
+
91
+ const res = await ensureMainGroupIndex({} as any, DEFAULT_GROUP_ID);
92
+ expect(res).toBe(true);
93
+ expect(mockWriteGroupFile).toHaveBeenCalledWith(
94
+ {},
95
+ DEFAULT_GROUP_ID,
96
+ "index.html",
97
+ "<html>Static Template</html>",
98
+ );
99
+ });
100
+
101
+ it("falls back to DEFAULT_MAIN_GROUP_INDEX_CONTENT when fetch fails", async () => {
102
+ mockGroupFileExists.mockResolvedValue(false);
103
+ mockWriteGroupFile.mockResolvedValue(undefined);
104
+
105
+ const res = await ensureMainGroupIndex({} as any, DEFAULT_GROUP_ID);
106
+ expect(res).toBe(true);
107
+ expect(mockWriteGroupFile).toHaveBeenCalledWith(
108
+ {},
109
+ DEFAULT_GROUP_ID,
110
+ "index.html",
111
+ DEFAULT_MAIN_GROUP_INDEX_CONTENT,
112
+ );
113
+ });
114
+
115
+ it("returns false if writeGroupFile throws an error", async () => {
116
+ mockGroupFileExists.mockResolvedValue(false);
117
+ mockWriteGroupFile.mockRejectedValue(new Error("Storage write failure"));
118
+
119
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
120
+ const res = await ensureMainGroupIndex({} as any, DEFAULT_GROUP_ID);
121
+ expect(res).toBe(false);
122
+ warnSpy.mockRestore();
123
+ });
124
+
125
+ it("tests isMainGroupIndexSuppressed and setMainGroupIndexSuppressed", async () => {
126
+ mockGetConfig.mockResolvedValueOnce("true").mockResolvedValueOnce(false);
127
+
128
+ expect(await isMainGroupIndexSuppressed({} as any)).toBe(true);
129
+ expect(await isMainGroupIndexSuppressed({} as any)).toBe(false);
130
+
131
+ await setMainGroupIndexSuppressed({} as any, true);
132
+ expect(mockSetConfig).toHaveBeenCalledWith(
133
+ {},
134
+ "main_group_index_suppressed",
135
+ true,
136
+ );
137
+ });
138
+
139
+ it("resolves static main group index url with and without window.location.origin", () => {
140
+ const url = resolveStaticMainGroupIndexUrl();
141
+ expect(url).toContain(STATIC_MAIN_GROUP_INDEX_PATH);
142
+
143
+ const originalLocation = window.location;
144
+ try {
145
+ delete (window as any).location;
146
+ expect(resolveStaticMainGroupIndexUrl()).toContain(
147
+ STATIC_MAIN_GROUP_INDEX_PATH,
148
+ );
149
+ } finally {
150
+ (window as any).location = originalLocation;
151
+ }
152
+ });
153
+ });
@@ -0,0 +1,66 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("./getGroupDir.js", () => ({
4
+ getGroupDir: jest.fn(),
5
+ }));
6
+
7
+ jest.unstable_mockModule("./parsePath.js", () => ({
8
+ parsePath: jest.fn(),
9
+ }));
10
+
11
+ const { getGroupFile } = await import("./getGroupFile.js");
12
+ const { getGroupDir } = await import("./getGroupDir.js");
13
+ const { parsePath } = await import("./parsePath.js");
14
+
15
+ describe("getGroupFile", () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ });
19
+
20
+ it("retrieves native File object from nested directory structure", async () => {
21
+ const mockFile = new File(["test data"], "report.pdf", {
22
+ type: "application/pdf",
23
+ });
24
+ const fileHandle: any = {
25
+ getFile: (jest.fn() as any).mockResolvedValue(mockFile),
26
+ };
27
+ const subDir: any = {
28
+ getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
29
+ };
30
+ const rootDir: any = {
31
+ getDirectoryHandle: (jest.fn() as any).mockResolvedValue(subDir),
32
+ };
33
+
34
+ (getGroupDir as any).mockResolvedValue(rootDir);
35
+ (parsePath as any).mockReturnValue({
36
+ dirs: ["docs"],
37
+ filename: "report.pdf",
38
+ });
39
+
40
+ const result = await getGroupFile({} as any, "group-1", "docs/report.pdf");
41
+
42
+ expect(getGroupDir).toHaveBeenCalledWith({}, "group-1");
43
+ expect(rootDir.getDirectoryHandle).toHaveBeenCalledWith("docs");
44
+ expect(subDir.getFileHandle).toHaveBeenCalledWith("report.pdf");
45
+ expect(fileHandle.getFile).toHaveBeenCalled();
46
+ expect(result).toBe(mockFile);
47
+ });
48
+
49
+ it("retrieves native File from root workspace directory", async () => {
50
+ const mockFile = new File(["hello"], "README.md");
51
+ const fileHandle: any = {
52
+ getFile: (jest.fn() as any).mockResolvedValue(mockFile),
53
+ };
54
+ const rootDir: any = {
55
+ getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
56
+ };
57
+
58
+ (getGroupDir as any).mockResolvedValue(rootDir);
59
+ (parsePath as any).mockReturnValue({ dirs: [], filename: "README.md" });
60
+
61
+ const result = await getGroupFile({} as any, "group-2", "README.md");
62
+
63
+ expect(rootDir.getFileHandle).toHaveBeenCalledWith("README.md");
64
+ expect(result).toBe(mockFile);
65
+ });
66
+ });
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  MemoryDirectoryHandle,
3
3
  MemoryFileHandle,
4
+ MemorySyncAccessHandle,
4
5
  getMemoryOpfsRoot,
6
+ isMemoryStorageFallbackActive,
5
7
  resetMemoryOpfsRoot,
6
8
  } from "./memoryStorage.js";
7
9
 
@@ -65,6 +67,41 @@ describe("memoryStorage", () => {
65
67
  expect(await file2.text()).toBe("AB");
66
68
  });
67
69
 
70
+ it("supports MemorySyncAccessHandle read, write, truncate, getSize, flush and close", async () => {
71
+ const fileHandle = new MemoryFileHandle("sync.dat");
72
+ const syncHandle: MemorySyncAccessHandle =
73
+ await fileHandle.createSyncAccessHandle();
74
+
75
+ expect(syncHandle.getSize()).toBe(0);
76
+
77
+ const writeData = new Uint8Array([10, 20, 30, 40]);
78
+ const written = syncHandle.write(writeData, { at: 0 });
79
+ expect(written).toBe(4);
80
+ expect(syncHandle.getSize()).toBe(4);
81
+
82
+ syncHandle.flush(); // No-op
83
+
84
+ const readBuffer = new Uint8Array(4);
85
+ const readBytes = syncHandle.read(readBuffer, { at: 0 });
86
+ expect(readBytes).toBe(4);
87
+ expect(Array.from(readBuffer)).toEqual([10, 20, 30, 40]);
88
+
89
+ // Read past end
90
+ const emptyBuffer = new Uint8Array(2);
91
+ expect(syncHandle.read(emptyBuffer, { at: 100 })).toBe(0);
92
+
93
+ // Truncate
94
+ syncHandle.truncate(2);
95
+ expect(syncHandle.getSize()).toBe(2);
96
+
97
+ // Close and verify operations throw
98
+ syncHandle.close();
99
+ expect(() => syncHandle.getSize()).toThrow();
100
+ expect(() => syncHandle.read(readBuffer)).toThrow();
101
+ expect(() => syncHandle.write(writeData)).toThrow();
102
+ expect(() => syncHandle.truncate(1)).toThrow();
103
+ });
104
+
68
105
  it("handles permissions and equality", async () => {
69
106
  const fileHandle = new MemoryFileHandle("test.txt");
70
107
  expect(await fileHandle.queryPermission()).toBe("granted");
@@ -94,6 +131,14 @@ describe("memoryStorage", () => {
94
131
  expect(existingFile).toBe(file);
95
132
  });
96
133
 
134
+ it("handles special directory names (. and ..)", async () => {
135
+ const dir = new MemoryDirectoryHandle("root");
136
+ const current = await dir.getDirectoryHandle(".");
137
+ expect(current).toBe(dir);
138
+
139
+ await expect(dir.getDirectoryHandle("..")).rejects.toThrow();
140
+ });
141
+
97
142
  it("throws NotFoundError when entry does not exist and create is false", async () => {
98
143
  const dir = new MemoryDirectoryHandle("root");
99
144
  await expect(dir.getDirectoryHandle("missing")).rejects.toThrow();
@@ -109,6 +154,28 @@ describe("memoryStorage", () => {
109
154
  await expect(dir.getDirectoryHandle("file.txt")).rejects.toThrow();
110
155
  });
111
156
 
157
+ it("resolves descendant handle paths", async () => {
158
+ const root = new MemoryDirectoryHandle("root");
159
+ const folderA = await root.getDirectoryHandle("folderA", {
160
+ create: true,
161
+ });
162
+ const folderB = await folderA.getDirectoryHandle("folderB", {
163
+ create: true,
164
+ });
165
+ const file = await folderB.getFileHandle("item.txt", { create: true });
166
+
167
+ expect(await root.resolve(root)).toEqual([]);
168
+ expect(await root.resolve(folderA)).toEqual(["folderA"]);
169
+ expect(await root.resolve(file)).toEqual([
170
+ "folderA",
171
+ "folderB",
172
+ "item.txt",
173
+ ]);
174
+
175
+ const unrelated = new MemoryFileHandle("unrelated.txt");
176
+ expect(await root.resolve(unrelated)).toBeNull();
177
+ });
178
+
112
179
  it("iterates entries, keys, and values", async () => {
113
180
  const dir = new MemoryDirectoryHandle("root");
114
181
  await dir.getDirectoryHandle("b_dir", { create: true });
@@ -131,6 +198,13 @@ describe("memoryStorage", () => {
131
198
  values.push(v.name);
132
199
  }
133
200
  expect(values).toEqual(["b_dir", "a_file.txt"]);
201
+
202
+ // async iterator fallback
203
+ const directEntries: string[] = [];
204
+ for await (const [name, handle] of dir) {
205
+ directEntries.push(`${name}:${handle.kind}`);
206
+ }
207
+ expect(directEntries).toEqual(["b_dir:directory", "a_file.txt:file"]);
134
208
  });
135
209
 
136
210
  it("removes files and empty/recursive directories", async () => {
@@ -149,12 +223,18 @@ describe("memoryStorage", () => {
149
223
  });
150
224
  });
151
225
 
152
- describe("getMemoryOpfsRoot", () => {
153
- it("returns a singleton OPFS root child", () => {
226
+ describe("getMemoryOpfsRoot & isMemoryStorageFallbackActive", () => {
227
+ it("returns a singleton OPFS root child and reports fallback active", () => {
228
+ expect(isMemoryStorageFallbackActive()).toBe(false);
154
229
  const root1 = getMemoryOpfsRoot();
230
+ expect(isMemoryStorageFallbackActive()).toBe(true);
231
+
155
232
  const root2 = getMemoryOpfsRoot();
156
233
  expect(root1).toBe(root2);
157
234
  expect(root1.name).toBeTruthy();
235
+
236
+ resetMemoryOpfsRoot();
237
+ expect(isMemoryStorageFallbackActive()).toBe(false);
158
238
  });
159
239
  });
160
240
  });
@@ -0,0 +1,143 @@
1
+ import { jest } from "@jest/globals";
2
+
3
+ jest.unstable_mockModule("./getGroupDir.js", () => ({
4
+ getGroupDir: jest.fn(),
5
+ }));
6
+
7
+ jest.unstable_mockModule("./parsePath.js", () => ({ parsePath: jest.fn() }));
8
+
9
+ jest.unstable_mockModule("./storage.js", () => ({
10
+ getOpfsRootDirName: jest.fn().mockReturnValue("shadowclaw"),
11
+ getStorageStatus: (jest.fn() as any).mockResolvedValue({
12
+ type: "opfs",
13
+ permission: "granted",
14
+ name: "OPFS",
15
+ }),
16
+ invalidateStorageRoot: jest.fn(),
17
+ isStaleHandleError: jest.fn(),
18
+ }));
19
+
20
+ jest.unstable_mockModule("./writeFileHandle.js", () => ({
21
+ writeFileHandle: (jest.fn() as any).mockResolvedValue(undefined),
22
+ writeOpfsPathViaWorker: (jest.fn() as any).mockResolvedValue(undefined),
23
+ }));
24
+
25
+ const { writeGroupFileBytes } = await import("./writeGroupFileBytes.js");
26
+ const { getGroupDir } = await import("./getGroupDir.js");
27
+ const { parsePath } = await import("./parsePath.js");
28
+ const { getStorageStatus, invalidateStorageRoot, isStaleHandleError } =
29
+ await import("./storage.js");
30
+ const { writeFileHandle, writeOpfsPathViaWorker } =
31
+ await import("./writeFileHandle.js");
32
+
33
+ describe("writeGroupFileBytes", () => {
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ (isStaleHandleError as any).mockReturnValue(false);
37
+ });
38
+
39
+ it("writes binary Uint8Array content to target file", async () => {
40
+ const fileHandle: any = {};
41
+ const nested: any = {
42
+ getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
43
+ };
44
+ const root: any = {
45
+ getDirectoryHandle: (jest.fn() as any).mockResolvedValue(nested),
46
+ };
47
+
48
+ (getGroupDir as any).mockResolvedValue(root);
49
+ (parsePath as any).mockReturnValue({
50
+ dirs: ["images"],
51
+ filename: "photo.png",
52
+ });
53
+
54
+ const content = new Uint8Array([1, 2, 3, 4]);
55
+ await writeGroupFileBytes({} as any, "g1", "images/photo.png", content);
56
+
57
+ expect(root.getDirectoryHandle).toHaveBeenCalledWith("images", {
58
+ create: true,
59
+ });
60
+ expect(nested.getFileHandle).toHaveBeenCalledWith("photo.png", {
61
+ create: true,
62
+ });
63
+ expect(writeFileHandle).toHaveBeenCalledWith(fileHandle, content);
64
+ });
65
+
66
+ it("falls back to OPFS worker write when stream writing is unsupported in OPFS", async () => {
67
+ const fileHandle: any = {};
68
+ const nested: any = {
69
+ getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
70
+ };
71
+ const root: any = {
72
+ getDirectoryHandle: (jest.fn() as any).mockResolvedValue(nested),
73
+ };
74
+
75
+ (getGroupDir as any).mockResolvedValue(root);
76
+ (parsePath as any).mockReturnValue({ dirs: ["sub"], filename: "bin.dat" });
77
+
78
+ (writeFileHandle as any).mockRejectedValueOnce(
79
+ new Error(
80
+ "Writable file streams are not supported by this browser/storage backend.",
81
+ ),
82
+ );
83
+
84
+ const content = new Uint8Array([10, 20]);
85
+ await writeGroupFileBytes(
86
+ {} as any,
87
+ "peer:abc:123",
88
+ "sub/bin.dat",
89
+ content,
90
+ );
91
+
92
+ expect(getStorageStatus).toHaveBeenCalled();
93
+ expect(writeOpfsPathViaWorker).toHaveBeenCalledWith(
94
+ ["shadowclaw", "groups", "peer-abc-123", "sub", "bin.dat"],
95
+ content,
96
+ );
97
+ });
98
+
99
+ it("retries on stale handle error", async () => {
100
+ const fileHandle: any = {};
101
+ const root: any = {
102
+ getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
103
+ };
104
+
105
+ (parsePath as any).mockReturnValue({ dirs: [], filename: "file.txt" });
106
+ (isStaleHandleError as any)
107
+ .mockReturnValueOnce(true)
108
+ .mockReturnValue(false);
109
+
110
+ (getGroupDir as any)
111
+ .mockRejectedValueOnce(new Error("Stale handle"))
112
+ .mockResolvedValueOnce(root);
113
+
114
+ const content = new Uint8Array([5, 6]);
115
+ await writeGroupFileBytes({} as any, "g1", "file.txt", content);
116
+
117
+ expect(invalidateStorageRoot).toHaveBeenCalledTimes(1);
118
+ expect(writeFileHandle).toHaveBeenCalledWith(fileHandle, content);
119
+ });
120
+
121
+ it("throws error when non-OPFS storage backend fails with unsupported stream error", async () => {
122
+ const fileHandle: any = {};
123
+ const root: any = {
124
+ getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
125
+ };
126
+
127
+ (getGroupDir as any).mockResolvedValue(root);
128
+ (parsePath as any).mockReturnValue({ dirs: [], filename: "file.txt" });
129
+ (getStorageStatus as any).mockResolvedValueOnce({
130
+ type: "local",
131
+ permission: "granted",
132
+ name: "Folder",
133
+ });
134
+
135
+ (writeFileHandle as any).mockRejectedValueOnce(
136
+ new Error("Writable file streams are not supported"),
137
+ );
138
+
139
+ await expect(
140
+ writeGroupFileBytes({} as any, "g1", "file.txt", new Uint8Array([1])),
141
+ ).rejects.toThrow("Writable file streams are not supported");
142
+ });
143
+ });
@@ -31,4 +31,24 @@ describe("ChatUiStore", () => {
31
31
 
32
32
  (URL as any).revokeObjectURL = originalRevoke;
33
33
  });
34
+
35
+ it("manages group scroll states and snapshots", () => {
36
+ const store = new ChatUiStore();
37
+
38
+ expect(store.getGroupScrollState("group-1")).toBeNull();
39
+
40
+ store.setGroupScrollState("group-1", 120, false);
41
+ expect(store.getGroupScrollState("group-1")).toEqual({
42
+ distanceFromBottom: 120,
43
+ nearBottom: false,
44
+ });
45
+
46
+ store.setNearBottom(false);
47
+ expect(store.nearBottomSnapshot).toBe(false);
48
+
49
+ store.reset();
50
+ expect(store.getGroupScrollState("group-1")).toBeNull();
51
+ expect(store.isNearBottom).toBe(true);
52
+ expect(store.nearBottomSnapshot).toBe(true);
53
+ });
34
54
  });
@@ -2887,5 +2887,139 @@ describe("OrchestratorStore", () => {
2887
2887
  configElement.remove();
2888
2888
  }
2889
2889
  });
2890
+
2891
+ it("handles sendMessage with and without a2uiAction", async () => {
2892
+ const store = new OrchestratorStore();
2893
+ const mockSubmit = jest.fn();
2894
+ const orch: any = {
2895
+ browserChat: { submit: mockSubmit },
2896
+ events: createEvents(),
2897
+ };
2898
+ await store.init({} as any, orch);
2899
+
2900
+ store.sendMessage("plain message", []);
2901
+ expect(mockSubmit).toHaveBeenCalledWith(
2902
+ "plain message",
2903
+ DEFAULT_GROUP_ID,
2904
+ [],
2905
+ );
2906
+
2907
+ const action = {
2908
+ actionId: "btn-click",
2909
+ surfaceId: "surf-1",
2910
+ componentId: "btn-1",
2911
+ };
2912
+ store.sendMessage("action message", [], action as any);
2913
+ expect(mockSubmit).toHaveBeenCalledWith(
2914
+ "action message",
2915
+ DEFAULT_GROUP_ID,
2916
+ [],
2917
+ action,
2918
+ );
2919
+ });
2920
+
2921
+ it("manages remote agent status, typing status, and ready state", () => {
2922
+ const store = new OrchestratorStore();
2923
+ expect(store.ready).toBe(false);
2924
+
2925
+ store.setReady(true);
2926
+ expect(store.ready).toBe(true);
2927
+
2928
+ store.setRemoteAgentStatus("peer:agent-1", "running" as any);
2929
+ expect(store._remoteAgentStatusByGroup.get().get("peer:agent-1")).toBe(
2930
+ "running",
2931
+ );
2932
+
2933
+ store.setRemoteAgentTyping("peer:agent-1", true);
2934
+ expect(store._remoteAgentTypingByGroup.get().get("peer:agent-1")).toBe(
2935
+ true,
2936
+ );
2937
+
2938
+ store.setRemoteAgentTyping("peer:agent-1", false);
2939
+ expect(
2940
+ store._remoteAgentTypingByGroup.get().get("peer:agent-1"),
2941
+ ).toBeUndefined();
2942
+ });
2943
+
2944
+ it("handles folder navigation and proxy/network settings", async () => {
2945
+ const store = new OrchestratorStore();
2946
+ const mockDb: any = {};
2947
+ const orch: any = {
2948
+ browserChat: { submit: jest.fn() },
2949
+ events: createEvents(),
2950
+ };
2951
+ await store.init(mockDb, orch);
2952
+
2953
+ // 1. Folder navigation
2954
+ await store.navigateIntoFolder(mockDb, "docs");
2955
+ expect(store.currentPath).toBe("docs");
2956
+
2957
+ await store.navigateIntoFolder(mockDb, "sub");
2958
+ expect(store.currentPath).toBe("docs/sub");
2959
+
2960
+ await store.navigateBackFolder(mockDb);
2961
+ expect(store.currentPath).toBe("docs");
2962
+
2963
+ await store.resetToRootFolder(mockDb);
2964
+ expect(store.currentPath).toBe(".");
2965
+
2966
+ // 2. Proxy & network settings
2967
+ await store.setGitProxyUrl(mockDb, "https://git-proxy.example.com");
2968
+ expect(store.gitProxyUrl).toBe("https://git-proxy.example.com");
2969
+
2970
+ await store.setProxyUrl(mockDb, "https://proxy.example.com");
2971
+ expect(store.proxyUrl).toBe("https://proxy.example.com");
2972
+
2973
+ await store.setUseProxy(mockDb, true);
2974
+ expect(store.useProxy).toBe(true);
2975
+
2976
+ await store.setVMBashFullInternetAccess(mockDb, true);
2977
+ expect(store.vmBashFullInternetAccess).toBe(true);
2978
+ });
2979
+
2980
+ it("handles task management helpers (clearAllTasks, restoreTasksFromBackup, toggleTask)", async () => {
2981
+ const store = new OrchestratorStore();
2982
+ const mockDb: any = {};
2983
+ const orch: any = {
2984
+ browserChat: { submit: jest.fn() },
2985
+ events: createEvents(),
2986
+ };
2987
+ await store.init(mockDb, orch);
2988
+
2989
+ const task: any = {
2990
+ id: "task-123",
2991
+ groupId: DEFAULT_GROUP_ID,
2992
+ name: "Test Task",
2993
+ enabled: true,
2994
+ };
2995
+ mockGetAllTasks.mockResolvedValue([task]);
2996
+ await store.loadTasks(mockDb);
2997
+ expect(store.tasks.length).toBe(1);
2998
+
2999
+ // 1. toggleTask
3000
+ mockGetAllTasks.mockResolvedValue([{ ...task, enabled: false }]);
3001
+ await store.toggleTask(mockDb, task, false);
3002
+ expect(store.tasks.find((t) => t.id === "task-123")?.enabled).toBe(false);
3003
+
3004
+ // 2. restoreTasksFromBackup
3005
+ mockGetAllTasks.mockResolvedValue([
3006
+ task,
3007
+ {
3008
+ id: "task-new",
3009
+ groupId: DEFAULT_GROUP_ID,
3010
+ name: "New Task",
3011
+ enabled: true,
3012
+ },
3013
+ ]);
3014
+ await store.restoreTasksFromBackup(mockDb, [
3015
+ { id: "task-new", name: "New Task", enabled: true } as any,
3016
+ ]);
3017
+ expect(store.tasks.some((t) => t.id === "task-new")).toBe(true);
3018
+
3019
+ // 3. clearAllTasks
3020
+ mockGetAllTasks.mockResolvedValue([]);
3021
+ await store.clearAllTasks(mockDb);
3022
+ expect(store.tasks).toEqual([]);
3023
+ });
2890
3024
  });
2891
3025
  });