shadow-claw 1.23.3 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/README.md +42 -5
  2. package/bin/assert-version-bump.mjs +60 -38
  3. package/bin/assert-version-bump.test.mjs +74 -11
  4. package/bin/cli.mjs +166 -0
  5. package/bin/cli.test.mjs +60 -0
  6. package/bin/commands/backup.mjs +70 -0
  7. package/bin/commands/clients.mjs +41 -0
  8. package/bin/commands/peer-id.mjs +76 -0
  9. package/bin/commands/peer-id.test.mjs +104 -0
  10. package/bin/commands/send.mjs +51 -0
  11. package/bin/commands/tasks.mjs +59 -0
  12. package/bin/commands/webrtc-listen.mjs +254 -0
  13. package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
  14. package/bin/rolldown.config.test.mjs +401 -8
  15. package/bin/utils/control-client.mjs +243 -0
  16. package/bin/utils/control-client.test.mjs +83 -0
  17. package/bin/utils/ulid.mjs +45 -0
  18. package/bin/utils/webrtc-control-client.mjs +699 -0
  19. package/bin/utils/webrtc-control-client.test.mjs +318 -0
  20. package/dist/electron/main.cjs +1 -1
  21. package/dist/public/AGENTS.md +5 -3
  22. package/dist/public/README.md +42 -5
  23. package/dist/public/agent.worker.js +6 -6
  24. package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
  25. package/dist/public/backup-controller-C_s9in2t.js +1 -0
  26. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
  27. package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
  28. package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
  29. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
  30. package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  31. package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
  32. package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
  33. package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
  34. package/dist/public/config-CrHchneq.js +1 -0
  35. package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-CxpZeDJJ.js} +1 -1
  36. package/dist/public/{connections-xd_66YVU.js → connections-DvbgdkaB.js} +1 -1
  37. package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-NCqQ5lmC.js} +1 -1
  38. package/dist/public/docs/README.md +3 -1
  39. package/dist/public/docs/architecture/overview.md +12 -12
  40. package/dist/public/docs/architecture/storage.md +5 -2
  41. package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
  42. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +6 -3
  43. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  44. package/dist/public/docs/example/article/index.html +2 -2
  45. package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
  46. package/dist/public/docs/guides/server-development-configuration.md +24 -0
  47. package/dist/public/docs/publishing/index.html +19 -76
  48. package/dist/public/docs/skill-creator/index.html +2 -2
  49. package/dist/public/docs/subsystems/backup.md +56 -0
  50. package/dist/public/docs/subsystems/cli.md +124 -0
  51. package/dist/public/docs/subsystems/control-plane.md +158 -0
  52. package/dist/public/docs/subsystems/notifications.md +54 -2
  53. package/dist/public/docs/subsystems/reactive-ui.md +1 -0
  54. package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
  55. package/dist/public/e2e/README.md +5 -1
  56. package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-IGWsRl4i.js} +1 -1
  57. package/dist/public/file-viewer-D0eflgt6.js +1 -0
  58. package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
  59. package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
  60. package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
  61. package/dist/public/{git-BkjrsRXX.js → git-7Vq_SJSp.js} +2 -2
  62. package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
  63. package/dist/public/index.html +2 -2
  64. package/dist/public/index.js +1 -1
  65. package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
  66. package/dist/public/initControlPlane-C9jpnWev.js +1 -0
  67. package/dist/public/llms.txt +1 -1
  68. package/dist/public/main/index.html +2 -2
  69. package/dist/public/main/memory/index.html +2 -2
  70. package/dist/public/{markdown-6MBrIo-S.js → markdown-BnpKxSxl.js} +1 -1
  71. package/dist/public/mcp-reconnect-9emsqGRe.js +1 -0
  72. package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-C0KvLNUp.js} +1 -1
  73. package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-3h9xxHUm.js} +676 -676
  74. package/dist/public/orchestrator-nJj59RqD.js +15 -0
  75. package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
  76. package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-BP4laMXj.js} +1 -1
  77. package/dist/public/{push-client-Dqq06wbB.js → push-client-CrjEXK3U.js} +1 -1
  78. package/dist/public/service-worker/fetch-proxy.js +1 -1
  79. package/dist/public/service-worker/push-handler.js +1 -1
  80. package/dist/public/service-worker.js +1 -1
  81. package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-CbwETBWf.js} +2 -2
  82. package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-B4sBTQEj.js} +3 -3
  83. package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
  84. package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
  85. package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
  86. package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
  87. package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
  88. package/dist/public/shadow-claw-file-viewer-Blf6yGVD.js +577 -0
  89. package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
  90. package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-v8S0JjuR.js} +3 -3
  91. package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
  92. package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
  93. package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
  94. package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-qHbP95z9.js} +1 -1
  95. package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
  96. package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
  97. package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DFZHhzFZ.js} +3 -3
  98. package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
  99. package/dist/public/shadow-claw-storage-BFynbP0j.js +219 -0
  100. package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
  101. package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Caxc9atL.js} +4 -4
  102. package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
  103. package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-BfJx3A9W.js} +2 -2
  104. package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
  105. package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
  106. package/dist/public/static-main-manifest.json +1 -1
  107. package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
  108. package/dist/server.js +286 -189
  109. package/package.json +2 -1
  110. package/src/__mocks__/jszip.cjs +15 -5
  111. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
  112. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
  113. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
  114. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
  115. package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
  116. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
  117. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
  118. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
  119. package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
  120. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
  121. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
  122. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
  123. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
  124. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
  125. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +120 -0
  126. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
  127. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
  128. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
  129. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
  130. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
  131. package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
  132. package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
  133. package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
  134. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
  135. package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
  136. package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
  137. package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
  138. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
  139. package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
  140. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +378 -0
  141. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
  142. package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
  143. package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
  144. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +627 -1
  145. package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
  146. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
  147. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
  148. package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
  149. package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
  150. package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
  151. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
  152. package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
  153. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
  154. package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
  155. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
  156. package/src/config/config.test.ts +9 -0
  157. package/src/config/config.ts +15 -0
  158. package/src/core/app-routes.test.ts +56 -0
  159. package/src/core/app-routes.ts +8 -1
  160. package/src/core/backup-controller.test.ts +101 -0
  161. package/src/core/backup-controller.ts +137 -0
  162. package/src/core/control-plane-client.test.ts +408 -0
  163. package/src/core/control-plane-client.ts +425 -0
  164. package/src/core/orchestrator/orchestrator.ts +4 -0
  165. package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
  166. package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
  167. package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
  168. package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
  169. package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
  170. package/src/core/utils/initControlPlane.test.ts +276 -0
  171. package/src/core/utils/initControlPlane.ts +437 -0
  172. package/src/core/utils/initializeApp.ts +18 -0
  173. package/src/db/getSession.test.ts +13 -1
  174. package/src/db/openDatabase.test.ts +10 -1
  175. package/src/db/openDatabase.ts +3 -1
  176. package/src/server/app.test.ts +12 -0
  177. package/src/server/app.ts +18 -8
  178. package/src/server/client-registry.test.ts +250 -0
  179. package/src/server/client-registry.ts +477 -0
  180. package/src/server/config.ts +15 -0
  181. package/src/server/control-plane-types.ts +120 -0
  182. package/src/server/control-plane.test.ts +472 -0
  183. package/src/server/control-plane.ts +581 -0
  184. package/src/server/routes/backup.test.ts +263 -0
  185. package/src/server/routes/backup.ts +262 -0
  186. package/src/server/routes/integrations-email.test.ts +532 -0
  187. package/src/server/routes/llamafile.test.ts +172 -48
  188. package/src/server/routes/mesh-llm.test.ts +192 -0
  189. package/src/server/routes/ollama.test.ts +49 -0
  190. package/src/server/server-peer.test.ts +86 -0
  191. package/src/server/server-peer.ts +330 -0
  192. package/src/server/server.ts +35 -0
  193. package/src/server/services/llamafile-manager.test.ts +230 -6
  194. package/src/server/services/transformers-runtime.test.ts +186 -18
  195. package/src/server/webrtc.test.ts +21 -0
  196. package/src/server/webrtc.ts +72 -0
  197. package/src/service-worker/fetch-proxy.test.ts +64 -1
  198. package/src/service-worker/push-handler.ts +39 -0
  199. package/src/shell/vm.test.ts +744 -12
  200. package/src/storage/ensureMainGroupIndex.test.ts +153 -0
  201. package/src/storage/getGroupFile.test.ts +66 -0
  202. package/src/storage/memoryStorage.test.ts +82 -2
  203. package/src/storage/migrateLegacyOpfs.test.ts +327 -0
  204. package/src/storage/migrateLegacyOpfs.ts +117 -0
  205. package/src/storage/writeGroupFileBytes.test.ts +143 -0
  206. package/src/stores/chat-ui.test.ts +20 -0
  207. package/src/stores/orchestrator.test.ts +134 -0
  208. package/src/stores/toast.test.ts +50 -0
  209. package/src/stores/tools.test.ts +147 -10
  210. package/src/subsystems/channels/imessage.test.ts +59 -0
  211. package/src/subsystems/channels/peerjs.test.ts +500 -62
  212. package/src/subsystems/channels/peerjs.ts +71 -4
  213. package/src/subsystems/channels/telegram.test.ts +177 -0
  214. package/src/subsystems/email/catalog.test.ts +1 -0
  215. package/src/subsystems/email/connections.test.ts +39 -1
  216. package/src/subsystems/git/credentials.test.ts +160 -825
  217. package/src/subsystems/git/git.test.ts +463 -0
  218. package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
  219. package/src/subsystems/notifications/push-routes.test.ts +33 -0
  220. package/src/subsystems/notifications/push-routes.ts +23 -0
  221. package/src/subsystems/notifications/push-store.ts +1 -1
  222. package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
  223. package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
  224. package/src/subsystems/providers/model-registry.test.ts +115 -0
  225. package/src/subsystems/providers/providers.test.ts +94 -1
  226. package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
  227. package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
  228. package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
  229. package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
  230. package/src/subsystems/skills/activateSkill.test.ts +101 -0
  231. package/src/subsystems/skills/discoverSkills.test.ts +251 -0
  232. package/src/subsystems/tools/cron.test.ts +64 -0
  233. package/src/subsystems/tools/declarative.test.ts +255 -89
  234. package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
  235. package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
  236. package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
  237. package/src/testing/jest-setup.ts +328 -40
  238. package/src/testing/mock-v86.mjs +49 -0
  239. package/src/ui/a2ui/types.ts +4 -3
  240. package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
  241. package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
  242. package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
  243. package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
  244. package/src/worker/tools/email/email.test.ts +193 -8
  245. package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
  246. package/src/worker/tools/git/git.test.ts +214 -3
  247. package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
  248. package/src/worker/tools/rooms/leave-room.test.ts +40 -0
  249. package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
  250. package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
  251. package/src/worker/tools/ui/ask-user.test.ts +50 -0
  252. package/src/worker/tools/ui/clear-chat.test.ts +19 -0
  253. package/src/worker/tools/ui/get-current-time.test.ts +19 -0
  254. package/src/worker/tools/ui/javascript.test.ts +116 -0
  255. package/src/worker/tools/ui/list-components.test.ts +10 -0
  256. package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
  257. package/src/worker/tools/ui/manage-tools.test.ts +34 -0
  258. package/src/worker/tools/ui/render-component.test.ts +346 -0
  259. package/src/worker/tools/ui/send-notification.test.ts +40 -0
  260. package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
  261. package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
  262. package/src/worker/tools/workspace/attach-file.test.ts +84 -0
  263. package/src/worker/tools/workspace/copy-file.test.ts +69 -0
  264. package/src/worker/tools/workspace/create-directory.test.ts +40 -0
  265. package/src/worker/tools/workspace/delete-file.test.ts +73 -0
  266. package/src/worker/tools/workspace/diff-files.test.ts +94 -0
  267. package/src/worker/tools/workspace/list-files.test.ts +36 -0
  268. package/src/worker/tools/workspace/move-file.test.ts +66 -0
  269. package/src/worker/tools/workspace/open-file.test.ts +57 -0
  270. package/src/worker/tools/workspace/patch-file.test.ts +81 -0
  271. package/src/worker/tools/workspace/read-file.test.ts +116 -0
  272. package/src/worker/tools/workspace/send-file.test.ts +82 -0
  273. package/src/worker/tools/workspace/update-memory.test.ts +29 -0
  274. package/src/worker/tools/workspace/write-file.test.ts +28 -0
  275. package/src/worker/utils/rate-limit.test.ts +199 -0
  276. package/src/worker/utils/tool-state.test.ts +29 -0
  277. package/src/worker/utils/transformers-js-utils.test.ts +59 -0
  278. package/dist/public/config-AL77QAx4.js +0 -1
  279. package/dist/public/downloadGroupFile-BABv_1mJ.js +0 -1
  280. package/dist/public/file-viewer-CCSABMsE.js +0 -1
  281. package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
  282. package/dist/public/orchestrator-4su7iyWj.js +0 -15
  283. package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
  284. package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
  285. package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
  286. package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
@@ -41,6 +41,14 @@ jest.unstable_mockModule("../../storage/uploadGroupFile.js", () => ({
41
41
  uploadGroupFile: jest.fn(),
42
42
  }));
43
43
 
44
+ jest.unstable_mockModule("../../db/getConfig.js", () => ({
45
+ getConfig: jest.fn<any>().mockResolvedValue(undefined),
46
+ }));
47
+
48
+ jest.unstable_mockModule("../../utils/ulid.js", () => ({
49
+ ulid: jest.fn(() => "mock-ulid"),
50
+ }));
51
+
44
52
  jest.unstable_mockModule("../../storage/writeGroupFile.js", () => ({
45
53
  writeGroupFile: jest.fn(),
46
54
  }));
@@ -84,7 +92,7 @@ jest.unstable_mockModule("../../stores/orchestrator.js", () => ({
84
92
  pages: [] as Array<{ groupId: string; path: string }>,
85
93
  addPage: mockAddPage,
86
94
  removePage: mockRemovePage,
87
- loadFiles: jest.fn(),
95
+ loadFiles: jest.fn<any>().mockResolvedValue(undefined),
88
96
  db: {},
89
97
  triggerFilesRefresh: jest.fn(),
90
98
  orchestrator: {
@@ -104,6 +112,9 @@ jest.unstable_mockModule("../../db/db.js", () => ({
104
112
  getDb: jest.fn<any>().mockResolvedValue({} as any),
105
113
  }));
106
114
 
115
+ const { CONFIG_KEYS } = await import("../../config/config.js");
116
+ const { getConfig } = await import("../../db/getConfig.js");
117
+ const { uploadGroupFile } = await import("../../storage/uploadGroupFile.js");
107
118
  const { ShadowClawFiles } = await import("./shadow-claw-files.js");
108
119
  const { deleteGroupFile } = await import("../../storage/deleteGroupFile.js");
109
120
  const { renameGroupEntry } = await import("../../storage/renameGroupEntry.js");
@@ -125,6 +136,12 @@ describe("shadow-claw-files", () => {
125
136
  (orchestratorStore as any).files = [];
126
137
  (mockAddPage as any).mockResolvedValue(undefined);
127
138
  (mockRemovePage as any).mockResolvedValue(undefined);
139
+ (writeGroupFile as any).mockResolvedValue(undefined);
140
+ (createGroupDirectory as any).mockResolvedValue(undefined);
141
+ (renameGroupEntry as any).mockResolvedValue(undefined);
142
+ (copyGroupEntry as any).mockResolvedValue(undefined);
143
+ (moveGroupEntry as any).mockResolvedValue(undefined);
144
+ (uploadGroupFile as any).mockResolvedValue(undefined);
128
145
  });
129
146
 
130
147
  it("registers custom element", () => {
@@ -145,6 +162,8 @@ describe("shadow-claw-files", () => {
145
162
 
146
163
  expect(template).toContain("files__rename-dialog");
147
164
 
165
+ expect(template).toContain("files__upload-conflict-dialog");
166
+
148
167
  expect(template).toContain("files__drop-hint");
149
168
 
150
169
  expect(template).toContain("files__upload-progress");
@@ -910,4 +929,611 @@ describe("shadow-claw-files", () => {
910
929
 
911
930
  document.body.removeChild(component);
912
931
  });
932
+
933
+ it("uploads files without ULID prefix by default", async () => {
934
+ const component = new ShadowClawFiles();
935
+ document.body.appendChild(component);
936
+ await component.render();
937
+
938
+ (getConfig as any).mockResolvedValue(undefined);
939
+ (orchestratorStore as any).currentPath = ".";
940
+ (orchestratorStore as any).files = [];
941
+
942
+ const file = new File(["content"], "document.pdf", {
943
+ type: "application/pdf",
944
+ });
945
+ await component.uploadFileList({} as any, [file]);
946
+
947
+ expect(uploadGroupFile).toHaveBeenCalledWith(
948
+ {} as any,
949
+ "default",
950
+ "document.pdf",
951
+ file,
952
+ );
953
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
954
+
955
+ document.body.removeChild(component);
956
+ });
957
+
958
+ it("uploads files with ULID prefix when setting is enabled", async () => {
959
+ const component = new ShadowClawFiles();
960
+ document.body.appendChild(component);
961
+ await component.render();
962
+
963
+ (getConfig as any).mockImplementation((_db: any, key: string) => {
964
+ if (key === CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID) {
965
+ return Promise.resolve("true");
966
+ }
967
+ return Promise.resolve(undefined);
968
+ });
969
+ (orchestratorStore as any).currentPath = "subfolder";
970
+ (orchestratorStore as any).files = [];
971
+
972
+ const file = new File(["content"], "image.png", {
973
+ type: "image/png",
974
+ });
975
+ await component.uploadFileList({} as any, [file]);
976
+
977
+ expect(uploadGroupFile).toHaveBeenCalledWith(
978
+ {} as any,
979
+ "default",
980
+ "subfolder/mock-ulid-image.png",
981
+ file,
982
+ );
983
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
984
+
985
+ document.body.removeChild(component);
986
+ });
987
+
988
+ it("prompts conflict dialog and overwrites existing file when selected", async () => {
989
+ const component = new ShadowClawFiles();
990
+ document.body.appendChild(component);
991
+ await component.render();
992
+
993
+ (getConfig as any).mockResolvedValue(undefined);
994
+ (orchestratorStore as any).currentPath = ".";
995
+ (orchestratorStore as any).files = ["existing.txt"];
996
+ (orchestratorStore as any).pages = [
997
+ { groupId: "default", path: "existing.txt" },
998
+ ];
999
+
1000
+ jest.spyOn(component, "promptUploadConflict").mockResolvedValue({
1001
+ action: "overwrite",
1002
+ });
1003
+
1004
+ const file = new File(["new content"], "existing.txt", {
1005
+ type: "text/plain",
1006
+ });
1007
+ await component.uploadFileList({} as any, [file]);
1008
+
1009
+ expect(component.promptUploadConflict).toHaveBeenCalledWith(
1010
+ "existing.txt",
1011
+ expect.any(Set),
1012
+ );
1013
+ expect(mockRemovePage).toHaveBeenCalledWith(
1014
+ {} as any,
1015
+ "existing.txt",
1016
+ "default",
1017
+ );
1018
+ expect(uploadGroupFile).toHaveBeenCalledWith(
1019
+ {} as any,
1020
+ "default",
1021
+ "existing.txt",
1022
+ file,
1023
+ );
1024
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
1025
+
1026
+ document.body.removeChild(component);
1027
+ });
1028
+
1029
+ it("prompts conflict dialog and renames file when new name is chosen", async () => {
1030
+ const component = new ShadowClawFiles();
1031
+ document.body.appendChild(component);
1032
+ await component.render();
1033
+
1034
+ (getConfig as any).mockResolvedValue(undefined);
1035
+ (orchestratorStore as any).currentPath = "docs";
1036
+ (orchestratorStore as any).files = ["existing.txt"];
1037
+
1038
+ jest.spyOn(component, "promptUploadConflict").mockResolvedValue({
1039
+ action: "rename",
1040
+ name: "existing-renamed.txt",
1041
+ });
1042
+
1043
+ const file = new File(["content"], "existing.txt", {
1044
+ type: "text/plain",
1045
+ });
1046
+ await component.uploadFileList({} as any, [file]);
1047
+
1048
+ expect(component.promptUploadConflict).toHaveBeenCalledWith(
1049
+ "existing.txt",
1050
+ expect.any(Set),
1051
+ );
1052
+ expect(uploadGroupFile).toHaveBeenCalledWith(
1053
+ {} as any,
1054
+ "default",
1055
+ "docs/existing-renamed.txt",
1056
+ file,
1057
+ );
1058
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
1059
+
1060
+ document.body.removeChild(component);
1061
+ });
1062
+
1063
+ it("prompts conflict dialog and skips upload when user cancels", async () => {
1064
+ const component = new ShadowClawFiles();
1065
+ document.body.appendChild(component);
1066
+ await component.render();
1067
+
1068
+ (getConfig as any).mockResolvedValue(undefined);
1069
+ (orchestratorStore as any).currentPath = ".";
1070
+ (orchestratorStore as any).files = ["existing.txt"];
1071
+
1072
+ jest.spyOn(component, "promptUploadConflict").mockResolvedValue(null);
1073
+
1074
+ const file = new File(["content"], "existing.txt", {
1075
+ type: "text/plain",
1076
+ });
1077
+ await component.uploadFileList({} as any, [file]);
1078
+
1079
+ expect(component.promptUploadConflict).toHaveBeenCalledWith(
1080
+ "existing.txt",
1081
+ expect.any(Set),
1082
+ );
1083
+ expect(uploadGroupFile).not.toHaveBeenCalled();
1084
+
1085
+ document.body.removeChild(component);
1086
+ });
1087
+
1088
+ it("handles conflict dialog UI submission and validation", async () => {
1089
+ const component = new ShadowClawFiles();
1090
+ document.body.appendChild(component);
1091
+ await component.render();
1092
+
1093
+ const existingNames = new Set(["existing.txt", "taken.txt"]);
1094
+ const conflictPromise = component.promptUploadConflict(
1095
+ "existing.txt",
1096
+ existingNames,
1097
+ );
1098
+
1099
+ const input = component.shadowRoot?.querySelector(
1100
+ ".files__upload-conflict-rename-input",
1101
+ ) as HTMLInputElement;
1102
+ const form = component.shadowRoot?.querySelector(
1103
+ ".files__upload-conflict-form",
1104
+ ) as HTMLFormElement;
1105
+
1106
+ expect(input.value).toBe("existing.txt");
1107
+
1108
+ // 1. Try empty name
1109
+ input.value = " ";
1110
+ form.dispatchEvent(new Event("submit"));
1111
+ expect(showWarning).toHaveBeenCalledWith("Please enter a name", 3000);
1112
+
1113
+ // 2. Try path name
1114
+ input.value = "folder/name.txt";
1115
+ form.dispatchEvent(new Event("submit"));
1116
+ expect(showWarning).toHaveBeenCalledWith(
1117
+ "Use only a name, not a path",
1118
+ 3500,
1119
+ );
1120
+
1121
+ // 3. Try already taken name
1122
+ input.value = "taken.txt";
1123
+ form.dispatchEvent(new Event("submit"));
1124
+ expect(showWarning).toHaveBeenCalledWith(
1125
+ '"taken.txt" already exists. Please choose a different name.',
1126
+ 4000,
1127
+ );
1128
+
1129
+ // 4. Valid new name
1130
+ input.value = "brand-new.txt";
1131
+ form.dispatchEvent(new Event("submit"));
1132
+
1133
+ const result = await conflictPromise;
1134
+ expect(result).toEqual({ action: "rename", name: "brand-new.txt" });
1135
+
1136
+ document.body.removeChild(component);
1137
+ });
1138
+
1139
+ describe("files component helpers and operations", () => {
1140
+ it("escapes html text", () => {
1141
+ const component = new ShadowClawFiles();
1142
+ expect(component.escapeHtml("<tag>")).toBe("&lt;tag&gt;");
1143
+ });
1144
+
1145
+ it("identifies page candidate files correctly", () => {
1146
+ const component = new ShadowClawFiles();
1147
+ expect(component.isPageCandidateFile("index.md", false)).toBe(true);
1148
+ expect(component.isPageCandidateFile("index.html", false)).toBe(true);
1149
+ expect(component.isPageCandidateFile("index.htm", false)).toBe(true);
1150
+ expect(component.isPageCandidateFile("index.xhtml", false)).toBe(true);
1151
+ expect(component.isPageCandidateFile("index.markdown", false)).toBe(true);
1152
+ expect(component.isPageCandidateFile("script.js", false)).toBe(false);
1153
+ expect(component.isPageCandidateFile("folder.md", true)).toBe(false);
1154
+ });
1155
+
1156
+ it("detects invalid folder paste targets", () => {
1157
+ const component = new ShadowClawFiles();
1158
+ expect(
1159
+ component.isInvalidFolderPasteTarget(
1160
+ { sourcePath: "docs", isDirectory: false },
1161
+ "docs/sub",
1162
+ ),
1163
+ ).toBe(false);
1164
+ expect(
1165
+ component.isInvalidFolderPasteTarget(
1166
+ { sourcePath: "docs", isDirectory: true },
1167
+ ".",
1168
+ ),
1169
+ ).toBe(false);
1170
+ expect(
1171
+ component.isInvalidFolderPasteTarget(
1172
+ { sourcePath: "docs", isDirectory: true },
1173
+ "docs",
1174
+ ),
1175
+ ).toBe(true);
1176
+ expect(
1177
+ component.isInvalidFolderPasteTarget(
1178
+ { sourcePath: "docs", isDirectory: true },
1179
+ "docs/nested",
1180
+ ),
1181
+ ).toBe(true);
1182
+ expect(
1183
+ component.isInvalidFolderPasteTarget(
1184
+ { sourcePath: "docs", isDirectory: true },
1185
+ "other-dir",
1186
+ ),
1187
+ ).toBe(false);
1188
+ });
1189
+
1190
+ it("detects drag files in DragEvent", () => {
1191
+ const component = new ShadowClawFiles();
1192
+ expect(component.hasDragFiles({ dataTransfer: null } as any)).toBe(false);
1193
+ expect(
1194
+ component.hasDragFiles({
1195
+ dataTransfer: { types: ["text/plain"] },
1196
+ } as any),
1197
+ ).toBe(false);
1198
+ expect(
1199
+ component.hasDragFiles({ dataTransfer: { types: ["Files"] } } as any),
1200
+ ).toBe(true);
1201
+ });
1202
+
1203
+ it("handles syncHostToVM action", () => {
1204
+ const component = new ShadowClawFiles();
1205
+ document.body.appendChild(component);
1206
+
1207
+ const button = document.createElement("button");
1208
+ button.className = "files__sync-host-btn";
1209
+ component.shadowRoot?.appendChild(button);
1210
+
1211
+ (orchestratorStore as any).syncHostWorkspaceToVM = jest.fn();
1212
+
1213
+ component.handleSyncHostToVM();
1214
+
1215
+ expect(
1216
+ (orchestratorStore as any).syncHostWorkspaceToVM,
1217
+ ).toHaveBeenCalled();
1218
+ expect(showSuccess).toHaveBeenCalledWith(
1219
+ "Requested host → VM workspace sync",
1220
+ 2200,
1221
+ );
1222
+
1223
+ document.body.removeChild(component);
1224
+ });
1225
+
1226
+ it("handles syncVMToHost action", async () => {
1227
+ const component = new ShadowClawFiles();
1228
+ document.body.appendChild(component);
1229
+
1230
+ const button = document.createElement("button");
1231
+ button.className = "files__sync-vm-btn";
1232
+ component.shadowRoot?.appendChild(button);
1233
+
1234
+ (orchestratorStore as any).syncVMWorkspaceToHost = jest.fn();
1235
+
1236
+ await component.handleSyncVMToHost({} as any);
1237
+
1238
+ expect(
1239
+ (orchestratorStore as any).syncVMWorkspaceToHost,
1240
+ ).toHaveBeenCalled();
1241
+ expect(showSuccess).toHaveBeenCalledWith(
1242
+ "Requested VM → host workspace sync",
1243
+ 2200,
1244
+ );
1245
+
1246
+ document.body.removeChild(component);
1247
+ });
1248
+
1249
+ it("handles backup (download all zip) and clear all files", async () => {
1250
+ const component = new ShadowClawFiles();
1251
+ const db = {} as any;
1252
+
1253
+ const { downloadAllGroupFilesAsZip } =
1254
+ await import("../../storage/downloadAllGroupFilesAsZip.js");
1255
+ const { deleteAllGroupFiles } =
1256
+ await import("../../storage/deleteAllGroupFiles.js");
1257
+
1258
+ // 1. Backup
1259
+ await component.handleBackup(db);
1260
+ expect(downloadAllGroupFilesAsZip).toHaveBeenCalledWith(db, "default");
1261
+ expect(showSuccess).toHaveBeenCalledWith(
1262
+ "Backup created successfully",
1263
+ 3000,
1264
+ );
1265
+
1266
+ // 2. Clear all
1267
+ component.requestConfirmation = jest.fn<any>().mockResolvedValue(true);
1268
+ (orchestratorStore as any).resetToRootFolder = jest.fn();
1269
+
1270
+ await component.handleClearAll(db);
1271
+ expect(deleteAllGroupFiles).toHaveBeenCalledWith(db, "default");
1272
+ expect(showSuccess).toHaveBeenCalledWith("All files deleted", 3500);
1273
+ });
1274
+
1275
+ it("handles create new file and folder flows with validation", async () => {
1276
+ const component = new ShadowClawFiles() as any;
1277
+ document.body.appendChild(component);
1278
+ const db = {} as any;
1279
+
1280
+ const { writeGroupFile } =
1281
+ await import("../../storage/writeGroupFile.js");
1282
+ const { createGroupDirectory } =
1283
+ await import("../../storage/createGroupDirectory.js");
1284
+
1285
+ const dialog = component.shadowRoot?.querySelector(".files__new-dialog");
1286
+ if (dialog) (dialog as any).close = jest.fn();
1287
+
1288
+ const input = component.shadowRoot?.querySelector(
1289
+ ".files__new-input",
1290
+ ) as HTMLInputElement;
1291
+ const isFolderInput = component.shadowRoot?.querySelector(
1292
+ ".files__new-is-folder",
1293
+ ) as HTMLInputElement;
1294
+
1295
+ // 1. Validation: empty name
1296
+ if (input) input.value = "";
1297
+ await component.handleCreateNewFile(db);
1298
+ expect(showWarning).toHaveBeenCalledWith(
1299
+ "Please enter a file name",
1300
+ 3000,
1301
+ );
1302
+
1303
+ // 2. Validation: slashes in name
1304
+ if (input) input.value = "folder/file.txt";
1305
+ await component.handleCreateNewFile(db);
1306
+ expect(showWarning).toHaveBeenCalledWith(
1307
+ "Use only a file name, not a path",
1308
+ 3500,
1309
+ );
1310
+
1311
+ // 3. Create file successfully
1312
+ if (input) input.value = "test.txt";
1313
+ if (isFolderInput) isFolderInput.checked = false;
1314
+ await component.handleCreateNewFile(db);
1315
+ expect(writeGroupFile).toHaveBeenCalledWith(
1316
+ db,
1317
+ "default",
1318
+ "test.txt",
1319
+ "",
1320
+ );
1321
+ expect(showSuccess).toHaveBeenCalledWith("Created file: test.txt", 3000);
1322
+
1323
+ // 4. Create folder successfully
1324
+ if (input) input.value = "docs";
1325
+ if (isFolderInput) isFolderInput.checked = true;
1326
+ await component.handleCreateNewFile(db);
1327
+ expect(createGroupDirectory).toHaveBeenCalledWith(db, "default", "docs");
1328
+ expect(showSuccess).toHaveBeenCalledWith("Created folder: docs", 3000);
1329
+
1330
+ document.body.removeChild(component);
1331
+ });
1332
+
1333
+ it("handles rename entry flow for files and folders", async () => {
1334
+ const component = new ShadowClawFiles() as any;
1335
+ document.body.appendChild(component);
1336
+ const db = {} as any;
1337
+
1338
+ const { renameGroupEntry } =
1339
+ await import("../../storage/renameGroupEntry.js");
1340
+
1341
+ const dialog = component.shadowRoot?.querySelector(
1342
+ ".files__rename-dialog",
1343
+ );
1344
+ if (dialog) (dialog as any).close = jest.fn();
1345
+
1346
+ const input = component.shadowRoot?.querySelector(
1347
+ ".files__rename-input",
1348
+ ) as HTMLInputElement;
1349
+
1350
+ component._pendingRenamePath = "old.txt";
1351
+ component._pendingRenameName = "old.txt";
1352
+ component._pendingRenameIsDirectory = false;
1353
+
1354
+ // 1. Validation: empty name
1355
+ if (input) input.value = "";
1356
+ await component.handleRenameEntry(db);
1357
+ expect(showWarning).toHaveBeenCalledWith("Please enter a name", 3000);
1358
+
1359
+ // 2. Validation: path with slashes
1360
+ if (input) input.value = "sub/new.txt";
1361
+ await component.handleRenameEntry(db);
1362
+ expect(showWarning).toHaveBeenCalledWith(
1363
+ "Use only a name, not a path",
1364
+ 3500,
1365
+ );
1366
+
1367
+ // 3. Same name closes dialog without calling renameGroupEntry
1368
+ if (input) input.value = "old.txt";
1369
+ await component.handleRenameEntry(db);
1370
+ expect(renameGroupEntry).not.toHaveBeenCalled();
1371
+
1372
+ // 4. Successful file rename
1373
+ component._pendingRenamePath = "old.txt";
1374
+ component._pendingRenameName = "old.txt";
1375
+ component._pendingRenameIsDirectory = false;
1376
+ if (input) input.value = "new.txt";
1377
+ await component.handleRenameEntry(db);
1378
+ expect(renameGroupEntry).toHaveBeenCalledWith(
1379
+ db,
1380
+ "default",
1381
+ "old.txt",
1382
+ "new.txt",
1383
+ );
1384
+ expect(showSuccess).toHaveBeenCalledWith(
1385
+ "Renamed file: old.txt -> new.txt",
1386
+ 3200,
1387
+ );
1388
+
1389
+ // 5. Successful folder rename
1390
+ component._pendingRenamePath = "old-folder";
1391
+ component._pendingRenameName = "old-folder";
1392
+ component._pendingRenameIsDirectory = true;
1393
+ if (input) input.value = "new-folder";
1394
+ await component.handleRenameEntry(db);
1395
+ expect(renameGroupEntry).toHaveBeenCalledWith(
1396
+ db,
1397
+ "default",
1398
+ "old-folder",
1399
+ "new-folder",
1400
+ );
1401
+ expect(showSuccess).toHaveBeenCalledWith(
1402
+ "Renamed folder: old-folder -> new-folder",
1403
+ 3200,
1404
+ );
1405
+
1406
+ document.body.removeChild(component);
1407
+ });
1408
+
1409
+ it("handles restore and upload file actions", async () => {
1410
+ const component = new ShadowClawFiles() as any;
1411
+ document.body.appendChild(component);
1412
+ const db = {} as any;
1413
+
1414
+ const { restoreAllGroupFilesFromZip } =
1415
+ await import("../../storage/restoreAllGroupFilesFromZip.js");
1416
+ const { uploadGroupFile } =
1417
+ await import("../../storage/uploadGroupFile.js");
1418
+
1419
+ // 1. Restore non-zip warning
1420
+ const nonZipInput = {
1421
+ files: [{ name: "test.txt" }],
1422
+ value: "test.txt",
1423
+ } as any;
1424
+ await component.handleRestore(db, nonZipInput);
1425
+ expect(showWarning).toHaveBeenCalledWith(
1426
+ "Please select a .zip file",
1427
+ 3500,
1428
+ );
1429
+
1430
+ // 2. Restore cancelled confirmation
1431
+ component.requestConfirmation = jest.fn<any>().mockResolvedValue(false);
1432
+ const zipFile = new File(["zipcontent"], "backup.zip", {
1433
+ type: "application/zip",
1434
+ });
1435
+ const zipInput = {
1436
+ files: [zipFile],
1437
+ value: "backup.zip",
1438
+ } as any;
1439
+ await component.handleRestore(db, zipInput);
1440
+ expect(restoreAllGroupFilesFromZip).not.toHaveBeenCalled();
1441
+
1442
+ // 3. Restore confirmed
1443
+ component.requestConfirmation = jest.fn<any>().mockResolvedValue(true);
1444
+ await component.handleRestore(db, zipInput);
1445
+ expect(restoreAllGroupFilesFromZip).toHaveBeenCalledWith(
1446
+ db,
1447
+ "default",
1448
+ zipFile,
1449
+ );
1450
+ expect(showSuccess).toHaveBeenCalledWith(
1451
+ "Files restored successfully",
1452
+ 3500,
1453
+ );
1454
+
1455
+ // 4. Upload file
1456
+ const uploadFile = new File(["content"], "upload.txt", {
1457
+ type: "text/plain",
1458
+ });
1459
+ const uploadInput = {
1460
+ files: [uploadFile],
1461
+ value: "upload.txt",
1462
+ } as any;
1463
+ await component.handleUpload(db, uploadInput);
1464
+ expect(uploadGroupFile).toHaveBeenCalledWith(
1465
+ db,
1466
+ "default",
1467
+ "upload.txt",
1468
+ uploadFile,
1469
+ );
1470
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
1471
+
1472
+ // 5. handleSyncVMToHost
1473
+ const syncBtn = document.createElement("button");
1474
+ syncBtn.className = "files__sync-vm-btn";
1475
+ component.shadowRoot?.appendChild(syncBtn);
1476
+ await component.handleSyncVMToHost(db);
1477
+ expect(orchestratorStore.syncVMWorkspaceToHost).toHaveBeenCalled();
1478
+
1479
+ // 6. handleRenameEntry
1480
+ const { renameGroupEntry } =
1481
+ await import("../../storage/renameGroupEntry.js");
1482
+ component._pendingRenamePath = "docs/old.txt";
1483
+ component._pendingRenameName = "old.txt";
1484
+
1485
+ const renameInput = component.shadowRoot?.querySelector(
1486
+ ".files__rename-input",
1487
+ ) as HTMLInputElement;
1488
+ if (renameInput) renameInput.value = "new.txt";
1489
+
1490
+ await component.handleRenameEntry(db);
1491
+ expect(renameGroupEntry).toHaveBeenCalledWith(
1492
+ db,
1493
+ "default",
1494
+ "docs/old.txt",
1495
+ "new.txt",
1496
+ );
1497
+
1498
+ // 7. updateBreadcrumbs
1499
+ (orchestratorStore as any).currentPath = "docs/guides";
1500
+ let breadcrumbsContainer = component.shadowRoot?.querySelector(
1501
+ "[data-breadcrumb-path]",
1502
+ );
1503
+ if (!breadcrumbsContainer) {
1504
+ breadcrumbsContainer = document.createElement("div");
1505
+ breadcrumbsContainer.setAttribute("data-breadcrumb-path", "");
1506
+ component.shadowRoot?.appendChild(breadcrumbsContainer);
1507
+ }
1508
+ component.updateBreadcrumbs(db);
1509
+
1510
+ const breadcrumbButtons = breadcrumbsContainer.querySelectorAll(
1511
+ ".files__breadcrumb-btn",
1512
+ );
1513
+ expect(breadcrumbButtons.length).toBeGreaterThan(0);
1514
+ // Click root
1515
+ (breadcrumbButtons[0] as HTMLButtonElement).click();
1516
+ expect(orchestratorStore.resetToRootFolder).toHaveBeenCalled();
1517
+
1518
+ // 8. updateFileList
1519
+ let listContainer = component.shadowRoot?.querySelector(".files__list");
1520
+ if (!listContainer) {
1521
+ listContainer = document.createElement("div");
1522
+ listContainer.className = "files__list";
1523
+ component.shadowRoot?.appendChild(listContainer);
1524
+ }
1525
+ (orchestratorStore as any).files = ["readme.md", "src/"];
1526
+ component.updateFileList(db);
1527
+ expect(listContainer.children.length).toBe(2);
1528
+
1529
+ // empty files
1530
+ (orchestratorStore as any).files = [];
1531
+ component.updateFileList(db);
1532
+ expect(
1533
+ listContainer.querySelector("shadow-claw-empty-state"),
1534
+ ).not.toBeNull();
1535
+
1536
+ document.body.removeChild(component);
1537
+ });
1538
+ });
913
1539
  });