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
|
@@ -53,6 +53,16 @@ const mockGit: any = {
|
|
|
53
53
|
walk: jest.fn<any>().mockResolvedValue([]),
|
|
54
54
|
getConfig: jest.fn(),
|
|
55
55
|
setConfig: jest.fn(),
|
|
56
|
+
readBlob: jest.fn(),
|
|
57
|
+
readCommit: jest.fn(),
|
|
58
|
+
deleteBranch: jest.fn(),
|
|
59
|
+
init: jest.fn(),
|
|
60
|
+
annotatedTag: jest.fn(),
|
|
61
|
+
tag: jest.fn(),
|
|
62
|
+
listTags: jest.fn(),
|
|
63
|
+
listRemotes: jest.fn(),
|
|
64
|
+
addRemote: jest.fn(),
|
|
65
|
+
deleteRemote: jest.fn(),
|
|
56
66
|
};
|
|
57
67
|
|
|
58
68
|
// Patch navigator.storage before the module is imported.
|
|
@@ -82,6 +92,17 @@ describe("git", () => {
|
|
|
82
92
|
let gitCurrentBranch: Function;
|
|
83
93
|
let gitDeleteRepo: Function;
|
|
84
94
|
let gitDiff: Function;
|
|
95
|
+
let gitFetch: Function;
|
|
96
|
+
let gitReadFileAtRef: Function;
|
|
97
|
+
let gitShow: Function;
|
|
98
|
+
let gitDeleteBranch: Function;
|
|
99
|
+
let gitInit: Function;
|
|
100
|
+
let gitTag: Function;
|
|
101
|
+
let gitListTags: Function;
|
|
102
|
+
let gitRemote: Function;
|
|
103
|
+
let getRemoteUrl: Function;
|
|
104
|
+
let gitConfig: Function;
|
|
105
|
+
let gitUnstage: Function;
|
|
85
106
|
let gitListBranches: Function;
|
|
86
107
|
let gitListRepos: Function;
|
|
87
108
|
let gitLog: Function;
|
|
@@ -112,6 +133,17 @@ describe("git", () => {
|
|
|
112
133
|
gitCurrentBranch = mod.gitCurrentBranch;
|
|
113
134
|
gitDeleteRepo = mod.gitDeleteRepo;
|
|
114
135
|
gitDiff = mod.gitDiff;
|
|
136
|
+
gitFetch = mod.gitFetch;
|
|
137
|
+
gitReadFileAtRef = mod.gitReadFileAtRef;
|
|
138
|
+
gitShow = mod.gitShow;
|
|
139
|
+
gitDeleteBranch = mod.gitDeleteBranch;
|
|
140
|
+
gitInit = mod.gitInit;
|
|
141
|
+
gitTag = mod.gitTag;
|
|
142
|
+
gitListTags = mod.gitListTags;
|
|
143
|
+
gitRemote = mod.gitRemote;
|
|
144
|
+
getRemoteUrl = mod.getRemoteUrl;
|
|
145
|
+
gitConfig = mod.gitConfig;
|
|
146
|
+
gitUnstage = mod.gitUnstage;
|
|
115
147
|
gitListBranches = mod.gitListBranches;
|
|
116
148
|
gitListRepos = mod.gitListRepos;
|
|
117
149
|
gitLog = mod.gitLog;
|
|
@@ -1203,4 +1235,435 @@ describe("git", () => {
|
|
|
1203
1235
|
).rejects.toThrow("Failed to clone/fetch");
|
|
1204
1236
|
});
|
|
1205
1237
|
});
|
|
1238
|
+
|
|
1239
|
+
describe("gitFetch", () => {
|
|
1240
|
+
it("fetches remote ref successfully", async () => {
|
|
1241
|
+
mockGit.fetch.mockResolvedValueOnce({ fetchHead: "f00ba1" });
|
|
1242
|
+
const result = await gitFetch({ repo: "my-repo", branch: "main" });
|
|
1243
|
+
expect(result).toContain("Fetched main from origin successfully");
|
|
1244
|
+
expect(result).toContain("f00ba1");
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
describe("gitReadFileAtRef", () => {
|
|
1249
|
+
it("reads file blob at ref without checkout", async () => {
|
|
1250
|
+
mockGit.resolveRef.mockResolvedValueOnce("commita1b2");
|
|
1251
|
+
mockGit.readBlob.mockResolvedValueOnce({
|
|
1252
|
+
blob: new TextEncoder().encode("file content at ref"),
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
const content = await gitReadFileAtRef({
|
|
1256
|
+
repo: "my-repo",
|
|
1257
|
+
ref: "main",
|
|
1258
|
+
filepath: "src/index.ts",
|
|
1259
|
+
});
|
|
1260
|
+
expect(content).toBe("file content at ref");
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
describe("gitShow", () => {
|
|
1265
|
+
it("shows commit details and diff against parent", async () => {
|
|
1266
|
+
mockGit.resolveRef.mockResolvedValueOnce("commita1b2");
|
|
1267
|
+
mockGit.readCommit.mockResolvedValueOnce({
|
|
1268
|
+
commit: {
|
|
1269
|
+
parent: ["parent123"],
|
|
1270
|
+
author: {
|
|
1271
|
+
name: "Dev",
|
|
1272
|
+
email: "dev@example.com",
|
|
1273
|
+
timestamp: 1700000000,
|
|
1274
|
+
},
|
|
1275
|
+
message: "Commit message here",
|
|
1276
|
+
},
|
|
1277
|
+
});
|
|
1278
|
+
mockGit.statusMatrix.mockResolvedValueOnce([]);
|
|
1279
|
+
|
|
1280
|
+
const output = await gitShow({ repo: "my-repo", ref: "commita1b2" });
|
|
1281
|
+
expect(output).toContain("commit commita1b2");
|
|
1282
|
+
expect(output).toContain("Author: Dev <dev@example.com>");
|
|
1283
|
+
expect(output).toContain("Commit message here");
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
it("shows initial commit message when there is no parent", async () => {
|
|
1287
|
+
mockGit.resolveRef.mockResolvedValueOnce("rootcommit");
|
|
1288
|
+
mockGit.readCommit.mockResolvedValueOnce({
|
|
1289
|
+
commit: {
|
|
1290
|
+
parent: [],
|
|
1291
|
+
author: {
|
|
1292
|
+
name: "Dev",
|
|
1293
|
+
email: "dev@example.com",
|
|
1294
|
+
timestamp: 1700000000,
|
|
1295
|
+
},
|
|
1296
|
+
message: "Initial commit",
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
const output = await gitShow({ repo: "my-repo", ref: "rootcommit" });
|
|
1301
|
+
expect(output).toContain("Initial commit.");
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
describe("gitDeleteBranch", () => {
|
|
1306
|
+
it("deletes a local branch", async () => {
|
|
1307
|
+
mockGit.deleteBranch.mockResolvedValueOnce(undefined);
|
|
1308
|
+
const result = await gitDeleteBranch({
|
|
1309
|
+
repo: "my-repo",
|
|
1310
|
+
name: "old-feature",
|
|
1311
|
+
});
|
|
1312
|
+
expect(result).toBe("Deleted local branch old-feature in my-repo");
|
|
1313
|
+
expect(mockGit.deleteBranch).toHaveBeenCalledWith(
|
|
1314
|
+
expect.objectContaining({ ref: "old-feature" }),
|
|
1315
|
+
);
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
describe("gitInit", () => {
|
|
1320
|
+
it("initializes a new empty git repository", async () => {
|
|
1321
|
+
mockGit.init.mockResolvedValueOnce(undefined);
|
|
1322
|
+
mockGit.getConfig.mockResolvedValueOnce(false);
|
|
1323
|
+
|
|
1324
|
+
const result = await gitInit({ repo: "new-repo" });
|
|
1325
|
+
expect(result).toContain("Initialized empty Git repository in new-repo");
|
|
1326
|
+
expect(mockGit.init).toHaveBeenCalledWith(
|
|
1327
|
+
expect.objectContaining({ defaultBranch: "main" }),
|
|
1328
|
+
);
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
describe("gitTag & gitListTags", () => {
|
|
1333
|
+
it("creates lightweight and annotated tags", async () => {
|
|
1334
|
+
mockGit.tag.mockResolvedValueOnce(undefined);
|
|
1335
|
+
const resLight = await gitTag({ repo: "my-repo", tag: "v1.0.0" });
|
|
1336
|
+
expect(resLight).toBe("Created tag v1.0.0 in my-repo");
|
|
1337
|
+
|
|
1338
|
+
mockGit.annotatedTag.mockResolvedValueOnce(undefined);
|
|
1339
|
+
const resAnnotated = await gitTag({
|
|
1340
|
+
repo: "my-repo",
|
|
1341
|
+
tag: "v1.1.0",
|
|
1342
|
+
message: "Release 1.1.0",
|
|
1343
|
+
});
|
|
1344
|
+
expect(resAnnotated).toBe("Created annotated tag v1.1.0 in my-repo");
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
it("lists tags", async () => {
|
|
1348
|
+
mockGit.listTags.mockResolvedValueOnce(["v1.0.0", "v1.1.0"]);
|
|
1349
|
+
const list = await gitListTags({ repo: "my-repo" });
|
|
1350
|
+
expect(list).toBe("v1.0.0\nv1.1.0");
|
|
1351
|
+
|
|
1352
|
+
mockGit.listTags.mockResolvedValueOnce([]);
|
|
1353
|
+
const emptyList = await gitListTags({ repo: "my-repo" });
|
|
1354
|
+
expect(emptyList).toBe("No tags found.");
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
describe("gitRemote", () => {
|
|
1359
|
+
it("manages remotes (list, add, remove)", async () => {
|
|
1360
|
+
mockGit.listRemotes.mockResolvedValueOnce([
|
|
1361
|
+
{ remote: "origin", url: "https://github.com/user/repo" },
|
|
1362
|
+
]);
|
|
1363
|
+
const list = await gitRemote({ repo: "my-repo", command: "list" });
|
|
1364
|
+
expect(list).toContain("origin\thttps://github.com/user/repo");
|
|
1365
|
+
|
|
1366
|
+
mockGit.addRemote.mockResolvedValueOnce(undefined);
|
|
1367
|
+
const add = await gitRemote({
|
|
1368
|
+
repo: "my-repo",
|
|
1369
|
+
command: "add",
|
|
1370
|
+
remote: "upstream",
|
|
1371
|
+
url: "https://github.com/upstream/repo",
|
|
1372
|
+
});
|
|
1373
|
+
expect(add).toBe(
|
|
1374
|
+
"Added remote upstream -> https://github.com/upstream/repo",
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
mockGit.deleteRemote.mockResolvedValueOnce(undefined);
|
|
1378
|
+
const remove = await gitRemote({
|
|
1379
|
+
repo: "my-repo",
|
|
1380
|
+
command: "remove",
|
|
1381
|
+
remote: "upstream",
|
|
1382
|
+
});
|
|
1383
|
+
expect(remove).toBe("Removed remote upstream");
|
|
1384
|
+
|
|
1385
|
+
mockGit.listRemotes.mockResolvedValueOnce([]);
|
|
1386
|
+
const emptyList = await gitRemote({ repo: "my-repo", command: "list" });
|
|
1387
|
+
expect(emptyList).toBe("No remotes.");
|
|
1388
|
+
|
|
1389
|
+
await expect(
|
|
1390
|
+
gitRemote({ repo: "my-repo", command: "add", remote: "" }),
|
|
1391
|
+
).rejects.toThrow("remote and url are required for add");
|
|
1392
|
+
|
|
1393
|
+
await expect(
|
|
1394
|
+
gitRemote({ repo: "my-repo", command: "remove", remote: "" }),
|
|
1395
|
+
).rejects.toThrow("remote is required for remove");
|
|
1396
|
+
|
|
1397
|
+
await expect(
|
|
1398
|
+
gitRemote({ repo: "my-repo", command: "unknown" as any }),
|
|
1399
|
+
).rejects.toThrow("Invalid command");
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
describe("getRemoteUrl", () => {
|
|
1404
|
+
it("returns url of matching remote", async () => {
|
|
1405
|
+
mockGit.listRemotes.mockResolvedValueOnce([
|
|
1406
|
+
{ remote: "origin", url: "https://github.com/user/origin-repo" },
|
|
1407
|
+
{ remote: "upstream", url: "https://github.com/user/upstream-repo" },
|
|
1408
|
+
]);
|
|
1409
|
+
|
|
1410
|
+
const url = await getRemoteUrl({ repo: "my-repo", remote: "upstream" });
|
|
1411
|
+
expect(url).toBe("https://github.com/user/upstream-repo");
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
it("returns undefined if remote not found or on error", async () => {
|
|
1415
|
+
mockGit.listRemotes.mockResolvedValueOnce([
|
|
1416
|
+
{ remote: "origin", url: "https://github.com/user/origin-repo" },
|
|
1417
|
+
]);
|
|
1418
|
+
|
|
1419
|
+
const url = await getRemoteUrl({ repo: "my-repo", remote: "missing" });
|
|
1420
|
+
expect(url).toBeUndefined();
|
|
1421
|
+
|
|
1422
|
+
mockGit.listRemotes.mockRejectedValueOnce(new Error("No repo"));
|
|
1423
|
+
const errUrl = await getRemoteUrl({ repo: "not-a-repo" });
|
|
1424
|
+
expect(errUrl).toBeUndefined();
|
|
1425
|
+
});
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
describe("gitConfig", () => {
|
|
1429
|
+
it("gets and sets config values", async () => {
|
|
1430
|
+
mockGit.getConfig.mockResolvedValueOnce("user@example.com");
|
|
1431
|
+
const getRes = await gitConfig({
|
|
1432
|
+
repo: "my-repo",
|
|
1433
|
+
command: "get",
|
|
1434
|
+
key: "user.email",
|
|
1435
|
+
});
|
|
1436
|
+
expect(getRes).toBe("user@example.com");
|
|
1437
|
+
|
|
1438
|
+
mockGit.getConfig.mockResolvedValueOnce(undefined);
|
|
1439
|
+
const emptyGet = await gitConfig({
|
|
1440
|
+
repo: "my-repo",
|
|
1441
|
+
command: "get",
|
|
1442
|
+
key: "user.signingkey",
|
|
1443
|
+
});
|
|
1444
|
+
expect(emptyGet).toBe("");
|
|
1445
|
+
|
|
1446
|
+
mockGit.setConfig.mockResolvedValueOnce(undefined);
|
|
1447
|
+
const setRes = await gitConfig({
|
|
1448
|
+
repo: "my-repo",
|
|
1449
|
+
command: "set",
|
|
1450
|
+
key: "user.name",
|
|
1451
|
+
value: "Custom Dev",
|
|
1452
|
+
});
|
|
1453
|
+
expect(setRes).toBe("Set user.name = Custom Dev");
|
|
1454
|
+
|
|
1455
|
+
await expect(
|
|
1456
|
+
gitConfig({
|
|
1457
|
+
repo: "my-repo",
|
|
1458
|
+
command: "set",
|
|
1459
|
+
key: "user.name",
|
|
1460
|
+
}),
|
|
1461
|
+
).rejects.toThrow("value is required for set");
|
|
1462
|
+
|
|
1463
|
+
await expect(
|
|
1464
|
+
gitConfig({
|
|
1465
|
+
repo: "my-repo",
|
|
1466
|
+
command: "invalid" as any,
|
|
1467
|
+
key: "user.name",
|
|
1468
|
+
}),
|
|
1469
|
+
).rejects.toThrow("Invalid command");
|
|
1470
|
+
});
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
describe("gitUnstage", () => {
|
|
1474
|
+
it("unstages single file or array of files", async () => {
|
|
1475
|
+
mockGit.remove.mockResolvedValue(undefined);
|
|
1476
|
+
const single = await gitUnstage({
|
|
1477
|
+
repo: "my-repo",
|
|
1478
|
+
filepath: "file1.txt",
|
|
1479
|
+
});
|
|
1480
|
+
expect(single).toBe("Unstaged file1.txt in my-repo");
|
|
1481
|
+
|
|
1482
|
+
const multiple = await gitUnstage({
|
|
1483
|
+
repo: "my-repo",
|
|
1484
|
+
filepath: ["file1.txt", "file2.txt"],
|
|
1485
|
+
});
|
|
1486
|
+
expect(multiple).toBe("Unstaged file1.txt, file2.txt in my-repo");
|
|
1487
|
+
expect(mockGit.remove).toHaveBeenCalledTimes(3);
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
describe("git utilities and context helpers", () => {
|
|
1492
|
+
it("derives repo name from various url formats", () => {
|
|
1493
|
+
expect(repoNameFromUrl("https://github.com/xt-ml/shadow-claw.git")).toBe(
|
|
1494
|
+
"shadow-claw",
|
|
1495
|
+
);
|
|
1496
|
+
expect(repoNameFromUrl("https://github.com/xt-ml/shadow-claw/")).toBe(
|
|
1497
|
+
"shadow-claw",
|
|
1498
|
+
);
|
|
1499
|
+
expect(repoNameFromUrl("git@github.com:xt-ml/custom-repo.git")).toBe(
|
|
1500
|
+
"custom-repo",
|
|
1501
|
+
);
|
|
1502
|
+
expect(repoNameFromUrl("")).toBe("repo");
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it("formats repo dir path correctly", () => {
|
|
1506
|
+
expect(repoDir("my-repo")).toBe("repos/my-repo");
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
it("resolves proxy URLs according to preferences", () => {
|
|
1510
|
+
expect(getProxyUrl("custom", "https://proxy.example.com")).toBe(
|
|
1511
|
+
"https://proxy.example.com",
|
|
1512
|
+
);
|
|
1513
|
+
expect(getProxyUrl("public")).toBe("https://www.cors-anywhere.com");
|
|
1514
|
+
expect(getProxyUrl("local")).toContain("/git-proxy");
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
it("initializes git context with and without groupRoot", async () => {
|
|
1518
|
+
const { initGitContext, makeOpfsFs, ensureCoreFilemodeFalse } =
|
|
1519
|
+
await import("./git.js");
|
|
1520
|
+
|
|
1521
|
+
const baseCtx = await initGitContext();
|
|
1522
|
+
expect(baseCtx.repoDirFn("test-repo")).toBe("repos/test-repo");
|
|
1523
|
+
|
|
1524
|
+
const mockGroupHandle: any = {
|
|
1525
|
+
kind: "directory",
|
|
1526
|
+
name: "group-dir",
|
|
1527
|
+
getDirectoryHandle: jest.fn(),
|
|
1528
|
+
getFileHandle: jest.fn(),
|
|
1529
|
+
};
|
|
1530
|
+
const groupCtx = await initGitContext(mockGroupHandle);
|
|
1531
|
+
expect(groupCtx.repoDirFn("test-repo")).toBe("repos/test-repo");
|
|
1532
|
+
|
|
1533
|
+
expect(typeof makeOpfsFs).toBe("function");
|
|
1534
|
+
|
|
1535
|
+
mockGit.getConfig.mockResolvedValueOnce(true);
|
|
1536
|
+
await ensureCoreFilemodeFalse(mockGit, {}, "repos/test-repo");
|
|
1537
|
+
expect(mockGit.setConfig).toHaveBeenCalledWith(
|
|
1538
|
+
expect.objectContaining({
|
|
1539
|
+
path: "core.filemode",
|
|
1540
|
+
value: false,
|
|
1541
|
+
}),
|
|
1542
|
+
);
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
it("handles gitInit, gitTag, gitListTags, gitRemote, gitConfig, gitUnstage, gitReset", async () => {
|
|
1546
|
+
const {
|
|
1547
|
+
gitInit,
|
|
1548
|
+
gitTag,
|
|
1549
|
+
gitListTags,
|
|
1550
|
+
gitRemote,
|
|
1551
|
+
gitConfig,
|
|
1552
|
+
gitUnstage,
|
|
1553
|
+
gitReset,
|
|
1554
|
+
} = await import("./git.js");
|
|
1555
|
+
|
|
1556
|
+
// 1. gitInit
|
|
1557
|
+
mockGit.init.mockResolvedValue(undefined);
|
|
1558
|
+
const initRes = await gitInit({ repo: "new-repo" });
|
|
1559
|
+
expect(initRes).toContain("Initialized empty Git repository");
|
|
1560
|
+
|
|
1561
|
+
// 2. gitTag
|
|
1562
|
+
mockGit.tag.mockResolvedValue(undefined);
|
|
1563
|
+
const tagRes = await gitTag({ repo: "new-repo", tag: "v1.0.0" });
|
|
1564
|
+
expect(tagRes).toContain("Created tag v1.0.0");
|
|
1565
|
+
|
|
1566
|
+
// 3. gitListTags
|
|
1567
|
+
mockGit.listTags.mockResolvedValue(["v1.0.0", "v1.1.0"]);
|
|
1568
|
+
const tagsRes = await gitListTags({ repo: "new-repo" });
|
|
1569
|
+
expect(tagsRes).toBe("v1.0.0\nv1.1.0");
|
|
1570
|
+
|
|
1571
|
+
// 4. gitRemote - list, add, remove
|
|
1572
|
+
mockGit.listRemotes.mockResolvedValue([
|
|
1573
|
+
{ remote: "origin", url: "https://github.com/xt-ml/repo.git" },
|
|
1574
|
+
]);
|
|
1575
|
+
const remotesRes = await gitRemote({ repo: "new-repo", command: "list" });
|
|
1576
|
+
expect(remotesRes).toContain("origin\thttps://github.com/xt-ml/repo.git");
|
|
1577
|
+
|
|
1578
|
+
mockGit.addRemote.mockResolvedValue(undefined);
|
|
1579
|
+
const addRemoteRes = await gitRemote({
|
|
1580
|
+
repo: "new-repo",
|
|
1581
|
+
command: "add",
|
|
1582
|
+
remote: "upstream",
|
|
1583
|
+
url: "https://github.com/upstream/repo.git",
|
|
1584
|
+
});
|
|
1585
|
+
expect(addRemoteRes).toContain("Added remote upstream");
|
|
1586
|
+
|
|
1587
|
+
mockGit.deleteRemote.mockResolvedValue(undefined);
|
|
1588
|
+
const delRemoteRes = await gitRemote({
|
|
1589
|
+
repo: "new-repo",
|
|
1590
|
+
command: "remove",
|
|
1591
|
+
remote: "upstream",
|
|
1592
|
+
});
|
|
1593
|
+
expect(delRemoteRes).toContain("Removed remote upstream");
|
|
1594
|
+
|
|
1595
|
+
// 5. gitConfig - get and set
|
|
1596
|
+
mockGit.getConfig.mockResolvedValue("John Doe");
|
|
1597
|
+
const configRes = await gitConfig({
|
|
1598
|
+
repo: "new-repo",
|
|
1599
|
+
key: "user.name",
|
|
1600
|
+
command: "get",
|
|
1601
|
+
});
|
|
1602
|
+
expect(configRes).toBe("John Doe");
|
|
1603
|
+
|
|
1604
|
+
mockGit.setConfig.mockResolvedValue(undefined);
|
|
1605
|
+
const setConfigRes = await gitConfig({
|
|
1606
|
+
repo: "new-repo",
|
|
1607
|
+
key: "user.name",
|
|
1608
|
+
value: "Jane Doe",
|
|
1609
|
+
command: "set",
|
|
1610
|
+
});
|
|
1611
|
+
expect(setConfigRes).toContain("Set user.name = Jane Doe");
|
|
1612
|
+
|
|
1613
|
+
// 6. gitUnstage
|
|
1614
|
+
mockGit.resetIndex = jest.fn<any>().mockResolvedValue(undefined);
|
|
1615
|
+
const unstageRes = await gitUnstage({
|
|
1616
|
+
repo: "new-repo",
|
|
1617
|
+
filepath: "file.txt",
|
|
1618
|
+
});
|
|
1619
|
+
expect(unstageRes).toContain("Unstaged file.txt");
|
|
1620
|
+
|
|
1621
|
+
// 7. gitReset
|
|
1622
|
+
mockGit.currentBranch.mockResolvedValue("main");
|
|
1623
|
+
mockGit.resolveRef.mockResolvedValue("abc1234");
|
|
1624
|
+
mockGit.writeRef = jest.fn<any>().mockResolvedValue(undefined);
|
|
1625
|
+
const resetRes = await gitReset({
|
|
1626
|
+
repo: "new-repo",
|
|
1627
|
+
ref: "HEAD~1",
|
|
1628
|
+
});
|
|
1629
|
+
expect(resetRes).toContain("Reset main to HEAD~1");
|
|
1630
|
+
|
|
1631
|
+
// 8. gitDiff between two commits (ref1 & ref2)
|
|
1632
|
+
mockGit.resolveRef = jest
|
|
1633
|
+
.fn<any>()
|
|
1634
|
+
.mockImplementation(async ({ ref }: any) => {
|
|
1635
|
+
return ref === "HEAD~1"
|
|
1636
|
+
? "11111111111111111111"
|
|
1637
|
+
: "22222222222222222222";
|
|
1638
|
+
});
|
|
1639
|
+
mockGit.readCommit = jest.fn<any>().mockResolvedValue({
|
|
1640
|
+
commit: { tree: "tree-oid" },
|
|
1641
|
+
});
|
|
1642
|
+
mockGit.walk = jest.fn<any>().mockImplementation(async ({ map }: any) => {
|
|
1643
|
+
const A = {
|
|
1644
|
+
type: async () => "blob",
|
|
1645
|
+
oid: async () => "oid-a",
|
|
1646
|
+
content: async () =>
|
|
1647
|
+
new TextEncoder().encode("line 1\nold line\nline 3"),
|
|
1648
|
+
};
|
|
1649
|
+
const B = {
|
|
1650
|
+
type: async () => "blob",
|
|
1651
|
+
oid: async () => "oid-b",
|
|
1652
|
+
content: async () =>
|
|
1653
|
+
new TextEncoder().encode("line 1\nnew line\nline 3"),
|
|
1654
|
+
};
|
|
1655
|
+
const diff = await map("test.txt", [A, B]);
|
|
1656
|
+
return [diff];
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
const { gitDiff } = await import("./git.js");
|
|
1660
|
+
const diffTwoRefs = await gitDiff({
|
|
1661
|
+
repo: "new-repo",
|
|
1662
|
+
ref1: "HEAD~1",
|
|
1663
|
+
ref2: "HEAD",
|
|
1664
|
+
});
|
|
1665
|
+
expect(diffTwoRefs).toContain("Comparing HEAD~1");
|
|
1666
|
+
expect(diffTwoRefs).toContain("test.txt");
|
|
1667
|
+
});
|
|
1668
|
+
});
|
|
1206
1669
|
});
|
|
@@ -202,4 +202,15 @@ class MockDatabaseSync {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
let realSqlite;
|
|
206
|
+
try {
|
|
207
|
+
if (typeof process.getBuiltinModule === "function") {
|
|
208
|
+
realSqlite = process.getBuiltinModule("node:sqlite");
|
|
209
|
+
}
|
|
210
|
+
} catch (_) {}
|
|
211
|
+
|
|
212
|
+
if (realSqlite && realSqlite.DatabaseSync) {
|
|
213
|
+
exports.DatabaseSync = realSqlite.DatabaseSync;
|
|
214
|
+
} else {
|
|
215
|
+
exports.DatabaseSync = MockDatabaseSync;
|
|
216
|
+
}
|
|
@@ -310,6 +310,39 @@ describe("push-routes", () => {
|
|
|
310
310
|
});
|
|
311
311
|
});
|
|
312
312
|
|
|
313
|
+
describe("POST /push/command", () => {
|
|
314
|
+
it("registers the route", () => {
|
|
315
|
+
expect(app.routes.post["/push/command"]).toBeDefined();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("broadcasts remote-command payload to subscribers", async () => {
|
|
319
|
+
(store.getAllSubscriptions as any).mockReturnValue([
|
|
320
|
+
{
|
|
321
|
+
id: 1,
|
|
322
|
+
endpoint: "https://fcm.example.com/sub1",
|
|
323
|
+
keys_p256dh: "k1",
|
|
324
|
+
keys_auth: "k2",
|
|
325
|
+
},
|
|
326
|
+
]);
|
|
327
|
+
const req = createMockReq({
|
|
328
|
+
action: "send-message",
|
|
329
|
+
clientId: "client-target",
|
|
330
|
+
args: { text: "Wake up" },
|
|
331
|
+
});
|
|
332
|
+
const res = createMockRes();
|
|
333
|
+
await app.routes.post["/push/command"](req, res);
|
|
334
|
+
expect(res.statusCode).toBe(200);
|
|
335
|
+
expect(res._json.sent).toBe(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("returns 400 when action is missing", async () => {
|
|
339
|
+
const req = createMockReq({});
|
|
340
|
+
const res = createMockRes();
|
|
341
|
+
await app.routes.post["/push/command"](req, res);
|
|
342
|
+
expect(res.statusCode).toBe(400);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
313
346
|
describe("broadcastPush()", () => {
|
|
314
347
|
beforeEach(() => {
|
|
315
348
|
jest.clearAllMocks();
|
|
@@ -195,4 +195,27 @@ export function registerPushRoutes(app: Express): void {
|
|
|
195
195
|
res.status(500).json({ error: err.message });
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
|
+
|
|
199
|
+
// Broadcast a remote command via push to wake dormant clients
|
|
200
|
+
app.post("/push/command", async (req, res) => {
|
|
201
|
+
const { clientId, action, args, prompt } = req.body || {};
|
|
202
|
+
|
|
203
|
+
if (!action) {
|
|
204
|
+
return res.status(400).json({ error: "Missing action" });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const result = await broadcastPush({
|
|
209
|
+
type: "remote-command",
|
|
210
|
+
title: "ShadowClaw Command",
|
|
211
|
+
body: prompt || `Remote command: ${action}`,
|
|
212
|
+
clientId,
|
|
213
|
+
action,
|
|
214
|
+
args,
|
|
215
|
+
});
|
|
216
|
+
res.json(result);
|
|
217
|
+
} catch (err: any) {
|
|
218
|
+
res.status(500).json({ error: err.message });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
198
221
|
}
|
|
@@ -185,7 +185,7 @@ export function getAllSubscriptions(): PushSubscriptionRow[] {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
const result = db
|
|
188
|
-
.prepare("SELECT * FROM subscriptions ORDER BY created_at DESC")
|
|
188
|
+
.prepare("SELECT * FROM subscriptions ORDER BY created_at DESC, id DESC")
|
|
189
189
|
.all();
|
|
190
190
|
|
|
191
191
|
return result
|
|
@@ -18,6 +18,8 @@ const {
|
|
|
18
18
|
rewriteText,
|
|
19
19
|
detectLanguage,
|
|
20
20
|
translateText,
|
|
21
|
+
proofreadText,
|
|
22
|
+
embedText,
|
|
21
23
|
} = await import("./builtin-ai-tasks.js");
|
|
22
24
|
|
|
23
25
|
describe("builtin-ai-tasks subsystem", () => {
|
|
@@ -530,4 +532,76 @@ describe("builtin-ai-tasks subsystem", () => {
|
|
|
530
532
|
expect(mockDestroy).toHaveBeenCalled();
|
|
531
533
|
});
|
|
532
534
|
});
|
|
535
|
+
|
|
536
|
+
describe("proofreadText", () => {
|
|
537
|
+
it("calls Proofreader.create and proofreader.proofread when available", async () => {
|
|
538
|
+
const mockProofread = jest.fn<any>().mockResolvedValue("Corrected text.");
|
|
539
|
+
const mockDestroy = jest.fn<any>();
|
|
540
|
+
|
|
541
|
+
(globalThis as any).Proofreader = {
|
|
542
|
+
create: jest.fn<any>().mockResolvedValue({
|
|
543
|
+
proofread: mockProofread,
|
|
544
|
+
destroy: mockDestroy,
|
|
545
|
+
}),
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const result = await proofreadText("Bad text", { context: "Email" });
|
|
549
|
+
expect(result).toBe("Corrected text.");
|
|
550
|
+
expect((globalThis as any).Proofreader.create).toHaveBeenCalledWith({
|
|
551
|
+
context: "Email",
|
|
552
|
+
});
|
|
553
|
+
expect(mockProofread).toHaveBeenCalledWith("Bad text", {
|
|
554
|
+
context: "Email",
|
|
555
|
+
});
|
|
556
|
+
expect(mockDestroy).toHaveBeenCalled();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("falls back to rewriteText when Proofreader is absent", async () => {
|
|
560
|
+
delete (globalThis as any).Proofreader;
|
|
561
|
+
const mockRewrite = jest
|
|
562
|
+
.fn<any>()
|
|
563
|
+
.mockResolvedValue("Rewritten proofread text.");
|
|
564
|
+
const mockDestroy = jest.fn<any>();
|
|
565
|
+
|
|
566
|
+
(globalThis as any).Rewriter = {
|
|
567
|
+
create: jest.fn<any>().mockResolvedValue({
|
|
568
|
+
rewrite: mockRewrite,
|
|
569
|
+
destroy: mockDestroy,
|
|
570
|
+
}),
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const result = await proofreadText("Bad text", { context: "Email" });
|
|
574
|
+
expect(result).toBe("Rewritten proofread text.");
|
|
575
|
+
expect(mockRewrite).toHaveBeenCalled();
|
|
576
|
+
expect(mockDestroy).toHaveBeenCalled();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
describe("embedText", () => {
|
|
581
|
+
it("calls SemanticEmbedder.create and embedder.embed", async () => {
|
|
582
|
+
const mockEmbed = jest.fn<any>().mockResolvedValue({
|
|
583
|
+
embeddings: [[0.1, 0.2, 0.3]],
|
|
584
|
+
});
|
|
585
|
+
const mockDestroy = jest.fn<any>();
|
|
586
|
+
|
|
587
|
+
(globalThis as any).SemanticEmbedder = {
|
|
588
|
+
create: jest.fn<any>().mockResolvedValue({
|
|
589
|
+
embed: mockEmbed,
|
|
590
|
+
destroy: mockDestroy,
|
|
591
|
+
}),
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const result = await embedText("Hello embed");
|
|
595
|
+
expect(result).toEqual({ embeddings: [[0.1, 0.2, 0.3]] });
|
|
596
|
+
expect(mockEmbed).toHaveBeenCalledWith("Hello embed");
|
|
597
|
+
expect(mockDestroy).toHaveBeenCalled();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("throws error when SemanticEmbedder is not available", async () => {
|
|
601
|
+
delete (globalThis as any).SemanticEmbedder;
|
|
602
|
+
await expect(embedText("Hello embed")).rejects.toThrow(
|
|
603
|
+
/Semantic Embedder API is not supported/,
|
|
604
|
+
);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
533
607
|
});
|