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.
- package/README.md +42 -5
- package/bin/assert-version-bump.mjs +60 -38
- package/bin/assert-version-bump.test.mjs +74 -11
- package/bin/cli.mjs +166 -0
- package/bin/cli.test.mjs +60 -0
- package/bin/commands/backup.mjs +70 -0
- package/bin/commands/clients.mjs +41 -0
- package/bin/commands/peer-id.mjs +76 -0
- package/bin/commands/peer-id.test.mjs +104 -0
- package/bin/commands/send.mjs +51 -0
- package/bin/commands/tasks.mjs +59 -0
- package/bin/commands/webrtc-listen.mjs +254 -0
- package/bin/prerender-dsd-shell/prerender-dsd-shell.test.mjs +46 -0
- package/bin/rolldown.config.test.mjs +401 -8
- package/bin/utils/control-client.mjs +243 -0
- package/bin/utils/control-client.test.mjs +83 -0
- package/bin/utils/ulid.mjs +45 -0
- package/bin/utils/webrtc-control-client.mjs +699 -0
- package/bin/utils/webrtc-control-client.test.mjs +318 -0
- package/dist/electron/main.cjs +1 -1
- package/dist/public/AGENTS.md +5 -3
- package/dist/public/README.md +42 -5
- package/dist/public/agent.worker.js +6 -6
- package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
- package/dist/public/backup-controller-C_s9in2t.js +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +1 -0
- package/dist/public/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +1 -0
- package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +1 -1
- package/dist/public/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
- package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
- package/dist/public/components/shadow-claw-settings/shadow-claw-settings.html +1 -1
- package/dist/public/config-CrHchneq.js +1 -0
- package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-CxpZeDJJ.js} +1 -1
- package/dist/public/{connections-xd_66YVU.js → connections-DvbgdkaB.js} +1 -1
- package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-NCqQ5lmC.js} +1 -1
- package/dist/public/docs/README.md +3 -1
- package/dist/public/docs/architecture/overview.md +12 -12
- package/dist/public/docs/architecture/storage.md +5 -2
- package/dist/public/docs/decisions/bundled-typescript-architecture.md +1 -1
- package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +6 -3
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +2 -2
- package/dist/public/docs/guides/configuring-messaging-channels.md +41 -6
- package/dist/public/docs/guides/server-development-configuration.md +24 -0
- package/dist/public/docs/publishing/index.html +19 -76
- package/dist/public/docs/skill-creator/index.html +2 -2
- package/dist/public/docs/subsystems/backup.md +56 -0
- package/dist/public/docs/subsystems/cli.md +124 -0
- package/dist/public/docs/subsystems/control-plane.md +158 -0
- package/dist/public/docs/subsystems/notifications.md +54 -2
- package/dist/public/docs/subsystems/reactive-ui.md +1 -0
- package/dist/public/downloadGroupFile-By0GR6eV.js +1 -0
- package/dist/public/e2e/README.md +5 -1
- package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-IGWsRl4i.js} +1 -1
- package/dist/public/file-viewer-D0eflgt6.js +1 -0
- package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
- package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
- package/dist/public/{git-BkjrsRXX.js → git-7Vq_SJSp.js} +2 -2
- package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/public/index.js +1 -1
- package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-CzV2E7Ul.js} +1 -1
- package/dist/public/initControlPlane-C9jpnWev.js +1 -0
- package/dist/public/llms.txt +1 -1
- package/dist/public/main/index.html +2 -2
- package/dist/public/main/memory/index.html +2 -2
- package/dist/public/{markdown-6MBrIo-S.js → markdown-BnpKxSxl.js} +1 -1
- package/dist/public/mcp-reconnect-9emsqGRe.js +1 -0
- package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-C0KvLNUp.js} +1 -1
- package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-3h9xxHUm.js} +676 -676
- package/dist/public/orchestrator-nJj59RqD.js +15 -0
- package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-BP4laMXj.js} +1 -1
- package/dist/public/{push-client-Dqq06wbB.js → push-client-CrjEXK3U.js} +1 -1
- package/dist/public/service-worker/fetch-proxy.js +1 -1
- package/dist/public/service-worker/push-handler.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-CbwETBWf.js} +2 -2
- package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-B4sBTQEj.js} +3 -3
- package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
- package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-Crv0DTCY.js} +6 -6
- package/dist/public/shadow-claw-control-plane-CX1IGbh9.js +199 -0
- package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-_9qAaRXk.js} +1 -1
- package/dist/public/shadow-claw-dialog-BrOwAOdk.js +1 -0
- package/dist/public/shadow-claw-file-viewer-Blf6yGVD.js +577 -0
- package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
- package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-v8S0JjuR.js} +3 -3
- package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
- package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
- package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-qHbP95z9.js} +1 -1
- package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
- package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
- package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DFZHhzFZ.js} +3 -3
- package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
- package/dist/public/shadow-claw-storage-BFynbP0j.js +219 -0
- package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
- package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Caxc9atL.js} +4 -4
- package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
- package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-BfJx3A9W.js} +2 -2
- package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
- package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/static-main-manifest.json +1 -1
- package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-B_M2fCjH.js} +1 -1
- package/dist/server.js +286 -189
- package/package.json +2 -1
- package/src/__mocks__/jszip.cjs +15 -5
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +35 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.css +117 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.html +81 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.test.ts +127 -0
- package/src/components/settings/shadow-claw-control-plane/shadow-claw-control-plane.ts +292 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +72 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +20 -30
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +116 -0
- package/src/components/settings/shadow-claw-llm/model-ranking.test.ts +143 -51
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +307 -4
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.ts +8 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +31 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +186 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +120 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.css +23 -2
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.html +1 -1
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +19 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.ts +2 -1
- package/src/components/shadow-claw/utils/initChatSplitResize.test.ts +180 -13
- package/src/components/shadow-claw/utils/setupEffects.test.ts +39 -0
- package/src/components/shadow-claw-a2ui/catalog/basic/shared.test.ts +117 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +1159 -1
- package/src/components/shadow-claw-chat/shadow-claw-chat.test.ts +1523 -19
- package/src/components/shadow-claw-chat/utils/escapeHtml.test.ts +13 -0
- package/src/components/shadow-claw-chat/utils/getPeerChatDisplayStatus.test.ts +20 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +494 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.ts +13 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +378 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
- package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
- package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +627 -1
- package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
- package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
- package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
- package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
- package/src/components/shadow-claw-pdf-viewer/utils/installGetOrInsertComputedPolyfill.test.ts +65 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.html +7 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +221 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.ts +2 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +407 -0
- package/src/components/shadow-claw-terminal/utils/consumeEscapeSequence.test.ts +58 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +80 -0
- package/src/config/config.test.ts +9 -0
- package/src/config/config.ts +15 -0
- package/src/core/app-routes.test.ts +56 -0
- package/src/core/app-routes.ts +8 -1
- package/src/core/backup-controller.test.ts +101 -0
- package/src/core/backup-controller.ts +137 -0
- package/src/core/control-plane-client.test.ts +408 -0
- package/src/core/control-plane-client.ts +425 -0
- package/src/core/orchestrator/orchestrator.ts +4 -0
- package/src/core/orchestrator/utils/compactContext.test.ts +78 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +254 -9
- package/src/core/orchestrator/utils/operations/provider.test.ts +326 -34
- package/src/core/orchestrator/utils/operations/vm.test.ts +114 -2
- package/src/core/orchestrator/utils/syncProxyConfigToServiceWorker.test.ts +60 -0
- package/src/core/utils/initControlPlane.test.ts +276 -0
- package/src/core/utils/initControlPlane.ts +437 -0
- package/src/core/utils/initializeApp.ts +18 -0
- package/src/db/getSession.test.ts +13 -1
- package/src/db/openDatabase.test.ts +10 -1
- package/src/db/openDatabase.ts +3 -1
- package/src/server/app.test.ts +12 -0
- package/src/server/app.ts +18 -8
- package/src/server/client-registry.test.ts +250 -0
- package/src/server/client-registry.ts +477 -0
- package/src/server/config.ts +15 -0
- package/src/server/control-plane-types.ts +120 -0
- package/src/server/control-plane.test.ts +472 -0
- package/src/server/control-plane.ts +581 -0
- package/src/server/routes/backup.test.ts +263 -0
- package/src/server/routes/backup.ts +262 -0
- package/src/server/routes/integrations-email.test.ts +532 -0
- package/src/server/routes/llamafile.test.ts +172 -48
- package/src/server/routes/mesh-llm.test.ts +192 -0
- package/src/server/routes/ollama.test.ts +49 -0
- package/src/server/server-peer.test.ts +86 -0
- package/src/server/server-peer.ts +330 -0
- package/src/server/server.ts +35 -0
- package/src/server/services/llamafile-manager.test.ts +230 -6
- package/src/server/services/transformers-runtime.test.ts +186 -18
- package/src/server/webrtc.test.ts +21 -0
- package/src/server/webrtc.ts +72 -0
- package/src/service-worker/fetch-proxy.test.ts +64 -1
- package/src/service-worker/push-handler.ts +39 -0
- package/src/shell/vm.test.ts +744 -12
- package/src/storage/ensureMainGroupIndex.test.ts +153 -0
- package/src/storage/getGroupFile.test.ts +66 -0
- package/src/storage/memoryStorage.test.ts +82 -2
- package/src/storage/migrateLegacyOpfs.test.ts +327 -0
- package/src/storage/migrateLegacyOpfs.ts +117 -0
- package/src/storage/writeGroupFileBytes.test.ts +143 -0
- package/src/stores/chat-ui.test.ts +20 -0
- package/src/stores/orchestrator.test.ts +134 -0
- package/src/stores/toast.test.ts +50 -0
- package/src/stores/tools.test.ts +147 -10
- package/src/subsystems/channels/imessage.test.ts +59 -0
- package/src/subsystems/channels/peerjs.test.ts +500 -62
- package/src/subsystems/channels/peerjs.ts +71 -4
- package/src/subsystems/channels/telegram.test.ts +177 -0
- package/src/subsystems/email/catalog.test.ts +1 -0
- package/src/subsystems/email/connections.test.ts +39 -1
- package/src/subsystems/git/credentials.test.ts +160 -825
- package/src/subsystems/git/git.test.ts +463 -0
- package/src/subsystems/notifications/__mocks__/node-sqlite.cjs +12 -1
- package/src/subsystems/notifications/push-routes.test.ts +33 -0
- package/src/subsystems/notifications/push-routes.ts +23 -0
- package/src/subsystems/notifications/push-store.ts +1 -1
- package/src/subsystems/providers/builtin-ai-tasks.test.ts +74 -0
- package/src/subsystems/providers/litert-lm-provider.test.ts +212 -51
- package/src/subsystems/providers/model-registry.test.ts +115 -0
- package/src/subsystems/providers/providers.test.ts +94 -1
- package/src/subsystems/providers/transformers-js-provider.test.ts +279 -0
- package/src/subsystems/providers/utils/createModelCacheFetch.test.ts +2 -2
- package/src/subsystems/providers/utils/downloadModelToCache.test.ts +2 -2
- package/src/subsystems/providers/utils/loadModelStream.test.ts +2 -2
- package/src/subsystems/skills/activateSkill.test.ts +101 -0
- package/src/subsystems/skills/discoverSkills.test.ts +251 -0
- package/src/subsystems/tools/cron.test.ts +64 -0
- package/src/subsystems/tools/declarative.test.ts +255 -89
- package/src/testing/assets/v86.9pfs/libv86.mjs +63 -0
- package/src/testing/assets/v86.ext2/libv86.mjs +3 -0
- package/src/testing/assets_missing_v86/assets/v86.9pfs/libv86.mjs +1 -0
- package/src/testing/jest-setup.ts +328 -40
- package/src/testing/mock-v86.mjs +49 -0
- package/src/ui/a2ui/types.ts +4 -3
- package/src/ui/a2ui/utils/normaliseComponentsToMap.test.ts +102 -0
- package/src/ui/a2ui/utils/resolveChildIds.test.ts +63 -0
- package/src/ui/a2ui/utils/resolveDynamicNumber.test.ts +13 -1
- package/src/worker/tools/bash/utils/waitForVMReady.test.ts +40 -0
- package/src/worker/tools/email/email.test.ts +193 -8
- package/src/worker/tools/email/utils/asEmailAttachmentInputs.test.ts +47 -0
- package/src/worker/tools/git/git.test.ts +214 -3
- package/src/worker/tools/rooms/invite-to-room.test.ts +54 -0
- package/src/worker/tools/rooms/leave-room.test.ts +40 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +75 -0
- package/src/worker/tools/tasks/tasks-utils.test.ts +22 -0
- package/src/worker/tools/ui/ask-user.test.ts +50 -0
- package/src/worker/tools/ui/clear-chat.test.ts +19 -0
- package/src/worker/tools/ui/get-current-time.test.ts +19 -0
- package/src/worker/tools/ui/javascript.test.ts +116 -0
- package/src/worker/tools/ui/list-components.test.ts +10 -0
- package/src/worker/tools/ui/list-tool-profiles.test.ts +42 -0
- package/src/worker/tools/ui/manage-tools.test.ts +34 -0
- package/src/worker/tools/ui/render-component.test.ts +346 -0
- package/src/worker/tools/ui/send-notification.test.ts +40 -0
- package/src/worker/tools/ui/utils/getAllowFullInternetAccess.test.ts +31 -0
- package/src/worker/tools/ui/utils/parseBooleanConfig.test.ts +25 -0
- package/src/worker/tools/workspace/attach-file.test.ts +84 -0
- package/src/worker/tools/workspace/copy-file.test.ts +69 -0
- package/src/worker/tools/workspace/create-directory.test.ts +40 -0
- package/src/worker/tools/workspace/delete-file.test.ts +73 -0
- package/src/worker/tools/workspace/diff-files.test.ts +94 -0
- package/src/worker/tools/workspace/list-files.test.ts +36 -0
- package/src/worker/tools/workspace/move-file.test.ts +66 -0
- package/src/worker/tools/workspace/open-file.test.ts +57 -0
- package/src/worker/tools/workspace/patch-file.test.ts +81 -0
- package/src/worker/tools/workspace/read-file.test.ts +116 -0
- package/src/worker/tools/workspace/send-file.test.ts +82 -0
- package/src/worker/tools/workspace/update-memory.test.ts +29 -0
- package/src/worker/tools/workspace/write-file.test.ts +28 -0
- package/src/worker/utils/rate-limit.test.ts +199 -0
- package/src/worker/utils/tool-state.test.ts +29 -0
- package/src/worker/utils/transformers-js-utils.test.ts +59 -0
- package/dist/public/config-AL77QAx4.js +0 -1
- package/dist/public/downloadGroupFile-BABv_1mJ.js +0 -1
- package/dist/public/file-viewer-CCSABMsE.js +0 -1
- package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
- package/dist/public/orchestrator-4su7iyWj.js +0 -15
- package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
- package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
- package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
- 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("<tag>");
|
|
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
|
});
|