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
@@ -1862,4 +1862,277 @@ describe("shadow-claw-file-viewer", () => {
1862
1862
  expect(resolved).toContain("data:image/png;base64,iVBORw==");
1863
1863
  });
1864
1864
  });
1865
+
1866
+ describe("syncIframeTheme & storage proxy bridge", () => {
1867
+ it("syncs theme and custom properties to previewFrameWindow", () => {
1868
+ const component = new ShadowClawFileViewer() as any;
1869
+ const mockPostMessage = jest.fn();
1870
+ component.previewFrameWindow = { postMessage: mockPostMessage };
1871
+
1872
+ component.syncIframeTheme();
1873
+
1874
+ expect(mockPostMessage).toHaveBeenCalledWith(
1875
+ expect.objectContaining({
1876
+ type: "shadow-claw-theme-update",
1877
+ theme: expect.any(String),
1878
+ customProperties: expect.any(Object),
1879
+ }),
1880
+ "*",
1881
+ );
1882
+ });
1883
+
1884
+ it("handles storage proxy requests (setItem, removeItem, getAllItems, idb-put, idb-get)", async () => {
1885
+ const component = new ShadowClawFileViewer() as any;
1886
+ const mockSource = { postMessage: jest.fn() };
1887
+
1888
+ (fileViewerStore as any).file = { path: "demo.html", name: "demo.html" };
1889
+
1890
+ // 1. setItem
1891
+ await component.handleStorageProxyRequest(
1892
+ mockSource,
1893
+ "req-1",
1894
+ "setItem",
1895
+ {
1896
+ key: "pref",
1897
+ value: "dark",
1898
+ },
1899
+ );
1900
+ expect(mockSource.postMessage).toHaveBeenCalledWith(
1901
+ expect.objectContaining({
1902
+ type: "shadow-claw-storage-proxy-result",
1903
+ requestId: "req-1",
1904
+ result: null,
1905
+ }),
1906
+ "*",
1907
+ );
1908
+
1909
+ // 2. idb-put
1910
+ await component.handleStorageProxyRequest(
1911
+ mockSource,
1912
+ "req-2",
1913
+ "idb-put",
1914
+ {
1915
+ dbName: "test-db",
1916
+ storeName: "items",
1917
+ key: "k1",
1918
+ value: { foo: "bar" },
1919
+ },
1920
+ );
1921
+ expect(mockSource.postMessage).toHaveBeenCalledWith(
1922
+ expect.objectContaining({
1923
+ type: "shadow-claw-storage-proxy-result",
1924
+ requestId: "req-2",
1925
+ result: "k1",
1926
+ }),
1927
+ "*",
1928
+ );
1929
+
1930
+ // 3. Unknown method sends error
1931
+ await component.handleStorageProxyRequest(
1932
+ mockSource,
1933
+ "req-3",
1934
+ "unknownMethod",
1935
+ {},
1936
+ );
1937
+ expect(mockSource.postMessage).toHaveBeenCalledWith(
1938
+ expect.objectContaining({
1939
+ type: "shadow-claw-storage-proxy-result",
1940
+ requestId: "req-3",
1941
+ error: expect.stringContaining("Unknown storage proxy method"),
1942
+ }),
1943
+ "*",
1944
+ );
1945
+ });
1946
+
1947
+ it("builds web share File instances from native files and binary buffers", () => {
1948
+ const component = new ShadowClawFileViewer() as any;
1949
+
1950
+ const native = new File(["content"], "test.txt", {
1951
+ type: "text/plain",
1952
+ });
1953
+ expect(component.buildWebShareFile({ nativeFile: native })).toBe(native);
1954
+
1955
+ const binaryFile = component.buildWebShareFile({
1956
+ name: "data.bin",
1957
+ binaryContent: new Uint8Array([1, 2, 3]),
1958
+ mimeType: "application/octet-stream",
1959
+ });
1960
+ expect(binaryFile).toBeInstanceOf(File);
1961
+ expect(binaryFile.name).toBe("data.bin");
1962
+
1963
+ expect(component.buildWebShareFile(null)).toBeNull();
1964
+ expect(component.buildWebShareFile({ binaryContent: null })).toBeNull();
1965
+ expect(
1966
+ component.buildWebShareFile({ binaryContent: new Uint8Array([]) }),
1967
+ ).toBeNull();
1968
+ });
1969
+
1970
+ it("cleans up resources and disconnects observers on disconnectedCallback", () => {
1971
+ const component = new ShadowClawFileViewer() as any;
1972
+ const disposeMock = jest.fn();
1973
+ component.broadcastProxy = { dispose: disposeMock };
1974
+ const disconnectMock = jest.fn();
1975
+ component.themeObserver = { disconnect: disconnectMock };
1976
+ const revokeMock = jest.spyOn(component, "revokeObjectUrl");
1977
+
1978
+ component.disconnectedCallback();
1979
+
1980
+ expect(disposeMock).toHaveBeenCalledTimes(1);
1981
+ expect(component.broadcastProxy).toBeNull();
1982
+ expect(disconnectMock).toHaveBeenCalledTimes(1);
1983
+ expect(component.themeObserver).toBeNull();
1984
+ expect(revokeMock).toHaveBeenCalledTimes(1);
1985
+ });
1986
+
1987
+ it("synchronizes iframe theme with document dark/light mode class", () => {
1988
+ const component = new ShadowClawFileViewer() as any;
1989
+ const mockPostMessage = jest.fn();
1990
+ component.previewFrameWindow = { postMessage: mockPostMessage };
1991
+
1992
+ document.documentElement.className = "dark-theme";
1993
+ component.syncIframeTheme();
1994
+
1995
+ expect(mockPostMessage).toHaveBeenCalledWith(
1996
+ expect.objectContaining({
1997
+ type: "shadow-claw-theme-update",
1998
+ }),
1999
+ "*",
2000
+ );
2001
+ });
2002
+
2003
+ it("handles openFolderInFilesView, openWorkspaceLink, and viewer dismissal", async () => {
2004
+ const component = new ShadowClawFileViewer() as any;
2005
+ component.db = {} as any;
2006
+
2007
+ // 1. openFolderInFilesView
2008
+ await component.openFolderInFilesView("docs/subfolder/");
2009
+ expect(orchestratorStore.setCurrentPath).toHaveBeenCalledWith(
2010
+ component.db,
2011
+ "docs/subfolder",
2012
+ );
2013
+ expect(fileViewerStore.closeFile).toHaveBeenCalled();
2014
+
2015
+ // 2. openWorkspaceLink with extension
2016
+ await component.openWorkspaceLink("notes.txt", "docs");
2017
+ expect(fileViewerStore.openFile).toHaveBeenCalledWith(
2018
+ component.db,
2019
+ expect.any(String),
2020
+ expect.any(String),
2021
+ );
2022
+
2023
+ // 3. canDismissViewer when not dirty
2024
+ component.isDirty = false;
2025
+ const canDismiss = await component.canDismissViewer();
2026
+ expect(canDismiss).toBe(true);
2027
+
2028
+ // 4. canDismissViewer when dirty
2029
+ component.isDirty = true;
2030
+ component.requestConfirmation = jest.fn<any>().mockResolvedValue(true);
2031
+ const canDismissDirty = await component.canDismissViewer();
2032
+ expect(canDismissDirty).toBe(true);
2033
+
2034
+ // 5. requestCloseViewer
2035
+ component.isDirty = false;
2036
+ const closed = await component.requestCloseViewer();
2037
+ expect(closed).toBe(true);
2038
+ expect(fileViewerStore.closeFile).toHaveBeenCalled();
2039
+ });
2040
+
2041
+ it("handles handleSave, handleShareFile, and toggleFullscreenMode", async () => {
2042
+ const { ShadowClawFileViewer } =
2043
+ await import("./shadow-claw-file-viewer.js");
2044
+ const { writeGroupFile } =
2045
+ await import("../../storage/writeGroupFile.js");
2046
+
2047
+ const component = new ShadowClawFileViewer() as any;
2048
+ document.body.appendChild(component);
2049
+ component.db = {} as any;
2050
+
2051
+ // 1. handleSave
2052
+ (fileViewerStore as any).file = {
2053
+ name: "test.txt",
2054
+ path: "docs/test.txt",
2055
+ kind: "text",
2056
+ content: "old content",
2057
+ groupId: "g1",
2058
+ };
2059
+ let editor = component.shadowRoot?.querySelector(".file-editor");
2060
+ if (!editor) {
2061
+ editor = document.createElement("textarea");
2062
+ editor.className = "file-editor";
2063
+ component.shadowRoot?.appendChild(editor);
2064
+ }
2065
+ editor.value = "new content";
2066
+
2067
+ await component.handleSave();
2068
+ expect(writeGroupFile).toHaveBeenCalledWith(
2069
+ component.db,
2070
+ "g1",
2071
+ "docs/test.txt",
2072
+ "new content",
2073
+ );
2074
+
2075
+ // 2. handleShareFile
2076
+ const mockShare = jest.fn<any>().mockResolvedValue(undefined);
2077
+ Object.defineProperty(navigator, "share", {
2078
+ value: mockShare,
2079
+ configurable: true,
2080
+ writable: true,
2081
+ });
2082
+ await component.handleShareFile();
2083
+ expect(mockShare).toHaveBeenCalledWith(
2084
+ expect.objectContaining({ title: "test.txt" }),
2085
+ );
2086
+
2087
+ // 3. renderBinaryPreview - empty
2088
+ const contentEl = document.createElement("div");
2089
+ component.renderBinaryPreview(contentEl, {
2090
+ name: "empty.bin",
2091
+ mimeType: "application/octet-stream",
2092
+ binaryContent: new Uint8Array(0),
2093
+ });
2094
+ expect(contentEl.textContent).toBe("Binary content unavailable.");
2095
+
2096
+ // 4. renderBinaryPreview - image
2097
+ (global.URL.createObjectURL as any) = jest
2098
+ .fn<any>()
2099
+ .mockReturnValue("blob:image-url");
2100
+ component.renderBinaryPreview(contentEl, {
2101
+ name: "img.jpg",
2102
+ mimeType: "image/jpeg",
2103
+ binaryContent: new Uint8Array([1, 2, 3]),
2104
+ });
2105
+ expect(contentEl.querySelector("img")).not.toBeNull();
2106
+
2107
+ // 5. renderBinaryPreview - video
2108
+ component.renderBinaryPreview(contentEl, {
2109
+ name: "clip.mp4",
2110
+ mimeType: "video/mp4",
2111
+ binaryContent: new Uint8Array([1, 2, 3]),
2112
+ });
2113
+ expect(contentEl.querySelector("video")).not.toBeNull();
2114
+
2115
+ // 6. renderBinaryPreview - audio
2116
+ component.renderBinaryPreview(contentEl, {
2117
+ name: "song.mp3",
2118
+ mimeType: "audio/mp3",
2119
+ binaryContent: new Uint8Array([1, 2, 3]),
2120
+ });
2121
+ expect(contentEl.querySelector("audio")).not.toBeNull();
2122
+
2123
+ // 7. renderBinaryPreview - iframe/pdf
2124
+ component.renderBinaryPreview(contentEl, {
2125
+ name: "doc.pdf",
2126
+ mimeType: "application/pdf",
2127
+ binaryContent: new Uint8Array([1, 2, 3]),
2128
+ });
2129
+ expect(contentEl.querySelector("iframe")).not.toBeNull();
2130
+
2131
+ // 8. resetContent
2132
+ component.resetContent();
2133
+ expect(component.isFullscreenMode).toBe(false);
2134
+
2135
+ document.body.removeChild(component);
2136
+ });
2137
+ });
1865
2138
  });
@@ -92,7 +92,7 @@ jest.unstable_mockModule("../../stores/orchestrator.js", () => ({
92
92
  pages: [] as Array<{ groupId: string; path: string }>,
93
93
  addPage: mockAddPage,
94
94
  removePage: mockRemovePage,
95
- loadFiles: jest.fn(),
95
+ loadFiles: jest.fn<any>().mockResolvedValue(undefined),
96
96
  db: {},
97
97
  triggerFilesRefresh: jest.fn(),
98
98
  orchestrator: {
@@ -136,6 +136,12 @@ describe("shadow-claw-files", () => {
136
136
  (orchestratorStore as any).files = [];
137
137
  (mockAddPage as any).mockResolvedValue(undefined);
138
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);
139
145
  });
140
146
 
141
147
  it("registers custom element", () => {
@@ -1129,4 +1135,405 @@ describe("shadow-claw-files", () => {
1129
1135
 
1130
1136
  document.body.removeChild(component);
1131
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
+ });
1132
1539
  });