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,117 @@
|
|
|
1
|
+
import { CONFIG_KEYS, LEGACY_OPFS_ROOT } from "../config/config.js";
|
|
2
|
+
import { getConfig } from "../db/getConfig.js";
|
|
3
|
+
import { setConfig } from "../db/setConfig.js";
|
|
4
|
+
import { getOpfsRootDirName } from "./storage.js";
|
|
5
|
+
import { writeFileHandle, writeOpfsPathViaWorker } from "./writeFileHandle.js";
|
|
6
|
+
import type { ShadowClawDatabase } from "../db/types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Recursively copy entries from a source OPFS directory to a target OPFS directory.
|
|
10
|
+
*/
|
|
11
|
+
async function copyOpfsEntriesRecursively(
|
|
12
|
+
sourceDir: FileSystemDirectoryHandle,
|
|
13
|
+
targetDir: FileSystemDirectoryHandle,
|
|
14
|
+
pathSegments: string[],
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
for await (const [name, handle] of (sourceDir as any).entries()) {
|
|
17
|
+
if (handle.kind === "directory") {
|
|
18
|
+
const nextTargetDir = await targetDir.getDirectoryHandle(name, {
|
|
19
|
+
create: true,
|
|
20
|
+
});
|
|
21
|
+
await copyOpfsEntriesRecursively(handle, nextTargetDir, [
|
|
22
|
+
...pathSegments,
|
|
23
|
+
name,
|
|
24
|
+
]);
|
|
25
|
+
} else if (handle.kind === "file") {
|
|
26
|
+
const file = await handle.getFile();
|
|
27
|
+
const targetFileHandle = await targetDir.getFileHandle(name, {
|
|
28
|
+
create: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await writeFileHandle(targetFileHandle, file);
|
|
33
|
+
} catch (writeErr) {
|
|
34
|
+
const message =
|
|
35
|
+
writeErr instanceof Error ? writeErr.message : String(writeErr);
|
|
36
|
+
const needsOpfsWorkerFallback = message.includes(
|
|
37
|
+
"Writable file streams are not supported",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (!needsOpfsWorkerFallback) {
|
|
41
|
+
throw writeErr;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await writeOpfsPathViaWorker([...pathSegments, name], file);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Perform a one-time migration of OPFS workspace files and directories from the
|
|
52
|
+
* legacy unnamespaced root directory ("shadowclaw") to the active namespaced
|
|
53
|
+
* deployment root directory if not already done.
|
|
54
|
+
*/
|
|
55
|
+
export async function migrateLegacyOpfs(db: ShadowClawDatabase): Promise<void> {
|
|
56
|
+
if (!db) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const alreadyMigrated = await getConfig(
|
|
62
|
+
db,
|
|
63
|
+
CONFIG_KEYS.OPFS_MIGRATED_FROM_LEGACY,
|
|
64
|
+
);
|
|
65
|
+
if (alreadyMigrated === "true") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// ignore read error
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const targetRootName = getOpfsRootDirName();
|
|
73
|
+
if (targetRootName === LEGACY_OPFS_ROOT) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
typeof navigator === "undefined" ||
|
|
79
|
+
!navigator.storage ||
|
|
80
|
+
typeof navigator.storage.getDirectory !== "function"
|
|
81
|
+
) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const opfsRoot = await navigator.storage.getDirectory();
|
|
87
|
+
let legacyRootHandle: FileSystemDirectoryHandle;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
legacyRootHandle = await opfsRoot.getDirectoryHandle(LEGACY_OPFS_ROOT);
|
|
91
|
+
} catch {
|
|
92
|
+
// Legacy OPFS folder does not exist (fresh installation or already cleaned up)
|
|
93
|
+
try {
|
|
94
|
+
await setConfig(db, CONFIG_KEYS.OPFS_MIGRATED_FROM_LEGACY, "true");
|
|
95
|
+
} catch {
|
|
96
|
+
// ignore write error
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const targetRootHandle = await opfsRoot.getDirectoryHandle(targetRootName, {
|
|
102
|
+
create: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await copyOpfsEntriesRecursively(legacyRootHandle, targetRootHandle, [
|
|
106
|
+
targetRootName,
|
|
107
|
+
]);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.warn("Legacy OPFS migration warning:", err);
|
|
110
|
+
} finally {
|
|
111
|
+
try {
|
|
112
|
+
await setConfig(db, CONFIG_KEYS.OPFS_MIGRATED_FROM_LEGACY, "true");
|
|
113
|
+
} catch {
|
|
114
|
+
// ignore write error
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
jest.unstable_mockModule("./getGroupDir.js", () => ({
|
|
4
|
+
getGroupDir: jest.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
jest.unstable_mockModule("./parsePath.js", () => ({ parsePath: jest.fn() }));
|
|
8
|
+
|
|
9
|
+
jest.unstable_mockModule("./storage.js", () => ({
|
|
10
|
+
getOpfsRootDirName: jest.fn().mockReturnValue("shadowclaw"),
|
|
11
|
+
getStorageStatus: (jest.fn() as any).mockResolvedValue({
|
|
12
|
+
type: "opfs",
|
|
13
|
+
permission: "granted",
|
|
14
|
+
name: "OPFS",
|
|
15
|
+
}),
|
|
16
|
+
invalidateStorageRoot: jest.fn(),
|
|
17
|
+
isStaleHandleError: jest.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.unstable_mockModule("./writeFileHandle.js", () => ({
|
|
21
|
+
writeFileHandle: (jest.fn() as any).mockResolvedValue(undefined),
|
|
22
|
+
writeOpfsPathViaWorker: (jest.fn() as any).mockResolvedValue(undefined),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const { writeGroupFileBytes } = await import("./writeGroupFileBytes.js");
|
|
26
|
+
const { getGroupDir } = await import("./getGroupDir.js");
|
|
27
|
+
const { parsePath } = await import("./parsePath.js");
|
|
28
|
+
const { getStorageStatus, invalidateStorageRoot, isStaleHandleError } =
|
|
29
|
+
await import("./storage.js");
|
|
30
|
+
const { writeFileHandle, writeOpfsPathViaWorker } =
|
|
31
|
+
await import("./writeFileHandle.js");
|
|
32
|
+
|
|
33
|
+
describe("writeGroupFileBytes", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
(isStaleHandleError as any).mockReturnValue(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("writes binary Uint8Array content to target file", async () => {
|
|
40
|
+
const fileHandle: any = {};
|
|
41
|
+
const nested: any = {
|
|
42
|
+
getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
|
|
43
|
+
};
|
|
44
|
+
const root: any = {
|
|
45
|
+
getDirectoryHandle: (jest.fn() as any).mockResolvedValue(nested),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
(getGroupDir as any).mockResolvedValue(root);
|
|
49
|
+
(parsePath as any).mockReturnValue({
|
|
50
|
+
dirs: ["images"],
|
|
51
|
+
filename: "photo.png",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const content = new Uint8Array([1, 2, 3, 4]);
|
|
55
|
+
await writeGroupFileBytes({} as any, "g1", "images/photo.png", content);
|
|
56
|
+
|
|
57
|
+
expect(root.getDirectoryHandle).toHaveBeenCalledWith("images", {
|
|
58
|
+
create: true,
|
|
59
|
+
});
|
|
60
|
+
expect(nested.getFileHandle).toHaveBeenCalledWith("photo.png", {
|
|
61
|
+
create: true,
|
|
62
|
+
});
|
|
63
|
+
expect(writeFileHandle).toHaveBeenCalledWith(fileHandle, content);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("falls back to OPFS worker write when stream writing is unsupported in OPFS", async () => {
|
|
67
|
+
const fileHandle: any = {};
|
|
68
|
+
const nested: any = {
|
|
69
|
+
getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
|
|
70
|
+
};
|
|
71
|
+
const root: any = {
|
|
72
|
+
getDirectoryHandle: (jest.fn() as any).mockResolvedValue(nested),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
(getGroupDir as any).mockResolvedValue(root);
|
|
76
|
+
(parsePath as any).mockReturnValue({ dirs: ["sub"], filename: "bin.dat" });
|
|
77
|
+
|
|
78
|
+
(writeFileHandle as any).mockRejectedValueOnce(
|
|
79
|
+
new Error(
|
|
80
|
+
"Writable file streams are not supported by this browser/storage backend.",
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const content = new Uint8Array([10, 20]);
|
|
85
|
+
await writeGroupFileBytes(
|
|
86
|
+
{} as any,
|
|
87
|
+
"peer:abc:123",
|
|
88
|
+
"sub/bin.dat",
|
|
89
|
+
content,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(getStorageStatus).toHaveBeenCalled();
|
|
93
|
+
expect(writeOpfsPathViaWorker).toHaveBeenCalledWith(
|
|
94
|
+
["shadowclaw", "groups", "peer-abc-123", "sub", "bin.dat"],
|
|
95
|
+
content,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("retries on stale handle error", async () => {
|
|
100
|
+
const fileHandle: any = {};
|
|
101
|
+
const root: any = {
|
|
102
|
+
getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
(parsePath as any).mockReturnValue({ dirs: [], filename: "file.txt" });
|
|
106
|
+
(isStaleHandleError as any)
|
|
107
|
+
.mockReturnValueOnce(true)
|
|
108
|
+
.mockReturnValue(false);
|
|
109
|
+
|
|
110
|
+
(getGroupDir as any)
|
|
111
|
+
.mockRejectedValueOnce(new Error("Stale handle"))
|
|
112
|
+
.mockResolvedValueOnce(root);
|
|
113
|
+
|
|
114
|
+
const content = new Uint8Array([5, 6]);
|
|
115
|
+
await writeGroupFileBytes({} as any, "g1", "file.txt", content);
|
|
116
|
+
|
|
117
|
+
expect(invalidateStorageRoot).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(writeFileHandle).toHaveBeenCalledWith(fileHandle, content);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("throws error when non-OPFS storage backend fails with unsupported stream error", async () => {
|
|
122
|
+
const fileHandle: any = {};
|
|
123
|
+
const root: any = {
|
|
124
|
+
getFileHandle: (jest.fn() as any).mockResolvedValue(fileHandle),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
(getGroupDir as any).mockResolvedValue(root);
|
|
128
|
+
(parsePath as any).mockReturnValue({ dirs: [], filename: "file.txt" });
|
|
129
|
+
(getStorageStatus as any).mockResolvedValueOnce({
|
|
130
|
+
type: "local",
|
|
131
|
+
permission: "granted",
|
|
132
|
+
name: "Folder",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
(writeFileHandle as any).mockRejectedValueOnce(
|
|
136
|
+
new Error("Writable file streams are not supported"),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await expect(
|
|
140
|
+
writeGroupFileBytes({} as any, "g1", "file.txt", new Uint8Array([1])),
|
|
141
|
+
).rejects.toThrow("Writable file streams are not supported");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -31,4 +31,24 @@ describe("ChatUiStore", () => {
|
|
|
31
31
|
|
|
32
32
|
(URL as any).revokeObjectURL = originalRevoke;
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
it("manages group scroll states and snapshots", () => {
|
|
36
|
+
const store = new ChatUiStore();
|
|
37
|
+
|
|
38
|
+
expect(store.getGroupScrollState("group-1")).toBeNull();
|
|
39
|
+
|
|
40
|
+
store.setGroupScrollState("group-1", 120, false);
|
|
41
|
+
expect(store.getGroupScrollState("group-1")).toEqual({
|
|
42
|
+
distanceFromBottom: 120,
|
|
43
|
+
nearBottom: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
store.setNearBottom(false);
|
|
47
|
+
expect(store.nearBottomSnapshot).toBe(false);
|
|
48
|
+
|
|
49
|
+
store.reset();
|
|
50
|
+
expect(store.getGroupScrollState("group-1")).toBeNull();
|
|
51
|
+
expect(store.isNearBottom).toBe(true);
|
|
52
|
+
expect(store.nearBottomSnapshot).toBe(true);
|
|
53
|
+
});
|
|
34
54
|
});
|
|
@@ -2887,5 +2887,139 @@ describe("OrchestratorStore", () => {
|
|
|
2887
2887
|
configElement.remove();
|
|
2888
2888
|
}
|
|
2889
2889
|
});
|
|
2890
|
+
|
|
2891
|
+
it("handles sendMessage with and without a2uiAction", async () => {
|
|
2892
|
+
const store = new OrchestratorStore();
|
|
2893
|
+
const mockSubmit = jest.fn();
|
|
2894
|
+
const orch: any = {
|
|
2895
|
+
browserChat: { submit: mockSubmit },
|
|
2896
|
+
events: createEvents(),
|
|
2897
|
+
};
|
|
2898
|
+
await store.init({} as any, orch);
|
|
2899
|
+
|
|
2900
|
+
store.sendMessage("plain message", []);
|
|
2901
|
+
expect(mockSubmit).toHaveBeenCalledWith(
|
|
2902
|
+
"plain message",
|
|
2903
|
+
DEFAULT_GROUP_ID,
|
|
2904
|
+
[],
|
|
2905
|
+
);
|
|
2906
|
+
|
|
2907
|
+
const action = {
|
|
2908
|
+
actionId: "btn-click",
|
|
2909
|
+
surfaceId: "surf-1",
|
|
2910
|
+
componentId: "btn-1",
|
|
2911
|
+
};
|
|
2912
|
+
store.sendMessage("action message", [], action as any);
|
|
2913
|
+
expect(mockSubmit).toHaveBeenCalledWith(
|
|
2914
|
+
"action message",
|
|
2915
|
+
DEFAULT_GROUP_ID,
|
|
2916
|
+
[],
|
|
2917
|
+
action,
|
|
2918
|
+
);
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
it("manages remote agent status, typing status, and ready state", () => {
|
|
2922
|
+
const store = new OrchestratorStore();
|
|
2923
|
+
expect(store.ready).toBe(false);
|
|
2924
|
+
|
|
2925
|
+
store.setReady(true);
|
|
2926
|
+
expect(store.ready).toBe(true);
|
|
2927
|
+
|
|
2928
|
+
store.setRemoteAgentStatus("peer:agent-1", "running" as any);
|
|
2929
|
+
expect(store._remoteAgentStatusByGroup.get().get("peer:agent-1")).toBe(
|
|
2930
|
+
"running",
|
|
2931
|
+
);
|
|
2932
|
+
|
|
2933
|
+
store.setRemoteAgentTyping("peer:agent-1", true);
|
|
2934
|
+
expect(store._remoteAgentTypingByGroup.get().get("peer:agent-1")).toBe(
|
|
2935
|
+
true,
|
|
2936
|
+
);
|
|
2937
|
+
|
|
2938
|
+
store.setRemoteAgentTyping("peer:agent-1", false);
|
|
2939
|
+
expect(
|
|
2940
|
+
store._remoteAgentTypingByGroup.get().get("peer:agent-1"),
|
|
2941
|
+
).toBeUndefined();
|
|
2942
|
+
});
|
|
2943
|
+
|
|
2944
|
+
it("handles folder navigation and proxy/network settings", async () => {
|
|
2945
|
+
const store = new OrchestratorStore();
|
|
2946
|
+
const mockDb: any = {};
|
|
2947
|
+
const orch: any = {
|
|
2948
|
+
browserChat: { submit: jest.fn() },
|
|
2949
|
+
events: createEvents(),
|
|
2950
|
+
};
|
|
2951
|
+
await store.init(mockDb, orch);
|
|
2952
|
+
|
|
2953
|
+
// 1. Folder navigation
|
|
2954
|
+
await store.navigateIntoFolder(mockDb, "docs");
|
|
2955
|
+
expect(store.currentPath).toBe("docs");
|
|
2956
|
+
|
|
2957
|
+
await store.navigateIntoFolder(mockDb, "sub");
|
|
2958
|
+
expect(store.currentPath).toBe("docs/sub");
|
|
2959
|
+
|
|
2960
|
+
await store.navigateBackFolder(mockDb);
|
|
2961
|
+
expect(store.currentPath).toBe("docs");
|
|
2962
|
+
|
|
2963
|
+
await store.resetToRootFolder(mockDb);
|
|
2964
|
+
expect(store.currentPath).toBe(".");
|
|
2965
|
+
|
|
2966
|
+
// 2. Proxy & network settings
|
|
2967
|
+
await store.setGitProxyUrl(mockDb, "https://git-proxy.example.com");
|
|
2968
|
+
expect(store.gitProxyUrl).toBe("https://git-proxy.example.com");
|
|
2969
|
+
|
|
2970
|
+
await store.setProxyUrl(mockDb, "https://proxy.example.com");
|
|
2971
|
+
expect(store.proxyUrl).toBe("https://proxy.example.com");
|
|
2972
|
+
|
|
2973
|
+
await store.setUseProxy(mockDb, true);
|
|
2974
|
+
expect(store.useProxy).toBe(true);
|
|
2975
|
+
|
|
2976
|
+
await store.setVMBashFullInternetAccess(mockDb, true);
|
|
2977
|
+
expect(store.vmBashFullInternetAccess).toBe(true);
|
|
2978
|
+
});
|
|
2979
|
+
|
|
2980
|
+
it("handles task management helpers (clearAllTasks, restoreTasksFromBackup, toggleTask)", async () => {
|
|
2981
|
+
const store = new OrchestratorStore();
|
|
2982
|
+
const mockDb: any = {};
|
|
2983
|
+
const orch: any = {
|
|
2984
|
+
browserChat: { submit: jest.fn() },
|
|
2985
|
+
events: createEvents(),
|
|
2986
|
+
};
|
|
2987
|
+
await store.init(mockDb, orch);
|
|
2988
|
+
|
|
2989
|
+
const task: any = {
|
|
2990
|
+
id: "task-123",
|
|
2991
|
+
groupId: DEFAULT_GROUP_ID,
|
|
2992
|
+
name: "Test Task",
|
|
2993
|
+
enabled: true,
|
|
2994
|
+
};
|
|
2995
|
+
mockGetAllTasks.mockResolvedValue([task]);
|
|
2996
|
+
await store.loadTasks(mockDb);
|
|
2997
|
+
expect(store.tasks.length).toBe(1);
|
|
2998
|
+
|
|
2999
|
+
// 1. toggleTask
|
|
3000
|
+
mockGetAllTasks.mockResolvedValue([{ ...task, enabled: false }]);
|
|
3001
|
+
await store.toggleTask(mockDb, task, false);
|
|
3002
|
+
expect(store.tasks.find((t) => t.id === "task-123")?.enabled).toBe(false);
|
|
3003
|
+
|
|
3004
|
+
// 2. restoreTasksFromBackup
|
|
3005
|
+
mockGetAllTasks.mockResolvedValue([
|
|
3006
|
+
task,
|
|
3007
|
+
{
|
|
3008
|
+
id: "task-new",
|
|
3009
|
+
groupId: DEFAULT_GROUP_ID,
|
|
3010
|
+
name: "New Task",
|
|
3011
|
+
enabled: true,
|
|
3012
|
+
},
|
|
3013
|
+
]);
|
|
3014
|
+
await store.restoreTasksFromBackup(mockDb, [
|
|
3015
|
+
{ id: "task-new", name: "New Task", enabled: true } as any,
|
|
3016
|
+
]);
|
|
3017
|
+
expect(store.tasks.some((t) => t.id === "task-new")).toBe(true);
|
|
3018
|
+
|
|
3019
|
+
// 3. clearAllTasks
|
|
3020
|
+
mockGetAllTasks.mockResolvedValue([]);
|
|
3021
|
+
await store.clearAllTasks(mockDb);
|
|
3022
|
+
expect(store.tasks).toEqual([]);
|
|
3023
|
+
});
|
|
2890
3024
|
});
|
|
2891
3025
|
});
|
package/src/stores/toast.test.ts
CHANGED
|
@@ -67,4 +67,54 @@ describe("ToastStore", () => {
|
|
|
67
67
|
|
|
68
68
|
expect(store.toasts).toHaveLength(0);
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
test("clear removes all toasts and timers", () => {
|
|
72
|
+
const store = new ToastStore();
|
|
73
|
+
store.show("toast 1");
|
|
74
|
+
store.show("toast 2");
|
|
75
|
+
expect(store.toasts).toHaveLength(2);
|
|
76
|
+
|
|
77
|
+
store.clear();
|
|
78
|
+
expect(store.toasts).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("caps visible toasts at MAX_VISIBLE_TOASTS", () => {
|
|
82
|
+
const store = new ToastStore();
|
|
83
|
+
for (let i = 1; i <= 7; i++) {
|
|
84
|
+
store.show(`toast ${i}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
expect(store.toasts).toHaveLength(5);
|
|
88
|
+
expect(store.toasts[0].message).toBe("toast 3");
|
|
89
|
+
expect(store.toasts[4].message).toBe("toast 7");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("executes action via runAction", async () => {
|
|
93
|
+
const store = new ToastStore();
|
|
94
|
+
const actionSpy: any = jest.fn();
|
|
95
|
+
const id = store.show("with action", {
|
|
96
|
+
action: { label: "Retry", onClick: actionSpy },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await store.runAction(id);
|
|
100
|
+
expect(actionSpy).toHaveBeenCalledTimes(1);
|
|
101
|
+
|
|
102
|
+
// No-op for non-existent or action-less toast
|
|
103
|
+
await store.runAction(999);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("resolvers handle invalid inputs gracefully", () => {
|
|
107
|
+
const store = new ToastStore();
|
|
108
|
+
expect(store.resolveType("invalid" as any)).toBe("info");
|
|
109
|
+
expect(store.resolveType("error")).toBe("error");
|
|
110
|
+
|
|
111
|
+
expect(store.resolveDuration(-10)).toBe(4000);
|
|
112
|
+
expect(store.resolveDuration(NaN)).toBe(4000);
|
|
113
|
+
expect(store.resolveDuration(2500)).toBe(2500);
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
store.resolveAction({ label: "", onClick: "not-fn" } as any),
|
|
117
|
+
).toBeUndefined();
|
|
118
|
+
expect(store.resolveAction(undefined)).toBeUndefined();
|
|
119
|
+
});
|
|
70
120
|
});
|
package/src/stores/tools.test.ts
CHANGED
|
@@ -2,8 +2,11 @@ import { jest } from "@jest/globals";
|
|
|
2
2
|
|
|
3
3
|
const mockGetConfig = jest.fn();
|
|
4
4
|
const mockSetConfig = jest.fn();
|
|
5
|
+
const mockLoadDeclarativeTools = jest.fn();
|
|
6
|
+
const mockFindDeclarativeTool = jest.fn();
|
|
7
|
+
const mockParseDeclarativeTool = jest.fn();
|
|
5
8
|
|
|
6
|
-
jest.unstable_mockModule("../db/
|
|
9
|
+
jest.unstable_mockModule("../db/getConfig.js", () => ({
|
|
7
10
|
getConfig: mockGetConfig,
|
|
8
11
|
}));
|
|
9
12
|
|
|
@@ -11,20 +14,24 @@ jest.unstable_mockModule("../db/setConfig.js", () => ({
|
|
|
11
14
|
setConfig: mockSetConfig,
|
|
12
15
|
}));
|
|
13
16
|
|
|
17
|
+
jest.unstable_mockModule("../subsystems/tools/declarative.js", () => ({
|
|
18
|
+
loadDeclarativeTools: mockLoadDeclarativeTools,
|
|
19
|
+
findDeclarativeTool: mockFindDeclarativeTool,
|
|
20
|
+
parseDeclarativeTool: mockParseDeclarativeTool,
|
|
21
|
+
}));
|
|
22
|
+
|
|
14
23
|
const { ToolsStore } = await import("./tools.js");
|
|
15
24
|
const { DEFAULT_BUILTIN_PROFILE } =
|
|
16
|
-
await import("../subsystems/
|
|
25
|
+
await import("../subsystems/tools/builtin-profiles.js");
|
|
17
26
|
|
|
18
27
|
const db: any = {} as any;
|
|
19
28
|
|
|
20
29
|
describe("ToolsStore — built-in Nano profile", () => {
|
|
21
|
-
let store;
|
|
30
|
+
let store: any;
|
|
22
31
|
|
|
23
32
|
beforeEach(() => {
|
|
24
33
|
jest.clearAllMocks();
|
|
25
|
-
|
|
26
34
|
(mockGetConfig as any).mockResolvedValue(undefined);
|
|
27
|
-
|
|
28
35
|
(mockSetConfig as any).mockResolvedValue(undefined);
|
|
29
36
|
store = new ToolsStore();
|
|
30
37
|
});
|
|
@@ -53,22 +60,22 @@ describe("ToolsStore — built-in Nano profile", () => {
|
|
|
53
60
|
|
|
54
61
|
it("refuses to delete the built-in profile", async () => {
|
|
55
62
|
await store.deleteProfile(db, DEFAULT_BUILTIN_PROFILE.id);
|
|
56
|
-
// Should still be in the list
|
|
57
63
|
expect(
|
|
58
|
-
store.profiles.find((p) => p.id === DEFAULT_BUILTIN_PROFILE.id),
|
|
64
|
+
store.profiles.find((p: any) => p.id === DEFAULT_BUILTIN_PROFILE.id),
|
|
59
65
|
).toBeTruthy();
|
|
60
|
-
// setConfig should NOT have been called
|
|
61
66
|
expect(mockSetConfig).not.toHaveBeenCalled();
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
it("findProfilesForProvider returns built-in for prompt_api", () => {
|
|
65
70
|
const matches = store.findProfilesForProvider("prompt_api");
|
|
66
|
-
expect(matches.some((p) => p.id === DEFAULT_BUILTIN_PROFILE.id)).toBe(
|
|
71
|
+
expect(matches.some((p: any) => p.id === DEFAULT_BUILTIN_PROFILE.id)).toBe(
|
|
72
|
+
true,
|
|
73
|
+
);
|
|
67
74
|
});
|
|
68
75
|
|
|
69
76
|
it("does not return built-in for non-matching provider", () => {
|
|
70
77
|
const matches = store.findProfilesForProvider("openai");
|
|
71
|
-
expect(matches.some((p) => p.id === DEFAULT_BUILTIN_PROFILE.id)).toBe(
|
|
78
|
+
expect(matches.some((p: any) => p.id === DEFAULT_BUILTIN_PROFILE.id)).toBe(
|
|
72
79
|
false,
|
|
73
80
|
);
|
|
74
81
|
});
|
|
@@ -93,6 +100,119 @@ describe("ToolsStore — built-in Nano profile", () => {
|
|
|
93
100
|
});
|
|
94
101
|
});
|
|
95
102
|
|
|
103
|
+
describe("ToolsStore — custom tools, cloning, profiles & web search config", () => {
|
|
104
|
+
let store: any;
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
jest.clearAllMocks();
|
|
108
|
+
(mockGetConfig as any).mockResolvedValue(undefined);
|
|
109
|
+
(mockSetConfig as any).mockResolvedValue(undefined);
|
|
110
|
+
store = new ToolsStore();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("adds, removes, and clones custom tools", async () => {
|
|
114
|
+
const customTool = {
|
|
115
|
+
name: "custom_math",
|
|
116
|
+
description: "Perform math",
|
|
117
|
+
input_schema: { type: "object", properties: {} },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await store.addCustomTool(db, customTool);
|
|
121
|
+
expect(store.customTools).toHaveLength(1);
|
|
122
|
+
expect(store.enabledToolNames.has("custom_math")).toBe(true);
|
|
123
|
+
|
|
124
|
+
const cloned = await store.cloneTool(
|
|
125
|
+
db,
|
|
126
|
+
"custom_math",
|
|
127
|
+
"custom_math_v2",
|
|
128
|
+
"Perform advanced math",
|
|
129
|
+
);
|
|
130
|
+
expect(cloned).toBe(true);
|
|
131
|
+
expect(store.customTools).toHaveLength(2);
|
|
132
|
+
|
|
133
|
+
// Cannot clone with existing name
|
|
134
|
+
const cloneConflict = await store.cloneTool(
|
|
135
|
+
db,
|
|
136
|
+
"custom_math",
|
|
137
|
+
"custom_math_v2",
|
|
138
|
+
);
|
|
139
|
+
expect(cloneConflict).toBe(false);
|
|
140
|
+
|
|
141
|
+
// Remove custom tool
|
|
142
|
+
await store.removeCustomTool(db, "custom_math");
|
|
143
|
+
expect(store.customTools).toHaveLength(1);
|
|
144
|
+
expect(store.customTools[0].name).toBe("custom_math_v2");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("manages custom tool profiles and updates", async () => {
|
|
148
|
+
const profile = {
|
|
149
|
+
id: "p1",
|
|
150
|
+
name: "Research Profile",
|
|
151
|
+
enabledToolNames: ["web_search", "read_file"],
|
|
152
|
+
customTools: [],
|
|
153
|
+
systemPromptOverride: "You are a researcher",
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await store.addProfile(db, profile);
|
|
157
|
+
expect(store.profiles.find((p: any) => p.id === "p1")).toBeTruthy();
|
|
158
|
+
|
|
159
|
+
await store.activateProfile(db, "p1");
|
|
160
|
+
expect(store.activeProfileId).toBe("p1");
|
|
161
|
+
expect(store.systemPromptOverride).toBe("You are a researcher");
|
|
162
|
+
|
|
163
|
+
await store.setSystemPromptOverride(db, "Updated Prompt");
|
|
164
|
+
expect(store.systemPromptOverride).toBe("Updated Prompt");
|
|
165
|
+
|
|
166
|
+
await store.saveToActiveProfile(db);
|
|
167
|
+
expect(mockSetConfig).toHaveBeenCalled();
|
|
168
|
+
|
|
169
|
+
await store.deactivateProfile(db);
|
|
170
|
+
expect(store.activeProfileId).toBeNull();
|
|
171
|
+
|
|
172
|
+
await store.deleteProfile(db, "p1");
|
|
173
|
+
expect(store.profiles.find((p: any) => p.id === "p1")).toBeFalsy();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("configures web search and search_files options", async () => {
|
|
177
|
+
await store.setWebSearchUseProxy(db, true);
|
|
178
|
+
expect(store.webSearchUseProxy).toBe(true);
|
|
179
|
+
|
|
180
|
+
await store.setWebSearchProxyUrl(db, "https://proxy.example.com");
|
|
181
|
+
expect(store.webSearchProxyUrl).toBe("https://proxy.example.com");
|
|
182
|
+
|
|
183
|
+
await store.setWebSearchUrl(db, "https://search.example.com?q={query}");
|
|
184
|
+
expect(store.webSearchUrl).toBe("https://search.example.com?q={query}");
|
|
185
|
+
|
|
186
|
+
await store.setSearchFilesMaxFileBytes(db, 1024 * 1024);
|
|
187
|
+
expect(store.searchFilesMaxFileBytes).toBe(1024 * 1024);
|
|
188
|
+
|
|
189
|
+
await store.setSearchFilesMaxFilesVisited(db, 2000);
|
|
190
|
+
expect(store.searchFilesMaxFilesVisited).toBe(2000);
|
|
191
|
+
|
|
192
|
+
await store.setSearchFilesSkipDirs(db, "dist,out,.git");
|
|
193
|
+
expect(store.searchFilesSkipDirs).toBe("dist,out,.git");
|
|
194
|
+
expect(store.searchFilesSkipDirsSet).toEqual(
|
|
195
|
+
new Set(["dist", "out", ".git"]),
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("loads config from DB on load()", async () => {
|
|
200
|
+
(mockGetConfig as any).mockImplementation(async (_db: any, key: string) => {
|
|
201
|
+
if (key === "enabled_tools") return ["read_file"];
|
|
202
|
+
if (key === "system_prompt_override") return "Loaded Prompt";
|
|
203
|
+
if (key === "web_search_use_proxy") return "true";
|
|
204
|
+
if (key === "search_files_max_file_bytes") return "2048";
|
|
205
|
+
return null;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await store.load(db);
|
|
209
|
+
expect(store.enabledToolNames.has("read_file")).toBe(true);
|
|
210
|
+
expect(store.systemPromptOverride).toBe("Loaded Prompt");
|
|
211
|
+
expect(store.webSearchUseProxy).toBe(true);
|
|
212
|
+
expect(store.searchFilesMaxFileBytes).toBe(2048);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
96
216
|
describe("ToolsStore — declarative tools management", () => {
|
|
97
217
|
let store: any;
|
|
98
218
|
|
|
@@ -149,4 +269,21 @@ describe("ToolsStore — declarative tools management", () => {
|
|
|
149
269
|
expect(newStore.isDeclarativeToolEnabled("tool_x")).toBe(true);
|
|
150
270
|
expect(newStore.isDeclarativeToolEnabled("tool_y")).toBe(false);
|
|
151
271
|
});
|
|
272
|
+
|
|
273
|
+
it("refreshes declarative tools from loader", async () => {
|
|
274
|
+
(mockLoadDeclarativeTools as any).mockResolvedValueOnce({
|
|
275
|
+
tools: [
|
|
276
|
+
{
|
|
277
|
+
name: "custom_decl_tool",
|
|
278
|
+
description: "Declarative tool desc",
|
|
279
|
+
execution: { type: "javascript", script: "return 1;" },
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const refreshed = await store.refreshDeclarativeTools(db, "br:main");
|
|
285
|
+
expect(refreshed).toHaveLength(1);
|
|
286
|
+
expect(store.declarativeTools).toHaveLength(1);
|
|
287
|
+
expect(store.enabledDeclarativeTools).toHaveLength(1);
|
|
288
|
+
});
|
|
152
289
|
});
|