shadow-claw 1.23.4 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -5
- 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 +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-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-settings/shadow-claw-settings.html +1 -1
- package/dist/public/config-CrHchneq.js +1 -0
- package/dist/public/{configurePeerJs-B4Ym9VMj.js → configurePeerJs-CxpZeDJJ.js} +1 -1
- package/dist/public/{connections-BEn6JYdu.js → connections-DvbgdkaB.js} +1 -1
- package/dist/public/{custom-element-security-DdYlUozq.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/decisions/bundled-typescript-architecture.md +1 -1
- package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +2 -2
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +1 -1
- 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 +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- 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-BQ4KzUt5.js → e2e-bridge-IGWsRl4i.js} +1 -1
- package/dist/public/file-viewer-D0eflgt6.js +1 -0
- package/dist/public/getAllTasks-vpBlgGsc.js +1 -0
- package/dist/public/getGroupDir-P1h9wl6S.js +1 -0
- package/dist/public/{git-DEUvQTUA.js → git-7Vq_SJSp.js} +2 -2
- package/dist/public/{iframe-sanitizer-0NS4XLOd.js → iframe-sanitizer-y2Z1pPwY.js} +1 -1
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/{initChatSplitResize-llGkSJXg.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 +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/{markdown-CyYR4App.js → markdown-BnpKxSxl.js} +1 -1
- package/dist/public/{mcp-reconnect-CSx4aMwy.js → mcp-reconnect-9emsqGRe.js} +1 -1
- package/dist/public/{memoryStorage-Dwg8NhAZ.js → memoryStorage-C0KvLNUp.js} +1 -1
- package/dist/public/{orchestrator-DRfEudXk.js → orchestrator-3h9xxHUm.js} +676 -676
- package/dist/public/orchestrator-nJj59RqD.js +15 -0
- package/dist/public/{peerjs-CurBuKvk.js → peerjs-BP4laMXj.js} +1 -1
- package/dist/public/{push-client-Dii-vPEh.js → push-client-CrjEXK3U.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-5IHaDDMJ.js → shadow-claw-CbwETBWf.js} +2 -2
- package/dist/public/{shadow-claw-accounts-CZvz4iQz.js → shadow-claw-accounts-B4sBTQEj.js} +1 -1
- package/dist/public/{shadow-claw-channels-BKXFyRS3.js → shadow-claw-channels-BDWG5Qa3.js} +1 -1
- package/dist/public/{shadow-claw-chat-CCLuhWdI.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-DV-87UUh.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-Dk2dbAUd.js → shadow-claw-file-viewer-Blf6yGVD.js} +1 -1
- package/dist/public/shadow-claw-files-CYySBDCl.js +840 -0
- package/dist/public/{shadow-claw-git-DRA7_qck.js → shadow-claw-git-v8S0JjuR.js} +2 -2
- package/dist/public/{shadow-claw-integrations-1Xu5snuV.js → shadow-claw-integrations-D7ufpwhX.js} +1 -1
- package/dist/public/{shadow-claw-llm-6Aci1BDP.js → shadow-claw-llm-GWgoVfhI.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-BUjAeVXC.js → shadow-claw-mcp-remote-kY4LMFNN.js} +1 -1
- package/dist/public/{shadow-claw-networking-FQe4nCp3.js → shadow-claw-networking-qHbP95z9.js} +1 -1
- package/dist/public/{shadow-claw-notifications-DV2wibL3.js → shadow-claw-notifications-YyHOK2Fr.js} +1 -1
- package/dist/public/{shadow-claw-page-header-B63S4HTU.js → shadow-claw-page-header-DMKbkv3s.js} +1 -1
- package/dist/public/{shadow-claw-pages-DE3jWj5K.js → shadow-claw-pages-DFZHhzFZ.js} +1 -1
- package/dist/public/{shadow-claw-settings-S6DAKfYx.js → shadow-claw-settings-DhxrKIfA.js} +10 -3
- package/dist/public/{shadow-claw-storage-DaacA3_K.js → shadow-claw-storage-BFynbP0j.js} +2 -2
- package/dist/public/{shadow-claw-task-server-IjKX042p.js → shadow-claw-task-server-3BIeb5oZ.js} +25 -4
- package/dist/public/{shadow-claw-tasks-Cy0JYmGU.js → shadow-claw-tasks-Caxc9atL.js} +1 -1
- package/dist/public/{shadow-claw-terminal-znAt7HyP.js → shadow-claw-terminal-Beb5qF-U.js} +1 -1
- package/dist/public/{shadow-claw-tools-Cutv9Si5.js → shadow-claw-tools-BfJx3A9W.js} +1 -1
- package/dist/public/{shadow-claw-webvm-BH8mPDEu.js → shadow-claw-webvm-BCS8ykeP.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-BqJxaMGR.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.test.ts +76 -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 +273 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +408 -1
- 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.ts +10 -0
- 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/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/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-BYAuxx3j.js +0 -1
- package/dist/public/downloadGroupFile-Drdvg1Ev.js +0 -1
- package/dist/public/file-viewer-CcZYw-PS.js +0 -1
- package/dist/public/orchestrator-o9Fj67BN.js +0 -15
- package/dist/public/shadow-claw-dialog-ByyHgxM9.js +0 -1
- package/dist/public/shadow-claw-files-BpfpJIuP.js +0 -840
|
@@ -45,6 +45,8 @@ jest.unstable_mockModule("../../stores/orchestrator.js", () => ({
|
|
|
45
45
|
toggleTask: jest.fn(),
|
|
46
46
|
getTasksByGroupId: jest.fn(() => []),
|
|
47
47
|
getTaskServerTerminalInstance: jest.fn(() => null),
|
|
48
|
+
reorderTasks: jest.fn(),
|
|
49
|
+
restoreTasksFromBackup: jest.fn(),
|
|
48
50
|
},
|
|
49
51
|
}));
|
|
50
52
|
|
|
@@ -261,5 +263,410 @@ describe("shadow-claw-tasks", () => {
|
|
|
261
263
|
}),
|
|
262
264
|
);
|
|
263
265
|
});
|
|
266
|
+
|
|
267
|
+
it("manages tools editor UI interactions (remove, modify, suppress)", () => {
|
|
268
|
+
const el = new ShadowClawTasks();
|
|
269
|
+
document.body.appendChild(el);
|
|
270
|
+
|
|
271
|
+
el.editingTools = [
|
|
272
|
+
{
|
|
273
|
+
name: "test_tool",
|
|
274
|
+
input: { a: 1 },
|
|
275
|
+
suppressOutput: false,
|
|
276
|
+
suppressToast: false,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
el.renderToolsEditor();
|
|
281
|
+
const toolsList = el.shadowRoot?.querySelector(
|
|
282
|
+
".tasks__tools-list",
|
|
283
|
+
) as HTMLElement;
|
|
284
|
+
expect(toolsList.querySelectorAll(".tasks__tool-item").length).toBe(1);
|
|
285
|
+
|
|
286
|
+
// Modify tool name
|
|
287
|
+
const nameInput = toolsList.querySelector(
|
|
288
|
+
".tasks__form-input",
|
|
289
|
+
) as HTMLInputElement;
|
|
290
|
+
nameInput.value = "updated_tool";
|
|
291
|
+
nameInput.dispatchEvent(new Event("input"));
|
|
292
|
+
expect(el.editingTools[0].name).toBe("updated_tool");
|
|
293
|
+
|
|
294
|
+
// Modify textarea params
|
|
295
|
+
const textarea = toolsList.querySelector(
|
|
296
|
+
".tasks__form-textarea",
|
|
297
|
+
) as HTMLTextAreaElement;
|
|
298
|
+
textarea.value = '{"foo": "bar"}';
|
|
299
|
+
textarea.dispatchEvent(new Event("change"));
|
|
300
|
+
expect(el.editingTools[0].input).toEqual({ foo: "bar" });
|
|
301
|
+
|
|
302
|
+
// Toggle suppress
|
|
303
|
+
const suppressCheckbox = toolsList.querySelector(
|
|
304
|
+
".tasks__tool-suppress input",
|
|
305
|
+
) as HTMLInputElement;
|
|
306
|
+
suppressCheckbox.checked = true;
|
|
307
|
+
suppressCheckbox.dispatchEvent(new Event("change"));
|
|
308
|
+
expect(el.editingTools[0].suppressOutput).toBe(true);
|
|
309
|
+
|
|
310
|
+
// Remove tool
|
|
311
|
+
const removeBtn = toolsList.querySelector(
|
|
312
|
+
".tasks__tool-remove-btn",
|
|
313
|
+
) as HTMLButtonElement;
|
|
314
|
+
removeBtn.click();
|
|
315
|
+
expect(el.editingTools.length).toBe(0);
|
|
316
|
+
|
|
317
|
+
document.body.removeChild(el);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("renders tasks and binds action buttons (toggle, copy, run, edit, delete)", async () => {
|
|
321
|
+
const el = new ShadowClawTasks();
|
|
322
|
+
const container = document.createElement("div");
|
|
323
|
+
container.className = "tasks__list";
|
|
324
|
+
const countEl = document.createElement("span");
|
|
325
|
+
countEl.className = "tasks__count";
|
|
326
|
+
el.shadowRoot?.replaceChildren(countEl, container);
|
|
327
|
+
|
|
328
|
+
const db = {} as any;
|
|
329
|
+
const tasks = [
|
|
330
|
+
{
|
|
331
|
+
id: "task-1",
|
|
332
|
+
name: "Daily Job",
|
|
333
|
+
schedule: "0 0 * * *",
|
|
334
|
+
prompt: "check status",
|
|
335
|
+
enabled: true,
|
|
336
|
+
type: "prompt",
|
|
337
|
+
freshContext: true,
|
|
338
|
+
},
|
|
339
|
+
];
|
|
340
|
+
(orchestratorStore as any).tasks = tasks;
|
|
341
|
+
|
|
342
|
+
await el.updateTaskList(db);
|
|
343
|
+
expect(container.querySelectorAll(".tasks__item").length).toBe(1);
|
|
344
|
+
|
|
345
|
+
const item = container.querySelector(".tasks__item") as HTMLElement;
|
|
346
|
+
const toggleInput = item.querySelector(
|
|
347
|
+
".tasks__toggle-input",
|
|
348
|
+
) as HTMLInputElement;
|
|
349
|
+
toggleInput.checked = false;
|
|
350
|
+
toggleInput.dispatchEvent(new Event("change"));
|
|
351
|
+
expect(orchestratorStore.toggleTask).toHaveBeenCalledWith(
|
|
352
|
+
db,
|
|
353
|
+
tasks[0],
|
|
354
|
+
false,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const runBtn = item.querySelector(".tasks__run-btn") as HTMLButtonElement;
|
|
358
|
+
runBtn.click();
|
|
359
|
+
expect(orchestratorStore.runTask).toHaveBeenCalledWith(tasks[0], true);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("handles restore with valid and invalid files", async () => {
|
|
363
|
+
const el = new ShadowClawTasks();
|
|
364
|
+
const db = {} as any;
|
|
365
|
+
|
|
366
|
+
// 1. No file selected
|
|
367
|
+
const emptyInput = document.createElement("input");
|
|
368
|
+
emptyInput.type = "file";
|
|
369
|
+
await el.handleRestore(db, emptyInput);
|
|
370
|
+
|
|
371
|
+
// 2. Valid JSON file
|
|
372
|
+
const backupData = [
|
|
373
|
+
{
|
|
374
|
+
id: "restored-1",
|
|
375
|
+
schedule: "0 0 * * *",
|
|
376
|
+
prompt: "daily report",
|
|
377
|
+
enabled: true,
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
const validFile = new File([JSON.stringify(backupData)], "backup.json", {
|
|
381
|
+
type: "application/json",
|
|
382
|
+
});
|
|
383
|
+
const fileInput = document.createElement("input");
|
|
384
|
+
fileInput.type = "file";
|
|
385
|
+
Object.defineProperty(fileInput, "files", { value: [validFile] });
|
|
386
|
+
|
|
387
|
+
await el.handleRestore(db, fileInput);
|
|
388
|
+
expect(orchestratorStore.upsertTask).toHaveBeenCalled();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("handles preview link clicks and opens in file viewer", async () => {
|
|
392
|
+
const el = new ShadowClawTasks();
|
|
393
|
+
const db = {} as any;
|
|
394
|
+
|
|
395
|
+
const link = document.createElement("a");
|
|
396
|
+
link.setAttribute("href", "docs/spec.md");
|
|
397
|
+
const targetDiv = document.createElement("div");
|
|
398
|
+
targetDiv.appendChild(link);
|
|
399
|
+
|
|
400
|
+
const event = new MouseEvent("click", {
|
|
401
|
+
bubbles: true,
|
|
402
|
+
cancelable: true,
|
|
403
|
+
});
|
|
404
|
+
Object.defineProperty(event, "target", { value: link });
|
|
405
|
+
Object.defineProperty(event, "button", { value: 0 });
|
|
406
|
+
|
|
407
|
+
const fileViewerMod = await import("../../stores/file-viewer.js");
|
|
408
|
+
await el.handlePreviewLinkClick(event, db);
|
|
409
|
+
expect(fileViewerMod.fileViewerStore.openFile).toHaveBeenCalledWith(
|
|
410
|
+
db,
|
|
411
|
+
"docs/spec.md",
|
|
412
|
+
"default",
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("cleans up on disconnectedCallback", () => {
|
|
417
|
+
const el = new ShadowClawTasks();
|
|
418
|
+
const cleanupMock = jest.fn();
|
|
419
|
+
el.cleanup = cleanupMock;
|
|
420
|
+
el.disconnectedCallback();
|
|
421
|
+
expect(cleanupMock).toHaveBeenCalledTimes(1);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("renders tools preview for tool task chains", () => {
|
|
425
|
+
const el = new ShadowClawTasks();
|
|
426
|
+
const tools = [
|
|
427
|
+
{ name: "read_file", input: { path: "hello.txt" } },
|
|
428
|
+
{ name: "write_file", input: { path: "out.txt", content: "$pipe" } },
|
|
429
|
+
];
|
|
430
|
+
const previewHtml = el.renderToolsPreview(tools);
|
|
431
|
+
expect(previewHtml).toContain("read_file");
|
|
432
|
+
expect(previewHtml).toContain("write_file");
|
|
433
|
+
expect(previewHtml).toContain("hello.txt");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("handles clear all tasks when confirmed", async () => {
|
|
437
|
+
const el = new ShadowClawTasks();
|
|
438
|
+
const db = {} as any;
|
|
439
|
+
window.confirm = jest.fn(() => true);
|
|
440
|
+
|
|
441
|
+
await el.handleClearAll(db);
|
|
442
|
+
expect(orchestratorStore.clearAllTasks).toHaveBeenCalledWith(db);
|
|
443
|
+
|
|
444
|
+
window.confirm = jest.fn(() => false);
|
|
445
|
+
(orchestratorStore.clearAllTasks as any).mockClear();
|
|
446
|
+
await el.handleClearAll(db);
|
|
447
|
+
expect(orchestratorStore.clearAllTasks).not.toHaveBeenCalled();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("handles edit submission for prompt and tool tasks", async () => {
|
|
451
|
+
const el = new ShadowClawTasks();
|
|
452
|
+
const db = {} as any;
|
|
453
|
+
|
|
454
|
+
const form = document.createElement("form");
|
|
455
|
+
const scheduleInput = document.createElement("input");
|
|
456
|
+
scheduleInput.name = "schedule";
|
|
457
|
+
scheduleInput.value = "*/15 * * * *";
|
|
458
|
+
|
|
459
|
+
const promptInput = document.createElement("textarea");
|
|
460
|
+
promptInput.name = "prompt";
|
|
461
|
+
promptInput.value = "Run automated scan";
|
|
462
|
+
|
|
463
|
+
const freshContextInput = document.createElement("input");
|
|
464
|
+
freshContextInput.name = "freshContext";
|
|
465
|
+
freshContextInput.type = "checkbox";
|
|
466
|
+
freshContextInput.checked = true;
|
|
467
|
+
|
|
468
|
+
const subagentInput = document.createElement("input");
|
|
469
|
+
subagentInput.name = "subagent";
|
|
470
|
+
subagentInput.type = "checkbox";
|
|
471
|
+
subagentInput.checked = false;
|
|
472
|
+
|
|
473
|
+
const promptRadio = document.createElement("input");
|
|
474
|
+
promptRadio.name = "taskType";
|
|
475
|
+
promptRadio.value = "prompt";
|
|
476
|
+
promptRadio.checked = true;
|
|
477
|
+
|
|
478
|
+
form.append(
|
|
479
|
+
scheduleInput,
|
|
480
|
+
promptInput,
|
|
481
|
+
freshContextInput,
|
|
482
|
+
subagentInput,
|
|
483
|
+
promptRadio,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const dialog = document.createElement("dialog");
|
|
487
|
+
dialog.close = jest.fn();
|
|
488
|
+
el.shadowRoot?.appendChild(dialog);
|
|
489
|
+
|
|
490
|
+
await el.handleEditSubmit(db, form);
|
|
491
|
+
expect(orchestratorStore.upsertTask).toHaveBeenCalledWith(
|
|
492
|
+
db,
|
|
493
|
+
expect.objectContaining({
|
|
494
|
+
schedule: "*/15 * * * *",
|
|
495
|
+
prompt: "Run automated scan",
|
|
496
|
+
freshContext: true,
|
|
497
|
+
type: "prompt",
|
|
498
|
+
}),
|
|
499
|
+
);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("handles copyId, delete, reorder, restore, and renderPreview", async () => {
|
|
503
|
+
const el = new ShadowClawTasks() as any;
|
|
504
|
+
const db = {} as any;
|
|
505
|
+
|
|
506
|
+
// 1. Copy ID
|
|
507
|
+
const mockClipboard = {
|
|
508
|
+
writeText: jest.fn<any>().mockResolvedValue(undefined),
|
|
509
|
+
};
|
|
510
|
+
Object.assign(navigator, { clipboard: mockClipboard });
|
|
511
|
+
await el.handleCopyId("task-123");
|
|
512
|
+
expect(mockClipboard.writeText).toHaveBeenCalledWith("task-123");
|
|
513
|
+
|
|
514
|
+
// 2. Delete task with confirmation
|
|
515
|
+
el.requestConfirmation = jest.fn<any>().mockResolvedValue(true);
|
|
516
|
+
await el.handleDelete(db, "task-123");
|
|
517
|
+
expect(orchestratorStore.deleteTask).toHaveBeenCalledWith(db, "task-123");
|
|
518
|
+
|
|
519
|
+
// 3. Reorder tasks
|
|
520
|
+
await el.handleReorder("t1", "t2", ["t2", "t1"]);
|
|
521
|
+
expect(orchestratorStore.reorderTasks).toHaveBeenCalledWith(
|
|
522
|
+
expect.anything(),
|
|
523
|
+
"default",
|
|
524
|
+
["t2", "t1"],
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// 4. Restore tasks from JSON
|
|
528
|
+
const restoreFile = {
|
|
529
|
+
name: "tasks-backup.json",
|
|
530
|
+
text: jest
|
|
531
|
+
.fn<() => Promise<string>>()
|
|
532
|
+
.mockResolvedValue(JSON.stringify([{ id: "t1" }])),
|
|
533
|
+
};
|
|
534
|
+
const restoreInput = {
|
|
535
|
+
files: [restoreFile],
|
|
536
|
+
value: "tasks-backup.json",
|
|
537
|
+
} as any;
|
|
538
|
+
await el.handleRestore(db, restoreInput);
|
|
539
|
+
expect(orchestratorStore.restoreTasksFromBackup).toHaveBeenCalledWith(
|
|
540
|
+
db,
|
|
541
|
+
[{ id: "t1" }],
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// 5. Render preview
|
|
545
|
+
const emptyPreview = await el.renderPreview("");
|
|
546
|
+
expect(emptyPreview).toContain("No content");
|
|
547
|
+
|
|
548
|
+
const shortPreview = await el.renderPreview("Short prompt");
|
|
549
|
+
expect(shortPreview).toContain("tasks__prompt");
|
|
550
|
+
|
|
551
|
+
const longPreview = await el.renderPreview(
|
|
552
|
+
"Long\nmulti-line\nprompt",
|
|
553
|
+
true,
|
|
554
|
+
);
|
|
555
|
+
expect(longPreview).toContain("tasks__content-details");
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("handles keyboard navigation, announcement, and autoscroll", async () => {
|
|
559
|
+
const el = new ShadowClawTasks() as any;
|
|
560
|
+
document.body.appendChild(el);
|
|
561
|
+
|
|
562
|
+
// 1. _announce
|
|
563
|
+
const liveRegion = document.createElement("div");
|
|
564
|
+
liveRegion.id = "live-region";
|
|
565
|
+
el.shadowRoot?.appendChild(liveRegion);
|
|
566
|
+
el._announce("Task test announcement");
|
|
567
|
+
|
|
568
|
+
// 2. Keyboard grab and navigation
|
|
569
|
+
(orchestratorStore as any).tasks = [
|
|
570
|
+
{ id: "t1", name: "Task 1", prompt: "Hello 1" },
|
|
571
|
+
{ id: "t2", name: "Task 2", prompt: "Hello 2" },
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
// Grab with 'm'
|
|
575
|
+
const keyM = new KeyboardEvent("keydown", { key: "m", cancelable: true });
|
|
576
|
+
el._handleKeyboard(keyM, "t1", "Task 1");
|
|
577
|
+
expect(el._keyboardGrabbedId).toBe("t1");
|
|
578
|
+
|
|
579
|
+
// Move down with ArrowDown
|
|
580
|
+
const keyDown = new KeyboardEvent("keydown", {
|
|
581
|
+
key: "ArrowDown",
|
|
582
|
+
cancelable: true,
|
|
583
|
+
});
|
|
584
|
+
el._handleKeyboard(keyDown, "t1", "Task 1");
|
|
585
|
+
|
|
586
|
+
// Drop with Space
|
|
587
|
+
const keySpace = new KeyboardEvent("keydown", {
|
|
588
|
+
key: " ",
|
|
589
|
+
cancelable: true,
|
|
590
|
+
});
|
|
591
|
+
el._handleKeyboard(keySpace, "t1", "Task 1");
|
|
592
|
+
expect(el._keyboardGrabbedId).toBeNull();
|
|
593
|
+
|
|
594
|
+
// Cancel with Escape
|
|
595
|
+
el._keyboardGrabbedId = "t1";
|
|
596
|
+
const keyEsc = new KeyboardEvent("keydown", {
|
|
597
|
+
key: "Escape",
|
|
598
|
+
cancelable: true,
|
|
599
|
+
});
|
|
600
|
+
el._handleKeyboard(keyEsc, "t1", "Task 1");
|
|
601
|
+
expect(el._keyboardGrabbedId).toBeNull();
|
|
602
|
+
|
|
603
|
+
// Navigation when not grabbed
|
|
604
|
+
const item1 = document.createElement("div");
|
|
605
|
+
item1.className = "tasks__item";
|
|
606
|
+
item1.setAttribute("tabindex", "0");
|
|
607
|
+
const item2 = document.createElement("div");
|
|
608
|
+
item2.className = "tasks__item";
|
|
609
|
+
item2.setAttribute("tabindex", "0");
|
|
610
|
+
el.shadowRoot?.appendChild(item1);
|
|
611
|
+
el.shadowRoot?.appendChild(item2);
|
|
612
|
+
|
|
613
|
+
const navDown = new KeyboardEvent("keydown", {
|
|
614
|
+
key: "ArrowDown",
|
|
615
|
+
cancelable: true,
|
|
616
|
+
});
|
|
617
|
+
Object.defineProperty(navDown, "target", { value: item1 });
|
|
618
|
+
el._handleKeyboard(navDown, "t1", "Task 1");
|
|
619
|
+
|
|
620
|
+
// 3. Autoscroll
|
|
621
|
+
let content = el.shadowRoot?.querySelector(
|
|
622
|
+
".tasks__content",
|
|
623
|
+
) as HTMLElement;
|
|
624
|
+
if (!content) {
|
|
625
|
+
content = document.createElement("div");
|
|
626
|
+
content.className = "tasks__content";
|
|
627
|
+
el.shadowRoot?.appendChild(content);
|
|
628
|
+
}
|
|
629
|
+
content.getBoundingClientRect = () =>
|
|
630
|
+
({ top: 100, bottom: 500, height: 400 }) as any;
|
|
631
|
+
|
|
632
|
+
el._updateAutoScrollSpeed(110); // Near top
|
|
633
|
+
expect(el._autoScrollActive).toBe(true);
|
|
634
|
+
el._stopAutoScroll();
|
|
635
|
+
expect(el._autoScrollActive).toBe(false);
|
|
636
|
+
|
|
637
|
+
// 4. Focus navigation
|
|
638
|
+
el._focusNext(item1);
|
|
639
|
+
el._focusPrev(item2);
|
|
640
|
+
el._focusNextItem(item1);
|
|
641
|
+
el._focusPrevItem(item2);
|
|
642
|
+
|
|
643
|
+
// 5. Touch events and findTouch
|
|
644
|
+
if (el.shadowRoot) {
|
|
645
|
+
(el.shadowRoot as any).elementFromPoint = jest.fn(() => item1);
|
|
646
|
+
}
|
|
647
|
+
const listEl = document.createElement("div");
|
|
648
|
+
listEl.className = "tasks__list";
|
|
649
|
+
el.shadowRoot?.appendChild(listEl);
|
|
650
|
+
el._bindTouchListEvents(listEl);
|
|
651
|
+
|
|
652
|
+
el._touchId = 123;
|
|
653
|
+
el._touchDraggedTaskId = "t1";
|
|
654
|
+
const fakeTouch = { identifier: 123, clientX: 50, clientY: 150 };
|
|
655
|
+
const touchMoveEvt = new Event("touchmove") as any;
|
|
656
|
+
touchMoveEvt.touches = [fakeTouch];
|
|
657
|
+
touchMoveEvt.changedTouches = [fakeTouch];
|
|
658
|
+
listEl.dispatchEvent(touchMoveEvt);
|
|
659
|
+
|
|
660
|
+
const touchEndEvt = new Event("touchend") as any;
|
|
661
|
+
touchEndEvt.touches = [];
|
|
662
|
+
touchEndEvt.changedTouches = [fakeTouch];
|
|
663
|
+
listEl.dispatchEvent(touchEndEvt);
|
|
664
|
+
|
|
665
|
+
const touchCancelEvt = new Event("touchcancel") as any;
|
|
666
|
+
listEl.dispatchEvent(touchCancelEvt);
|
|
667
|
+
|
|
668
|
+
el.cleanup = jest.fn();
|
|
669
|
+
document.body.removeChild(el);
|
|
670
|
+
});
|
|
264
671
|
});
|
|
265
672
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { consumeEscapeSequence } from "./consumeEscapeSequence.js";
|
|
2
|
+
|
|
3
|
+
describe("consumeEscapeSequence", () => {
|
|
4
|
+
it("handles end of string right after escape character", () => {
|
|
5
|
+
const res = consumeEscapeSequence("\x1b", 0);
|
|
6
|
+
expect(res).toEqual({ action: "ignore", nextIndex: 1, incomplete: true });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("CSI sequences (\\x1b[)", () => {
|
|
10
|
+
it("handles clear-screen sequence ending with J (e.g. \\x1b[2J)", () => {
|
|
11
|
+
const res = consumeEscapeSequence("\x1b[2JHello", 0);
|
|
12
|
+
expect(res).toEqual({ action: "clear-screen", nextIndex: 4 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("handles clear-line sequence ending with K (e.g. \\x1b[K)", () => {
|
|
16
|
+
const res = consumeEscapeSequence("\x1b[KRest", 0);
|
|
17
|
+
expect(res).toEqual({ action: "clear-line", nextIndex: 3 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("handles other CSI sequences (e.g. color \\x1b[31m)", () => {
|
|
21
|
+
const res = consumeEscapeSequence("\x1b[31mRed text", 0);
|
|
22
|
+
expect(res).toEqual({ action: "ignore", nextIndex: 5 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("handles incomplete CSI sequence at end of buffer", () => {
|
|
26
|
+
const res = consumeEscapeSequence("\x1b[31", 0);
|
|
27
|
+
expect(res).toEqual({ action: "ignore", nextIndex: 4, incomplete: true });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("OSC sequences (\\x1b])", () => {
|
|
32
|
+
it("handles OSC sequence terminated by BEL (\\u0007)", () => {
|
|
33
|
+
const res = consumeEscapeSequence("\x1b]0;Title\u0007Remaining", 0);
|
|
34
|
+
expect(res).toEqual({ action: "ignore", nextIndex: 10 });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("handles OSC sequence terminated by ST (\\u001b\\\\)", () => {
|
|
38
|
+
const res = consumeEscapeSequence("\x1b]0;Title\u001b\\Remaining", 0);
|
|
39
|
+
expect(res).toEqual({ action: "ignore", nextIndex: 11 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("handles incomplete OSC sequence at end of buffer", () => {
|
|
43
|
+
const res = consumeEscapeSequence("\x1b]0;Incomplete", 0);
|
|
44
|
+
expect(res).toEqual({
|
|
45
|
+
action: "ignore",
|
|
46
|
+
nextIndex: 14,
|
|
47
|
+
incomplete: true,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("Other escape sequences", () => {
|
|
53
|
+
it("handles simple 2-char escape sequence (e.g. \\x1bM)", () => {
|
|
54
|
+
const res = consumeEscapeSequence("\x1bMRest", 0);
|
|
55
|
+
expect(res).toEqual({ action: "ignore", nextIndex: 2 });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -461,4 +461,84 @@ describe("shadow-claw-tools", () => {
|
|
|
461
461
|
|
|
462
462
|
document.body.removeChild(el);
|
|
463
463
|
});
|
|
464
|
+
|
|
465
|
+
it("handles handleAddTool, handleCloneTool, handleRestore, and handleSaveProfile", async () => {
|
|
466
|
+
const el = new ShadowClawTools() as any;
|
|
467
|
+
document.body.appendChild(el);
|
|
468
|
+
await el.connectedCallback();
|
|
469
|
+
const db = {} as any;
|
|
470
|
+
|
|
471
|
+
const dialog = el.shadowRoot?.querySelector(".tools__dialog");
|
|
472
|
+
if (dialog) (dialog as any).close = jest.fn();
|
|
473
|
+
const cloneDialog = el.shadowRoot?.querySelector(".tools__clone-dialog");
|
|
474
|
+
if (cloneDialog) (cloneDialog as any).close = jest.fn();
|
|
475
|
+
const profileDialog = el.shadowRoot?.querySelector(
|
|
476
|
+
".tools__profile-dialog",
|
|
477
|
+
);
|
|
478
|
+
if (profileDialog) (profileDialog as any).close = jest.fn();
|
|
479
|
+
|
|
480
|
+
// 1. Add Tool validation
|
|
481
|
+
const form = document.createElement("form");
|
|
482
|
+
form.innerHTML = `
|
|
483
|
+
<input name="name" value="" />
|
|
484
|
+
<input name="description" value="" />
|
|
485
|
+
`;
|
|
486
|
+
await el.handleAddTool(db, form);
|
|
487
|
+
expect(toolsStore.addCustomTool).not.toHaveBeenCalled();
|
|
488
|
+
|
|
489
|
+
// 2. Add Tool success
|
|
490
|
+
form.innerHTML = `
|
|
491
|
+
<input name="name" value="unique_tool" />
|
|
492
|
+
<input name="description" value="A unique tool" />
|
|
493
|
+
<input name="input_schema" value='{"type":"object"}' />
|
|
494
|
+
`;
|
|
495
|
+
await el.handleAddTool(db, form);
|
|
496
|
+
expect(toolsStore.addCustomTool).toHaveBeenCalledWith(
|
|
497
|
+
db,
|
|
498
|
+
expect.objectContaining({ name: "unique_tool" }),
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// 3. Clone Tool
|
|
502
|
+
const cloneForm = document.createElement("form");
|
|
503
|
+
cloneForm.innerHTML = `
|
|
504
|
+
<input name="source" value="bash" />
|
|
505
|
+
<input name="name" value="bash_copy" />
|
|
506
|
+
<input name="description" value="Cloned bash" />
|
|
507
|
+
`;
|
|
508
|
+
await el.handleCloneTool(db, cloneForm);
|
|
509
|
+
expect(toolsStore.cloneTool).toHaveBeenCalledWith(
|
|
510
|
+
db,
|
|
511
|
+
"bash",
|
|
512
|
+
"bash_copy",
|
|
513
|
+
"Cloned bash",
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// 4. Restore backup
|
|
517
|
+
const restoreFile = {
|
|
518
|
+
text: jest
|
|
519
|
+
.fn<() => Promise<string>>()
|
|
520
|
+
.mockResolvedValue(JSON.stringify({ tools: [] })),
|
|
521
|
+
};
|
|
522
|
+
const restoreInput = {
|
|
523
|
+
files: [restoreFile],
|
|
524
|
+
value: "backup.json",
|
|
525
|
+
} as any;
|
|
526
|
+
await el.handleRestore(db, restoreInput);
|
|
527
|
+
expect(toolsStore.importBackup).toHaveBeenCalled();
|
|
528
|
+
|
|
529
|
+
// 5. Save Profile
|
|
530
|
+
const profileForm = document.createElement("form");
|
|
531
|
+
profileForm.innerHTML = `
|
|
532
|
+
<input name="name" value="Dev Profile" />
|
|
533
|
+
<input name="providerId" value="anthropic" />
|
|
534
|
+
<input name="model" value="claude-3" />
|
|
535
|
+
`;
|
|
536
|
+
await el.handleSaveProfile(db, profileForm);
|
|
537
|
+
expect(toolsStore.addProfile).toHaveBeenCalledWith(
|
|
538
|
+
db,
|
|
539
|
+
expect.objectContaining({ name: "Dev Profile", providerId: "anthropic" }),
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
document.body.removeChild(el);
|
|
543
|
+
});
|
|
464
544
|
});
|
package/src/config/config.ts
CHANGED
|
@@ -1035,6 +1035,10 @@ export const CONFIG_KEYS = {
|
|
|
1035
1035
|
CHAT_SPLIT_VIEW_HEIGHT: "chat_split_view_height",
|
|
1036
1036
|
COMPACTION_ENGINE_PREFERENCE: "compaction_engine_preference",
|
|
1037
1037
|
CONTEXT_COMPRESSION_ENABLED: "context_compression_enabled",
|
|
1038
|
+
CONTROL_PLANE_CLIENT_ID: "control_plane_client_id",
|
|
1039
|
+
CONTROL_PLANE_ENABLED: "control_plane_enabled",
|
|
1040
|
+
CONTROL_PLANE_TRANSPORT: "control_plane_transport",
|
|
1041
|
+
CONTROL_PLANE_URL: "control_plane_url",
|
|
1038
1042
|
CONVERSATIONS_HEIGHT: "conversations_height",
|
|
1039
1043
|
CUSTOM_TOOLS: "custom_tools",
|
|
1040
1044
|
DB_MIGRATED_FROM_LEGACY: "db_migrated_from_legacy",
|
|
@@ -1185,3 +1189,9 @@ export const DEFAULT_TASK_SERVER_ENABLED = false;
|
|
|
1185
1189
|
|
|
1186
1190
|
/** Default Files Upload Append ULID */
|
|
1187
1191
|
export const DEFAULT_FILES_UPLOAD_APPEND_ULID = false;
|
|
1192
|
+
|
|
1193
|
+
/** Default Control Plane Enabled (opt-in) */
|
|
1194
|
+
export const DEFAULT_CONTROL_PLANE_ENABLED = false;
|
|
1195
|
+
|
|
1196
|
+
/** Default Control Plane Transport ("sse" or "websocket") */
|
|
1197
|
+
export const DEFAULT_CONTROL_PLANE_TRANSPORT = "sse";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { BackupController } from "./backup-controller.js";
|
|
3
|
+
|
|
4
|
+
describe("backup-controller", () => {
|
|
5
|
+
let mockFetch: jest.Mock<any>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockFetch = jest.fn() as any;
|
|
9
|
+
(globalThis as any).fetch = mockFetch;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("enumerates files, uploads each, and completes the backup", async () => {
|
|
13
|
+
const mockFiles: Record<string, string> = {
|
|
14
|
+
"MEMORY.md": "# Memory content",
|
|
15
|
+
"src/index.ts": "console.log('hello');",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
mockFetch.mockImplementation(async (url: string, init: any) => {
|
|
19
|
+
const urlStr = url.toString();
|
|
20
|
+
if (urlStr.includes("/api/backup/upload")) {
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
status: 200,
|
|
24
|
+
json: async () => ({ success: true }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (urlStr.includes("/api/backup/complete")) {
|
|
28
|
+
const body = JSON.parse(init.body);
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
status: 200,
|
|
32
|
+
json: async () => ({
|
|
33
|
+
success: true,
|
|
34
|
+
backupId: body.backupId,
|
|
35
|
+
fileCount: body.fileCount,
|
|
36
|
+
totalBytes: body.totalBytes,
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { ok: false, status: 404 };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const progressUpdates: any[] = [];
|
|
44
|
+
const controller = new BackupController({
|
|
45
|
+
clientId: "test-client-backup",
|
|
46
|
+
token: "secret-token",
|
|
47
|
+
serverBaseUrl: "http://127.0.0.1:8888",
|
|
48
|
+
fileEnumerator: async () => Object.keys(mockFiles),
|
|
49
|
+
fileReader: async (path: string) => mockFiles[path] || null,
|
|
50
|
+
onProgress: (p) => progressUpdates.push(p),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = await controller.initiate();
|
|
54
|
+
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(result.fileCount).toBe(2);
|
|
57
|
+
expect(result.totalBytes).toBeGreaterThan(0);
|
|
58
|
+
expect(result.backupId).toBeTruthy();
|
|
59
|
+
|
|
60
|
+
// Verify upload calls
|
|
61
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
62
|
+
expect.stringContaining("/api/backup/upload"),
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: expect.objectContaining({
|
|
66
|
+
"x-control-token": "secret-token",
|
|
67
|
+
}),
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Verify complete call
|
|
72
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
73
|
+
expect.stringContaining("/api/backup/complete"),
|
|
74
|
+
expect.objectContaining({
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: expect.objectContaining({
|
|
77
|
+
"x-control-token": "secret-token",
|
|
78
|
+
}),
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(progressUpdates.length).toBe(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("handles upload failure gracefully", async () => {
|
|
86
|
+
mockFetch.mockResolvedValueOnce({
|
|
87
|
+
ok: false,
|
|
88
|
+
status: 500,
|
|
89
|
+
text: async () => "Internal Server Error",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const controller = new BackupController({
|
|
93
|
+
clientId: "test-err-client",
|
|
94
|
+
serverBaseUrl: "http://127.0.0.1:8888",
|
|
95
|
+
fileEnumerator: async () => ["file1.txt"],
|
|
96
|
+
fileReader: async () => "content",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await expect(controller.initiate()).rejects.toThrow(/Upload failed/i);
|
|
100
|
+
});
|
|
101
|
+
});
|