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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { DEFAULT_MAIN_GROUP_INDEX_CONTENT } from "./defaultIndexContent.mjs";
|
|
3
|
+
|
|
4
|
+
const mockGroupFileExists = jest.fn() as any;
|
|
5
|
+
const mockWriteGroupFile = jest.fn() as any;
|
|
6
|
+
const mockGetConfig = jest.fn() as any;
|
|
7
|
+
const mockSetConfig = jest.fn() as any;
|
|
8
|
+
|
|
9
|
+
jest.unstable_mockModule("./groupFileExists.js", () => ({
|
|
10
|
+
groupFileExists: mockGroupFileExists,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.unstable_mockModule("./writeGroupFile.js", () => ({
|
|
14
|
+
writeGroupFile: mockWriteGroupFile,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.unstable_mockModule("../db/getConfig.js", () => ({
|
|
18
|
+
getConfig: mockGetConfig,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
jest.unstable_mockModule("../db/setConfig.js", () => ({
|
|
22
|
+
setConfig: mockSetConfig,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
ensureMainGroupIndex,
|
|
27
|
+
STATIC_MAIN_GROUP_INDEX_PATH,
|
|
28
|
+
resolveStaticMainGroupIndexUrl,
|
|
29
|
+
isMainGroupIndexSuppressed,
|
|
30
|
+
setMainGroupIndexSuppressed,
|
|
31
|
+
} = await import("./ensureMainGroupIndex.js");
|
|
32
|
+
|
|
33
|
+
const { DEFAULT_GROUP_ID } = await import("../config/config.js");
|
|
34
|
+
|
|
35
|
+
describe("ensureMainGroupIndex", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest.clearAllMocks();
|
|
38
|
+
(mockGroupFileExists as any).mockReset();
|
|
39
|
+
(mockWriteGroupFile as any).mockReset();
|
|
40
|
+
(mockGetConfig as any).mockReset();
|
|
41
|
+
(mockSetConfig as any).mockReset();
|
|
42
|
+
(mockGetConfig as any).mockResolvedValue(undefined);
|
|
43
|
+
(mockSetConfig as any).mockResolvedValue(undefined);
|
|
44
|
+
(globalThis as any).fetch = (jest.fn() as any).mockRejectedValue(
|
|
45
|
+
new Error("no static template"),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns true immediately if index.html already exists", async () => {
|
|
50
|
+
mockGroupFileExists.mockResolvedValue(true);
|
|
51
|
+
|
|
52
|
+
const res = await ensureMainGroupIndex({} as any, "group-1");
|
|
53
|
+
expect(res).toBe(true);
|
|
54
|
+
expect(mockWriteGroupFile).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns false if index.html is suppressed for DEFAULT_GROUP_ID", async () => {
|
|
58
|
+
mockGroupFileExists.mockResolvedValue(false);
|
|
59
|
+
mockGetConfig.mockResolvedValue(true);
|
|
60
|
+
|
|
61
|
+
const res = await ensureMainGroupIndex({} as any, DEFAULT_GROUP_ID);
|
|
62
|
+
expect(res).toBe(false);
|
|
63
|
+
expect(mockWriteGroupFile).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("creates index.html with custom content if supplied", async () => {
|
|
67
|
+
mockGroupFileExists.mockResolvedValue(false);
|
|
68
|
+
mockWriteGroupFile.mockResolvedValue(undefined);
|
|
69
|
+
|
|
70
|
+
const res = await ensureMainGroupIndex(
|
|
71
|
+
{} as any,
|
|
72
|
+
"custom-group",
|
|
73
|
+
"<h1>Custom Title</h1>",
|
|
74
|
+
);
|
|
75
|
+
expect(res).toBe(true);
|
|
76
|
+
expect(mockWriteGroupFile).toHaveBeenCalledWith(
|
|
77
|
+
{},
|
|
78
|
+
"custom-group",
|
|
79
|
+
"index.html",
|
|
80
|
+
"<h1>Custom Title</h1>",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("fetches static template if available", async () => {
|
|
85
|
+
mockGroupFileExists.mockResolvedValue(false);
|
|
86
|
+
(globalThis as any).fetch = (jest.fn() as any).mockResolvedValue({
|
|
87
|
+
ok: true,
|
|
88
|
+
text: async () => "<html>Static Template</html>",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const res = await ensureMainGroupIndex({} as any, DEFAULT_GROUP_ID);
|
|
92
|
+
expect(res).toBe(true);
|
|
93
|
+
expect(mockWriteGroupFile).toHaveBeenCalledWith(
|
|
94
|
+
{},
|
|
95
|
+
DEFAULT_GROUP_ID,
|
|
96
|
+
"index.html",
|
|
97
|
+
"<html>Static Template</html>",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("falls back to DEFAULT_MAIN_GROUP_INDEX_CONTENT when fetch fails", async () => {
|
|
102
|
+
mockGroupFileExists.mockResolvedValue(false);
|
|
103
|
+
mockWriteGroupFile.mockResolvedValue(undefined);
|
|
104
|
+
|
|
105
|
+
const res = await ensureMainGroupIndex({} as any, DEFAULT_GROUP_ID);
|
|
106
|
+
expect(res).toBe(true);
|
|
107
|
+
expect(mockWriteGroupFile).toHaveBeenCalledWith(
|
|
108
|
+
{},
|
|
109
|
+
DEFAULT_GROUP_ID,
|
|
110
|
+
"index.html",
|
|
111
|
+
DEFAULT_MAIN_GROUP_INDEX_CONTENT,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns false if writeGroupFile throws an error", async () => {
|
|
116
|
+
mockGroupFileExists.mockResolvedValue(false);
|
|
117
|
+
mockWriteGroupFile.mockRejectedValue(new Error("Storage write failure"));
|
|
118
|
+
|
|
119
|
+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
120
|
+
const res = await ensureMainGroupIndex({} as any, DEFAULT_GROUP_ID);
|
|
121
|
+
expect(res).toBe(false);
|
|
122
|
+
warnSpy.mockRestore();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("tests isMainGroupIndexSuppressed and setMainGroupIndexSuppressed", async () => {
|
|
126
|
+
mockGetConfig.mockResolvedValueOnce("true").mockResolvedValueOnce(false);
|
|
127
|
+
|
|
128
|
+
expect(await isMainGroupIndexSuppressed({} as any)).toBe(true);
|
|
129
|
+
expect(await isMainGroupIndexSuppressed({} as any)).toBe(false);
|
|
130
|
+
|
|
131
|
+
await setMainGroupIndexSuppressed({} as any, true);
|
|
132
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
133
|
+
{},
|
|
134
|
+
"main_group_index_suppressed",
|
|
135
|
+
true,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("resolves static main group index url with and without window.location.origin", () => {
|
|
140
|
+
const url = resolveStaticMainGroupIndexUrl();
|
|
141
|
+
expect(url).toContain(STATIC_MAIN_GROUP_INDEX_PATH);
|
|
142
|
+
|
|
143
|
+
const originalLocation = window.location;
|
|
144
|
+
try {
|
|
145
|
+
delete (window as any).location;
|
|
146
|
+
expect(resolveStaticMainGroupIndexUrl()).toContain(
|
|
147
|
+
STATIC_MAIN_GROUP_INDEX_PATH,
|
|
148
|
+
);
|
|
149
|
+
} finally {
|
|
150
|
+
(window as any).location = originalLocation;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
jest.unstable_mockModule("./getGroupDir.js", () => ({
|
|
4
|
+
getGroupDir: jest.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
jest.unstable_mockModule("./parsePath.js", () => ({
|
|
8
|
+
parsePath: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { getGroupFile } = await import("./getGroupFile.js");
|
|
12
|
+
const { getGroupDir } = await import("./getGroupDir.js");
|
|
13
|
+
const { parsePath } = await import("./parsePath.js");
|
|
14
|
+
|
|
15
|
+
describe("getGroupFile", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("retrieves native File object from nested directory structure", async () => {
|
|
21
|
+
const mockFile = new File(["test data"], "report.pdf", {
|
|
22
|
+
type: "application/pdf",
|
|
23
|
+
});
|
|
24
|
+
const fileHandle: any = {
|
|
25
|
+
getFile: (jest.fn() as any).mockResolvedValue(mockFile),
|
|
26
|
+
};
|
|
27
|
+
const subDir: any = {
|
|
28
|
+
getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
|
|
29
|
+
};
|
|
30
|
+
const rootDir: any = {
|
|
31
|
+
getDirectoryHandle: (jest.fn() as any).mockResolvedValue(subDir),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
(getGroupDir as any).mockResolvedValue(rootDir);
|
|
35
|
+
(parsePath as any).mockReturnValue({
|
|
36
|
+
dirs: ["docs"],
|
|
37
|
+
filename: "report.pdf",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const result = await getGroupFile({} as any, "group-1", "docs/report.pdf");
|
|
41
|
+
|
|
42
|
+
expect(getGroupDir).toHaveBeenCalledWith({}, "group-1");
|
|
43
|
+
expect(rootDir.getDirectoryHandle).toHaveBeenCalledWith("docs");
|
|
44
|
+
expect(subDir.getFileHandle).toHaveBeenCalledWith("report.pdf");
|
|
45
|
+
expect(fileHandle.getFile).toHaveBeenCalled();
|
|
46
|
+
expect(result).toBe(mockFile);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("retrieves native File from root workspace directory", async () => {
|
|
50
|
+
const mockFile = new File(["hello"], "README.md");
|
|
51
|
+
const fileHandle: any = {
|
|
52
|
+
getFile: (jest.fn() as any).mockResolvedValue(mockFile),
|
|
53
|
+
};
|
|
54
|
+
const rootDir: any = {
|
|
55
|
+
getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
(getGroupDir as any).mockResolvedValue(rootDir);
|
|
59
|
+
(parsePath as any).mockReturnValue({ dirs: [], filename: "README.md" });
|
|
60
|
+
|
|
61
|
+
const result = await getGroupFile({} as any, "group-2", "README.md");
|
|
62
|
+
|
|
63
|
+
expect(rootDir.getFileHandle).toHaveBeenCalledWith("README.md");
|
|
64
|
+
expect(result).toBe(mockFile);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MemoryDirectoryHandle,
|
|
3
3
|
MemoryFileHandle,
|
|
4
|
+
MemorySyncAccessHandle,
|
|
4
5
|
getMemoryOpfsRoot,
|
|
6
|
+
isMemoryStorageFallbackActive,
|
|
5
7
|
resetMemoryOpfsRoot,
|
|
6
8
|
} from "./memoryStorage.js";
|
|
7
9
|
|
|
@@ -65,6 +67,41 @@ describe("memoryStorage", () => {
|
|
|
65
67
|
expect(await file2.text()).toBe("AB");
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
it("supports MemorySyncAccessHandle read, write, truncate, getSize, flush and close", async () => {
|
|
71
|
+
const fileHandle = new MemoryFileHandle("sync.dat");
|
|
72
|
+
const syncHandle: MemorySyncAccessHandle =
|
|
73
|
+
await fileHandle.createSyncAccessHandle();
|
|
74
|
+
|
|
75
|
+
expect(syncHandle.getSize()).toBe(0);
|
|
76
|
+
|
|
77
|
+
const writeData = new Uint8Array([10, 20, 30, 40]);
|
|
78
|
+
const written = syncHandle.write(writeData, { at: 0 });
|
|
79
|
+
expect(written).toBe(4);
|
|
80
|
+
expect(syncHandle.getSize()).toBe(4);
|
|
81
|
+
|
|
82
|
+
syncHandle.flush(); // No-op
|
|
83
|
+
|
|
84
|
+
const readBuffer = new Uint8Array(4);
|
|
85
|
+
const readBytes = syncHandle.read(readBuffer, { at: 0 });
|
|
86
|
+
expect(readBytes).toBe(4);
|
|
87
|
+
expect(Array.from(readBuffer)).toEqual([10, 20, 30, 40]);
|
|
88
|
+
|
|
89
|
+
// Read past end
|
|
90
|
+
const emptyBuffer = new Uint8Array(2);
|
|
91
|
+
expect(syncHandle.read(emptyBuffer, { at: 100 })).toBe(0);
|
|
92
|
+
|
|
93
|
+
// Truncate
|
|
94
|
+
syncHandle.truncate(2);
|
|
95
|
+
expect(syncHandle.getSize()).toBe(2);
|
|
96
|
+
|
|
97
|
+
// Close and verify operations throw
|
|
98
|
+
syncHandle.close();
|
|
99
|
+
expect(() => syncHandle.getSize()).toThrow();
|
|
100
|
+
expect(() => syncHandle.read(readBuffer)).toThrow();
|
|
101
|
+
expect(() => syncHandle.write(writeData)).toThrow();
|
|
102
|
+
expect(() => syncHandle.truncate(1)).toThrow();
|
|
103
|
+
});
|
|
104
|
+
|
|
68
105
|
it("handles permissions and equality", async () => {
|
|
69
106
|
const fileHandle = new MemoryFileHandle("test.txt");
|
|
70
107
|
expect(await fileHandle.queryPermission()).toBe("granted");
|
|
@@ -94,6 +131,14 @@ describe("memoryStorage", () => {
|
|
|
94
131
|
expect(existingFile).toBe(file);
|
|
95
132
|
});
|
|
96
133
|
|
|
134
|
+
it("handles special directory names (. and ..)", async () => {
|
|
135
|
+
const dir = new MemoryDirectoryHandle("root");
|
|
136
|
+
const current = await dir.getDirectoryHandle(".");
|
|
137
|
+
expect(current).toBe(dir);
|
|
138
|
+
|
|
139
|
+
await expect(dir.getDirectoryHandle("..")).rejects.toThrow();
|
|
140
|
+
});
|
|
141
|
+
|
|
97
142
|
it("throws NotFoundError when entry does not exist and create is false", async () => {
|
|
98
143
|
const dir = new MemoryDirectoryHandle("root");
|
|
99
144
|
await expect(dir.getDirectoryHandle("missing")).rejects.toThrow();
|
|
@@ -109,6 +154,28 @@ describe("memoryStorage", () => {
|
|
|
109
154
|
await expect(dir.getDirectoryHandle("file.txt")).rejects.toThrow();
|
|
110
155
|
});
|
|
111
156
|
|
|
157
|
+
it("resolves descendant handle paths", async () => {
|
|
158
|
+
const root = new MemoryDirectoryHandle("root");
|
|
159
|
+
const folderA = await root.getDirectoryHandle("folderA", {
|
|
160
|
+
create: true,
|
|
161
|
+
});
|
|
162
|
+
const folderB = await folderA.getDirectoryHandle("folderB", {
|
|
163
|
+
create: true,
|
|
164
|
+
});
|
|
165
|
+
const file = await folderB.getFileHandle("item.txt", { create: true });
|
|
166
|
+
|
|
167
|
+
expect(await root.resolve(root)).toEqual([]);
|
|
168
|
+
expect(await root.resolve(folderA)).toEqual(["folderA"]);
|
|
169
|
+
expect(await root.resolve(file)).toEqual([
|
|
170
|
+
"folderA",
|
|
171
|
+
"folderB",
|
|
172
|
+
"item.txt",
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const unrelated = new MemoryFileHandle("unrelated.txt");
|
|
176
|
+
expect(await root.resolve(unrelated)).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
112
179
|
it("iterates entries, keys, and values", async () => {
|
|
113
180
|
const dir = new MemoryDirectoryHandle("root");
|
|
114
181
|
await dir.getDirectoryHandle("b_dir", { create: true });
|
|
@@ -131,6 +198,13 @@ describe("memoryStorage", () => {
|
|
|
131
198
|
values.push(v.name);
|
|
132
199
|
}
|
|
133
200
|
expect(values).toEqual(["b_dir", "a_file.txt"]);
|
|
201
|
+
|
|
202
|
+
// async iterator fallback
|
|
203
|
+
const directEntries: string[] = [];
|
|
204
|
+
for await (const [name, handle] of dir) {
|
|
205
|
+
directEntries.push(`${name}:${handle.kind}`);
|
|
206
|
+
}
|
|
207
|
+
expect(directEntries).toEqual(["b_dir:directory", "a_file.txt:file"]);
|
|
134
208
|
});
|
|
135
209
|
|
|
136
210
|
it("removes files and empty/recursive directories", async () => {
|
|
@@ -149,12 +223,18 @@ describe("memoryStorage", () => {
|
|
|
149
223
|
});
|
|
150
224
|
});
|
|
151
225
|
|
|
152
|
-
describe("getMemoryOpfsRoot", () => {
|
|
153
|
-
it("returns a singleton OPFS root child", () => {
|
|
226
|
+
describe("getMemoryOpfsRoot & isMemoryStorageFallbackActive", () => {
|
|
227
|
+
it("returns a singleton OPFS root child and reports fallback active", () => {
|
|
228
|
+
expect(isMemoryStorageFallbackActive()).toBe(false);
|
|
154
229
|
const root1 = getMemoryOpfsRoot();
|
|
230
|
+
expect(isMemoryStorageFallbackActive()).toBe(true);
|
|
231
|
+
|
|
155
232
|
const root2 = getMemoryOpfsRoot();
|
|
156
233
|
expect(root1).toBe(root2);
|
|
157
234
|
expect(root1.name).toBeTruthy();
|
|
235
|
+
|
|
236
|
+
resetMemoryOpfsRoot();
|
|
237
|
+
expect(isMemoryStorageFallbackActive()).toBe(false);
|
|
158
238
|
});
|
|
159
239
|
});
|
|
160
240
|
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { CONFIG_KEYS, LEGACY_OPFS_ROOT } from "../config/config.js";
|
|
3
|
+
import type { ShadowClawDatabase } from "../db/types.js";
|
|
4
|
+
|
|
5
|
+
const mockGetConfig = jest.fn() as any;
|
|
6
|
+
const mockSetConfig = jest.fn() as any;
|
|
7
|
+
const mockGetOpfsRootDirName = jest.fn() as any;
|
|
8
|
+
const mockWriteFileHandle = jest.fn() as any;
|
|
9
|
+
const mockWriteOpfsPathViaWorker = jest.fn() as any;
|
|
10
|
+
|
|
11
|
+
jest.unstable_mockModule("../db/getConfig.js", () => ({
|
|
12
|
+
getConfig: mockGetConfig,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.unstable_mockModule("../db/setConfig.js", () => ({
|
|
16
|
+
setConfig: mockSetConfig,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.unstable_mockModule("./storage.js", () => ({
|
|
20
|
+
getOpfsRootDirName: mockGetOpfsRootDirName,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.unstable_mockModule("./writeFileHandle.js", () => ({
|
|
24
|
+
writeFileHandle: mockWriteFileHandle,
|
|
25
|
+
writeOpfsPathViaWorker: mockWriteOpfsPathViaWorker,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const { migrateLegacyOpfs } = await import("./migrateLegacyOpfs.js");
|
|
29
|
+
|
|
30
|
+
describe("migrateLegacyOpfs", () => {
|
|
31
|
+
const originalNavigator = globalThis.navigator;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
mockGetConfig.mockResolvedValue(null);
|
|
36
|
+
mockSetConfig.mockResolvedValue(undefined);
|
|
37
|
+
mockGetOpfsRootDirName.mockReturnValue("shadowclaw-shadow-claw");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
42
|
+
value: originalNavigator,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("skips migration if already completed", async () => {
|
|
49
|
+
mockGetConfig.mockResolvedValue("true");
|
|
50
|
+
const mockDb = {} as ShadowClawDatabase;
|
|
51
|
+
|
|
52
|
+
await migrateLegacyOpfs(mockDb);
|
|
53
|
+
|
|
54
|
+
expect(mockGetConfig).toHaveBeenCalledWith(
|
|
55
|
+
mockDb,
|
|
56
|
+
CONFIG_KEYS.OPFS_MIGRATED_FROM_LEGACY,
|
|
57
|
+
);
|
|
58
|
+
expect(mockSetConfig).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("skips migration if target root is the legacy root", async () => {
|
|
62
|
+
mockGetOpfsRootDirName.mockReturnValue(LEGACY_OPFS_ROOT);
|
|
63
|
+
const mockDb = {} as ShadowClawDatabase;
|
|
64
|
+
|
|
65
|
+
await migrateLegacyOpfs(mockDb);
|
|
66
|
+
|
|
67
|
+
expect(mockSetConfig).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("handles when navigator.storage is undefined", async () => {
|
|
71
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
72
|
+
value: {},
|
|
73
|
+
configurable: true,
|
|
74
|
+
writable: true,
|
|
75
|
+
});
|
|
76
|
+
const mockDb = {} as ShadowClawDatabase;
|
|
77
|
+
|
|
78
|
+
await expect(migrateLegacyOpfs(mockDb)).resolves.toBeUndefined();
|
|
79
|
+
expect(mockSetConfig).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("handles when legacy OPFS directory does not exist and sets flag", async () => {
|
|
83
|
+
const mockOpfsRoot: any = {
|
|
84
|
+
getDirectoryHandle: (jest.fn() as any).mockImplementation(
|
|
85
|
+
(name: string) => {
|
|
86
|
+
if (name === LEGACY_OPFS_ROOT) {
|
|
87
|
+
throw new DOMException("Directory not found", "NotFoundError");
|
|
88
|
+
}
|
|
89
|
+
return Promise.resolve({});
|
|
90
|
+
},
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
95
|
+
value: {
|
|
96
|
+
storage: {
|
|
97
|
+
getDirectory: (jest.fn() as any).mockResolvedValue(mockOpfsRoot),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
configurable: true,
|
|
101
|
+
writable: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const mockDb = {} as ShadowClawDatabase;
|
|
105
|
+
await migrateLegacyOpfs(mockDb);
|
|
106
|
+
|
|
107
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
108
|
+
mockDb,
|
|
109
|
+
CONFIG_KEYS.OPFS_MIGRATED_FROM_LEGACY,
|
|
110
|
+
"true",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("recursively copies legacy files and directories into the namespaced target root", async () => {
|
|
115
|
+
const mockFile1 = new File(["notes content"], "note.md", {
|
|
116
|
+
type: "text/markdown",
|
|
117
|
+
});
|
|
118
|
+
const mockFile2 = new File(["memory content"], "MEMORY.md", {
|
|
119
|
+
type: "text/markdown",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const mockNotesWorkspaceDir = {
|
|
123
|
+
entries: async function* () {
|
|
124
|
+
yield [
|
|
125
|
+
"note.md",
|
|
126
|
+
{
|
|
127
|
+
kind: "file",
|
|
128
|
+
getFile: jest.fn(async () => mockFile1),
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const mockNotesGroupDir = {
|
|
135
|
+
entries: async function* () {
|
|
136
|
+
yield [
|
|
137
|
+
"workspace",
|
|
138
|
+
{
|
|
139
|
+
kind: "directory",
|
|
140
|
+
...mockNotesWorkspaceDir,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const mockMainWorkspaceDir = {
|
|
147
|
+
entries: async function* () {
|
|
148
|
+
yield [
|
|
149
|
+
"MEMORY.md",
|
|
150
|
+
{
|
|
151
|
+
kind: "file",
|
|
152
|
+
getFile: jest.fn(async () => mockFile2),
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const mockMainGroupDir = {
|
|
159
|
+
entries: async function* () {
|
|
160
|
+
yield [
|
|
161
|
+
"workspace",
|
|
162
|
+
{
|
|
163
|
+
kind: "directory",
|
|
164
|
+
...mockMainWorkspaceDir,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const mockLegacyGroupsDir = {
|
|
171
|
+
entries: async function* () {
|
|
172
|
+
yield [
|
|
173
|
+
"br-notes",
|
|
174
|
+
{
|
|
175
|
+
kind: "directory",
|
|
176
|
+
...mockNotesGroupDir,
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
yield [
|
|
180
|
+
"br-main",
|
|
181
|
+
{
|
|
182
|
+
kind: "directory",
|
|
183
|
+
...mockMainGroupDir,
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const mockLegacyRoot = {
|
|
190
|
+
entries: async function* () {
|
|
191
|
+
yield [
|
|
192
|
+
"groups",
|
|
193
|
+
{
|
|
194
|
+
kind: "directory",
|
|
195
|
+
...mockLegacyGroupsDir,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const createdDirs = new Map<string, any>();
|
|
202
|
+
const createdFiles = new Map<string, any>();
|
|
203
|
+
|
|
204
|
+
function createMockTargetDir(path: string) {
|
|
205
|
+
const dirHandle = {
|
|
206
|
+
name: path.split("/").pop() || "",
|
|
207
|
+
getDirectoryHandle: (jest.fn() as any).mockImplementation(
|
|
208
|
+
async (name: string) => {
|
|
209
|
+
const childPath = `${path}/${name}`;
|
|
210
|
+
if (!createdDirs.has(childPath)) {
|
|
211
|
+
createdDirs.set(childPath, createMockTargetDir(childPath));
|
|
212
|
+
}
|
|
213
|
+
return createdDirs.get(childPath);
|
|
214
|
+
},
|
|
215
|
+
),
|
|
216
|
+
getFileHandle: (jest.fn() as any).mockImplementation(
|
|
217
|
+
async (name: string) => {
|
|
218
|
+
const filePath = `${path}/${name}`;
|
|
219
|
+
if (!createdFiles.has(filePath)) {
|
|
220
|
+
createdFiles.set(filePath, { name, path: filePath });
|
|
221
|
+
}
|
|
222
|
+
return createdFiles.get(filePath);
|
|
223
|
+
},
|
|
224
|
+
),
|
|
225
|
+
};
|
|
226
|
+
return dirHandle;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const mockTargetRoot = createMockTargetDir("shadowclaw-shadow-claw");
|
|
230
|
+
|
|
231
|
+
const mockOpfsRoot = {
|
|
232
|
+
getDirectoryHandle: (jest.fn() as any).mockImplementation(
|
|
233
|
+
async (name: string) => {
|
|
234
|
+
if (name === LEGACY_OPFS_ROOT) {
|
|
235
|
+
return mockLegacyRoot;
|
|
236
|
+
}
|
|
237
|
+
if (name === "shadowclaw-shadow-claw") {
|
|
238
|
+
return mockTargetRoot;
|
|
239
|
+
}
|
|
240
|
+
throw new DOMException("Not found", "NotFoundError");
|
|
241
|
+
},
|
|
242
|
+
),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
246
|
+
value: {
|
|
247
|
+
storage: {
|
|
248
|
+
getDirectory: (jest.fn() as any).mockResolvedValue(mockOpfsRoot),
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
configurable: true,
|
|
252
|
+
writable: true,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const mockDb = {} as ShadowClawDatabase;
|
|
256
|
+
await migrateLegacyOpfs(mockDb);
|
|
257
|
+
|
|
258
|
+
expect(mockWriteFileHandle).toHaveBeenCalledTimes(2);
|
|
259
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
260
|
+
mockDb,
|
|
261
|
+
CONFIG_KEYS.OPFS_MIGRATED_FROM_LEGACY,
|
|
262
|
+
"true",
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("falls back to writeOpfsPathViaWorker on Safari/iPad when writeFileHandle throws unsupported", async () => {
|
|
267
|
+
const mockFile = new File(["test content"], "doc.txt", {
|
|
268
|
+
type: "text/plain",
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const mockLegacyRoot = {
|
|
272
|
+
entries: async function* () {
|
|
273
|
+
yield [
|
|
274
|
+
"doc.txt",
|
|
275
|
+
{
|
|
276
|
+
kind: "file",
|
|
277
|
+
getFile: jest.fn(async () => mockFile),
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const mockTargetFile = { name: "doc.txt" };
|
|
284
|
+
const mockTargetRoot = {
|
|
285
|
+
getDirectoryHandle: jest.fn(),
|
|
286
|
+
getFileHandle: (jest.fn() as any).mockResolvedValue(mockTargetFile),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const mockOpfsRoot = {
|
|
290
|
+
getDirectoryHandle: (jest.fn() as any).mockImplementation(
|
|
291
|
+
async (name: string) => {
|
|
292
|
+
if (name === LEGACY_OPFS_ROOT) return mockLegacyRoot;
|
|
293
|
+
if (name === "shadowclaw-shadow-claw") return mockTargetRoot;
|
|
294
|
+
throw new DOMException("Not found", "NotFoundError");
|
|
295
|
+
},
|
|
296
|
+
),
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
300
|
+
value: {
|
|
301
|
+
storage: {
|
|
302
|
+
getDirectory: (jest.fn() as any).mockResolvedValue(mockOpfsRoot),
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
configurable: true,
|
|
306
|
+
writable: true,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
mockWriteFileHandle.mockRejectedValueOnce(
|
|
310
|
+
new Error("Writable file streams are not supported"),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const mockDb = {} as ShadowClawDatabase;
|
|
314
|
+
await migrateLegacyOpfs(mockDb);
|
|
315
|
+
|
|
316
|
+
expect(mockWriteFileHandle).toHaveBeenCalledTimes(1);
|
|
317
|
+
expect(mockWriteOpfsPathViaWorker).toHaveBeenCalledWith(
|
|
318
|
+
["shadowclaw-shadow-claw", "doc.txt"],
|
|
319
|
+
mockFile,
|
|
320
|
+
);
|
|
321
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
322
|
+
mockDb,
|
|
323
|
+
CONFIG_KEYS.OPFS_MIGRATED_FROM_LEGACY,
|
|
324
|
+
"true",
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
});
|