shadow-claw 1.52.0 → 1.52.1
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/.well-known/mcp/server-card.json +1 -1
- package/.well-known/mcp.json +1 -1
- package/README.md +9 -9
- package/dist/cli/build/build.js +1 -1
- package/dist/cli/{chunk-DYq-orHK.js → chunk-BgTv9w0K.js} +1 -1
- package/dist/cli/{chunk-C6jii2Fl.js → chunk-d5hIhj8t.js} +1 -1
- package/dist/cli/cli.js +1 -1
- package/dist/lib/components/index.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/{shadow-claw-pages-kA2qa-4B.js → shadow-claw-pages-CjHS50HN.js} +2 -2
- package/dist/lib/{shadow-claw-pages-kA2qa-4B.js.map → shadow-claw-pages-CjHS50HN.js.map} +1 -1
- package/dist/public/.well-known/mcp/server-card.json +1 -1
- package/dist/public/.well-known/mcp.json +1 -1
- package/dist/public/AGENTS.md +1 -1
- package/dist/public/README.md +9 -9
- package/dist/public/docs/README.md +1 -1
- package/dist/public/docs/architecture/orchestrator.md +2 -2
- package/dist/public/docs/architecture/overview.md +3 -3
- package/dist/public/docs/architecture/worker-protocol.md +29 -29
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/example/article.html +1 -1
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/publishing.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/skill-creator.html +1 -1
- package/dist/public/docs/subsystems/local-models.md +1 -1
- package/dist/public/docs/subsystems/providers.md +11 -4
- package/dist/public/e2e/README.md +2 -2
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- 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/main/memory.html +1 -1
- package/dist/public/main.html +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-Ba2ZFE9m.js → shadow-claw-T9oPpqxT.js} +1 -1
- package/dist/public/{shadow-claw-pages-Cy6T0G9y.js → shadow-claw-pages-IW_LNEL_.js} +1 -1
- package/package.json +1 -1
- package/src/cli/build/build.test.ts +95 -0
- package/src/cli/build/build.ts +5 -1
- package/src/cli/commands/agent.test.ts +134 -0
- package/src/cli/commands/backup.test.ts +85 -0
- package/src/cli/commands/clients.test.ts +32 -0
- package/src/cli/commands/mcp.test.ts +260 -1
- package/src/cli/commands/send-file.test.ts +73 -0
- package/src/cli/commands/send.test.ts +58 -0
- package/src/cli/commands/tasks.test.ts +78 -0
- package/src/cli/commands/webrtc-listen.test.ts +74 -0
- package/src/cli/tools/assert-version-bump.test.ts +30 -1
- package/src/cli/tools/file-search-replace.test.ts +33 -1
- package/src/cli/tools/patch-service-worker-trusted-types.test.ts +24 -1
- package/src/cli/tools/touch-nojekyll.test.ts +12 -0
- package/src/cli/tools/version.test.ts +109 -5
- package/src/cli/utils/agent-core.test.ts +40 -0
- package/src/cli/utils/control-client.test.ts +239 -2
- package/src/cli/utils/load-workspace-config.test.ts +63 -0
- package/src/cli/utils/local-models.test.ts +98 -0
- package/src/cli/utils/progress-bar.test.ts +41 -2
- package/src/cli/utils/resolve-cache-dir.test.ts +61 -0
- package/src/cli/utils/resolve-project-root.test.ts +17 -0
- package/src/cli/utils/stdin.test.ts +38 -0
- package/src/cli/utils/stdio.test.ts +51 -1
- package/src/cli/utils/suppress-warnings.test.ts +9 -0
- package/src/cli/utils/webrtc-control-client.test.ts +569 -0
- package/src/common/utils/frontmatter.test.ts +117 -0
- package/src/components/common/help/providers.test.ts +67 -0
- package/src/components/common/shadow-claw-provider-model-picker/shadow-claw-provider-model-picker.test.ts +121 -0
- package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +131 -0
- package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +130 -0
- package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +12 -0
- package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +328 -0
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +530 -8
- package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +459 -0
- package/src/components/settings/shadow-claw-networking/shadow-claw-networking.test.ts +88 -0
- package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +92 -0
- package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +127 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +173 -0
- package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +62 -0
- package/src/components/settings/shadow-claw-webvm/shadow-claw-webvm.test.ts +104 -0
- package/src/components/shadow-claw/shadow-claw.test.ts +112 -0
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +109 -0
- package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +361 -0
- package/src/components/shadow-claw-dialog/shadow-claw-dialog.test.ts +76 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +307 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +398 -1
- package/src/components/shadow-claw-pages/shadow-claw-pages.test.ts +85 -0
- package/src/components/shadow-claw-pages/shadow-claw-pages.ts +2 -1
- package/src/components/shadow-claw-pdf-viewer/shadow-claw-pdf-viewer.test.ts +229 -0
- package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +233 -0
- package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +223 -1
- package/src/components/shadow-claw-toast/shadow-claw-toast.test.ts +79 -0
- package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +229 -1
- package/src/server/logger.test.ts +57 -0
- package/src/server/peer.test.ts +47 -0
- package/src/server/routes/gemini.test.ts +244 -0
- package/src/server/server-peer.test.ts +245 -11
- package/src/server/services/llamafile-manager.test.ts +668 -0
- package/src/share-target/pending-shares.test.ts +90 -0
- package/src/shell/fs.test.ts +125 -0
- package/src/shell/shell.test.ts +8 -0
- package/src/subsystems/git/credentials.test.ts +260 -0
- package/src/subsystems/git/git.test.ts +64 -0
- package/src/subsystems/providers/executeNativeAiTask.test.ts +78 -0
- package/src/subsystems/providers/utils/parseLocalModelToolCall.test.ts +57 -0
- package/src/worker/tools/builtin-ai/builtin-ai.test.ts +88 -0
- package/src/worker/tools/email/email.test.ts +358 -0
- package/src/worker/tools/email/utils/resolveIntegrationEmailAuth.test.ts +120 -0
- package/src/worker/tools/email/utils/resolveUniqueAttachmentPath.test.ts +80 -0
- package/src/worker/tools/fetch-file/fetch-file.test.ts +111 -0
- package/src/worker/tools/fetch-url/fetch-url.test.ts +295 -0
|
@@ -384,4 +384,131 @@ describe("shadow-claw-peerjs", () => {
|
|
|
384
384
|
|
|
385
385
|
document.body.removeChild(el);
|
|
386
386
|
});
|
|
387
|
+
|
|
388
|
+
it("handles room card button actions: invite, link/qr, and disband/leave", async () => {
|
|
389
|
+
const el = new ShadowClawPeerJs() as any;
|
|
390
|
+
document.body.appendChild(el);
|
|
391
|
+
await el.connectedCallback();
|
|
392
|
+
|
|
393
|
+
const { listRooms, inviteToRoom, leaveRoom } =
|
|
394
|
+
await import("../../../core/orchestrator/utils/operations/room.js");
|
|
395
|
+
|
|
396
|
+
const testRooms = [
|
|
397
|
+
{
|
|
398
|
+
roomId: "room-host-1",
|
|
399
|
+
name: "My Host Room",
|
|
400
|
+
hostPeerId: "test-peer-id",
|
|
401
|
+
members: [{ peerId: "test-peer-id", alias: "Host Me", kind: "human" }],
|
|
402
|
+
createdAt: Date.now(),
|
|
403
|
+
updatedAt: Date.now(),
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
(listRooms as jest.Mock<any>).mockReturnValue(testRooms);
|
|
407
|
+
|
|
408
|
+
el.renderRooms();
|
|
409
|
+
|
|
410
|
+
const card = el.shadowRoot?.querySelector(".peerjs-room-card");
|
|
411
|
+
expect(card).not.toBeNull();
|
|
412
|
+
|
|
413
|
+
// Invite button without selected peer (shows error)
|
|
414
|
+
const buttons = Array.from(card?.querySelectorAll("button") || []) as any[];
|
|
415
|
+
const inviteBtn = buttons.find((b: any) => b.textContent === "Invite");
|
|
416
|
+
const linkBtn = buttons.find((b: any) => b.textContent?.includes("Link"));
|
|
417
|
+
const disbandBtn = buttons.find((b: any) => b.textContent === "Disband");
|
|
418
|
+
const select = card?.querySelector("select");
|
|
419
|
+
|
|
420
|
+
// 1. Invite without peer
|
|
421
|
+
if (select) select.value = "";
|
|
422
|
+
inviteBtn?.dispatchEvent(new Event("click"));
|
|
423
|
+
|
|
424
|
+
// 2. Invite with peer
|
|
425
|
+
if (select) {
|
|
426
|
+
const opt = document.createElement("option");
|
|
427
|
+
opt.value = "trusted-1";
|
|
428
|
+
select.appendChild(opt);
|
|
429
|
+
select.value = "trusted-1";
|
|
430
|
+
}
|
|
431
|
+
inviteBtn?.dispatchEvent(new Event("click"));
|
|
432
|
+
expect(inviteToRoom).toHaveBeenCalledWith(
|
|
433
|
+
expect.anything(),
|
|
434
|
+
"room-host-1",
|
|
435
|
+
"trusted-1",
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// 3. Link/QR
|
|
439
|
+
linkBtn?.dispatchEvent(new Event("click"));
|
|
440
|
+
|
|
441
|
+
// 4. Disband
|
|
442
|
+
disbandBtn?.dispatchEvent(new Event("click"));
|
|
443
|
+
expect(leaveRoom).toHaveBeenCalledWith(expect.anything(), "room-host-1");
|
|
444
|
+
|
|
445
|
+
document.body.removeChild(el);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("handles adding trusted peers and auto-generating Peer ID on save", async () => {
|
|
449
|
+
const el = new ShadowClawPeerJs() as any;
|
|
450
|
+
document.body.appendChild(el);
|
|
451
|
+
await el.connectedCallback();
|
|
452
|
+
|
|
453
|
+
const root = el.shadowRoot!;
|
|
454
|
+
const idInput = root.querySelector(
|
|
455
|
+
'[data-setting="peerjs-new-trusted-id-input"]',
|
|
456
|
+
) as HTMLInputElement;
|
|
457
|
+
const aliasInput = root.querySelector(
|
|
458
|
+
'[data-setting="peerjs-new-trusted-alias-input"]',
|
|
459
|
+
) as HTMLInputElement;
|
|
460
|
+
const addBtn = root.querySelector('[data-action="add-trusted-peer"]');
|
|
461
|
+
|
|
462
|
+
if (idInput && aliasInput && addBtn) {
|
|
463
|
+
idInput.value = "peer-abc";
|
|
464
|
+
aliasInput.value = "Alice";
|
|
465
|
+
addBtn.dispatchEvent(new Event("click"));
|
|
466
|
+
|
|
467
|
+
expect(idInput.value).toBe("");
|
|
468
|
+
expect(aliasInput.value).toBe("");
|
|
469
|
+
|
|
470
|
+
const rows = root.querySelectorAll(".peerjs-trusted-peer-row");
|
|
471
|
+
expect(rows.length).toBeGreaterThan(0);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Clear myPeerIdInput to test auto-generation
|
|
475
|
+
const myPeerIdInput = root.querySelector(
|
|
476
|
+
'[data-setting="peerjs-my-peer-id-input"]',
|
|
477
|
+
) as HTMLInputElement;
|
|
478
|
+
if (myPeerIdInput) {
|
|
479
|
+
myPeerIdInput.value = "";
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
await el.savePeerJsConfig();
|
|
483
|
+
expect(mockConfigurePeerJs).toHaveBeenCalled();
|
|
484
|
+
|
|
485
|
+
document.body.removeChild(el);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("handles peerAliases in render", async () => {
|
|
489
|
+
const el = new ShadowClawPeerJs() as any;
|
|
490
|
+
document.body.appendChild(el);
|
|
491
|
+
await el.connectedCallback();
|
|
492
|
+
|
|
493
|
+
mockGetPeerJsConfig.mockReturnValueOnce({
|
|
494
|
+
enabled: true,
|
|
495
|
+
myPeerId: "peer-1",
|
|
496
|
+
myAlias: "Me",
|
|
497
|
+
trustedPeerIds: ["peer-2"],
|
|
498
|
+
peerAliases: { Bob: "peer-2" },
|
|
499
|
+
serverHost: "localhost",
|
|
500
|
+
serverPort: 9000,
|
|
501
|
+
serverPath: "/",
|
|
502
|
+
serverSecure: false,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
await el.render();
|
|
506
|
+
|
|
507
|
+
const status = el.shadowRoot?.querySelector(
|
|
508
|
+
'[data-info="peerjs-trusted-ids-status"]',
|
|
509
|
+
);
|
|
510
|
+
expect(status?.textContent).toContain("Bob");
|
|
511
|
+
|
|
512
|
+
document.body.removeChild(el);
|
|
513
|
+
});
|
|
387
514
|
});
|
|
@@ -32,6 +32,10 @@ jest.unstable_mockModule(
|
|
|
32
32
|
}),
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
+
jest.unstable_mockModule("../../../storage/selectStorageDirectory.js", () => ({
|
|
36
|
+
selectStorageDirectory: jest.fn<any>().mockResolvedValue(true),
|
|
37
|
+
}));
|
|
38
|
+
|
|
35
39
|
jest.unstable_mockModule("../../../db/db.js", () => ({
|
|
36
40
|
getDb: jest.fn<any>().mockResolvedValue({
|
|
37
41
|
transaction: jest.fn(() => ({
|
|
@@ -70,6 +74,7 @@ jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
|
|
|
70
74
|
storageStatus: { type: "opfs", permission: "granted" },
|
|
71
75
|
resetSiteConfigSeed: jest.fn<any>().mockResolvedValue(undefined),
|
|
72
76
|
loadFiles: jest.fn<any>().mockResolvedValue(undefined),
|
|
77
|
+
grantStorageAccess: jest.fn<any>().mockResolvedValue(undefined),
|
|
73
78
|
},
|
|
74
79
|
}));
|
|
75
80
|
|
|
@@ -191,4 +196,172 @@ describe("shadow-claw-storage", () => {
|
|
|
191
196
|
3500,
|
|
192
197
|
);
|
|
193
198
|
});
|
|
199
|
+
|
|
200
|
+
it("handles handleChangeStorageDir success and errors", async () => {
|
|
201
|
+
const { selectStorageDirectory } =
|
|
202
|
+
await import("../../../storage/selectStorageDirectory.js");
|
|
203
|
+
const { showError } = await import("../../../ui/toast.js");
|
|
204
|
+
|
|
205
|
+
const el = new ShadowClawStorage() as any;
|
|
206
|
+
el.db = {};
|
|
207
|
+
el.updateStorageInfo = jest.fn();
|
|
208
|
+
|
|
209
|
+
(selectStorageDirectory as jest.Mock<any>).mockResolvedValueOnce(true);
|
|
210
|
+
await el.handleChangeStorageDir();
|
|
211
|
+
expect(showSuccess).toHaveBeenCalledWith(
|
|
212
|
+
"Storage location changed. Existing OPFS files were not moved.",
|
|
213
|
+
4500,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
(selectStorageDirectory as jest.Mock<any>).mockRejectedValueOnce(
|
|
217
|
+
new Error("Directory pick error"),
|
|
218
|
+
);
|
|
219
|
+
await el.handleChangeStorageDir();
|
|
220
|
+
expect(showError).toHaveBeenCalledWith(
|
|
221
|
+
expect.stringContaining("Directory pick error"),
|
|
222
|
+
6000,
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("handles errors during storage operations", async () => {
|
|
227
|
+
const { showError } = await import("../../../ui/toast.js");
|
|
228
|
+
const { requestPersistentStorage } =
|
|
229
|
+
await import("../../../storage/requestPersistentStorage.js");
|
|
230
|
+
const { resetStorageDirectory } =
|
|
231
|
+
await import("../../../storage/storage.js");
|
|
232
|
+
const { orchestratorStore } =
|
|
233
|
+
await import("../../../stores/orchestrator.js");
|
|
234
|
+
|
|
235
|
+
const el = new ShadowClawStorage() as any;
|
|
236
|
+
el.db = {};
|
|
237
|
+
el.updateStorageInfo = jest.fn();
|
|
238
|
+
el.requestConfirmation = jest.fn().mockResolvedValue(true);
|
|
239
|
+
|
|
240
|
+
// 1. handleRequestPersistent error
|
|
241
|
+
(requestPersistentStorage as jest.Mock<any>).mockRejectedValueOnce(
|
|
242
|
+
new Error("Persistent error"),
|
|
243
|
+
);
|
|
244
|
+
await el.handleRequestPersistent();
|
|
245
|
+
expect(showError).toHaveBeenCalledWith(
|
|
246
|
+
expect.stringContaining("Persistent error"),
|
|
247
|
+
6000,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// 2. handleResetSiteConfig error
|
|
251
|
+
(
|
|
252
|
+
orchestratorStore.resetSiteConfigSeed as jest.Mock<any>
|
|
253
|
+
).mockRejectedValueOnce(new Error("Site config error"));
|
|
254
|
+
await el.handleResetSiteConfig();
|
|
255
|
+
expect(showError).toHaveBeenCalledWith(
|
|
256
|
+
expect.stringContaining("Site config error"),
|
|
257
|
+
6000,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// 3. handleResetStorageDir error
|
|
261
|
+
(resetStorageDirectory as jest.Mock<any>).mockRejectedValueOnce(
|
|
262
|
+
new Error("Reset storage error"),
|
|
263
|
+
);
|
|
264
|
+
await el.handleResetStorageDir();
|
|
265
|
+
expect(showError).toHaveBeenCalledWith(
|
|
266
|
+
expect.stringContaining("Reset storage error"),
|
|
267
|
+
6000,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// 4. onUploadAppendUlidToggle error
|
|
271
|
+
(setConfig as jest.Mock<any>).mockRejectedValueOnce(
|
|
272
|
+
new Error("ULID toggle error"),
|
|
273
|
+
);
|
|
274
|
+
await el.onUploadAppendUlidToggle(true);
|
|
275
|
+
expect(showError).toHaveBeenCalledWith(
|
|
276
|
+
expect.stringContaining("ULID toggle error"),
|
|
277
|
+
6000,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("handles requestConfirmation via appShell and fallback warning", async () => {
|
|
282
|
+
const { showWarning } = await import("../../../ui/toast.js");
|
|
283
|
+
const el = new ShadowClawStorage();
|
|
284
|
+
|
|
285
|
+
// 1. Fallback when shadow-claw element does not exist
|
|
286
|
+
const res1 = await el.requestConfirmation({
|
|
287
|
+
title: "Test",
|
|
288
|
+
message: "Confirm action?",
|
|
289
|
+
});
|
|
290
|
+
expect(res1).toBe(false);
|
|
291
|
+
expect(showWarning).toHaveBeenCalledWith("Confirm action?", 4500);
|
|
292
|
+
|
|
293
|
+
// 2. With appShell requestDialog
|
|
294
|
+
const appShell = document.createElement("shadow-claw") as any;
|
|
295
|
+
appShell.requestDialog = jest.fn().mockResolvedValue(true);
|
|
296
|
+
document.body.appendChild(appShell);
|
|
297
|
+
|
|
298
|
+
const res2 = await el.requestConfirmation({
|
|
299
|
+
title: "Test",
|
|
300
|
+
message: "Confirm action?",
|
|
301
|
+
});
|
|
302
|
+
expect(res2).toBe(true);
|
|
303
|
+
expect(appShell.requestDialog).toHaveBeenCalledWith(
|
|
304
|
+
expect.objectContaining({ mode: "confirm", title: "Test" }),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
document.body.removeChild(appShell);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("handles button clicks in template and early returns when db is null", async () => {
|
|
311
|
+
const el = new ShadowClawStorage();
|
|
312
|
+
document.body.appendChild(el);
|
|
313
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
314
|
+
el.db = {} as any;
|
|
315
|
+
|
|
316
|
+
const root = el.shadowRoot!;
|
|
317
|
+
const reqBtn = root.querySelector('[data-action="request-persistent"]');
|
|
318
|
+
const changeBtn = root.querySelector('[data-action="change-storage-dir"]');
|
|
319
|
+
const resetStorageBtn = root.querySelector(
|
|
320
|
+
'[data-action="reset-storage-dir"]',
|
|
321
|
+
);
|
|
322
|
+
const resetConfigBtn = root.querySelector(
|
|
323
|
+
'[data-action="reset-site-config"]',
|
|
324
|
+
);
|
|
325
|
+
const grantBtn = root.querySelector(
|
|
326
|
+
'[data-action="grant-storage-permission"]',
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const handleReqSpy = jest
|
|
330
|
+
.spyOn(el, "handleRequestPersistent")
|
|
331
|
+
.mockImplementation(async () => {});
|
|
332
|
+
const handleChangeSpy = jest
|
|
333
|
+
.spyOn(el, "handleChangeStorageDir")
|
|
334
|
+
.mockImplementation(async () => {});
|
|
335
|
+
const handleResetStorageSpy = jest
|
|
336
|
+
.spyOn(el, "handleResetStorageDir")
|
|
337
|
+
.mockImplementation(async () => {});
|
|
338
|
+
const handleResetConfigSpy = jest
|
|
339
|
+
.spyOn(el, "handleResetSiteConfig")
|
|
340
|
+
.mockImplementation(async () => {});
|
|
341
|
+
|
|
342
|
+
reqBtn?.dispatchEvent(new Event("click"));
|
|
343
|
+
expect(handleReqSpy).toHaveBeenCalled();
|
|
344
|
+
|
|
345
|
+
changeBtn?.dispatchEvent(new Event("click"));
|
|
346
|
+
expect(handleChangeSpy).toHaveBeenCalled();
|
|
347
|
+
|
|
348
|
+
resetStorageBtn?.dispatchEvent(new Event("click"));
|
|
349
|
+
expect(handleResetStorageSpy).toHaveBeenCalled();
|
|
350
|
+
|
|
351
|
+
resetConfigBtn?.dispatchEvent(new Event("click"));
|
|
352
|
+
expect(handleResetConfigSpy).toHaveBeenCalled();
|
|
353
|
+
|
|
354
|
+
grantBtn?.dispatchEvent(new Event("click"));
|
|
355
|
+
|
|
356
|
+
// Early returns when db is null
|
|
357
|
+
el.db = null;
|
|
358
|
+
await el.handleChangeStorageDir();
|
|
359
|
+
await el.handleRequestPersistent();
|
|
360
|
+
await el.handleResetSiteConfig();
|
|
361
|
+
await el.handleResetStorageDir();
|
|
362
|
+
await el.onUploadAppendUlidToggle(true);
|
|
363
|
+
await el.updateStorageInfo();
|
|
364
|
+
|
|
365
|
+
document.body.removeChild(el);
|
|
366
|
+
});
|
|
194
367
|
});
|
|
@@ -110,4 +110,66 @@ describe("shadow-claw-task-server", () => {
|
|
|
110
110
|
);
|
|
111
111
|
expect(mockEnsureAllConnections).toHaveBeenCalled();
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
it("handles event listeners for save button and toggle", async () => {
|
|
115
|
+
const el = new ShadowClawTaskServer();
|
|
116
|
+
document.body.appendChild(el);
|
|
117
|
+
await el.connectedCallback();
|
|
118
|
+
|
|
119
|
+
const input = el.shadowRoot?.querySelector(
|
|
120
|
+
'[data-setting="task-server-url-input"]',
|
|
121
|
+
) as HTMLInputElement;
|
|
122
|
+
input.value = "";
|
|
123
|
+
|
|
124
|
+
const saveBtn = el.shadowRoot?.querySelector(
|
|
125
|
+
'[data-action="save-task-server-url"]',
|
|
126
|
+
);
|
|
127
|
+
saveBtn?.dispatchEvent(new Event("click"));
|
|
128
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
129
|
+
|
|
130
|
+
expect(mockSetTaskServerUrl).toHaveBeenCalledWith(
|
|
131
|
+
orchestratorStore.orchestrator,
|
|
132
|
+
expect.anything(),
|
|
133
|
+
"/schedule",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const toggle = el.shadowRoot?.querySelector(
|
|
137
|
+
'[data-setting="task-server-enabled-toggle"]',
|
|
138
|
+
) as HTMLInputElement;
|
|
139
|
+
toggle.checked = true;
|
|
140
|
+
toggle.dispatchEvent(new Event("change"));
|
|
141
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
142
|
+
expect(mockSetTaskServerEnabled).toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("handles errors during save", async () => {
|
|
146
|
+
const { showError } = (await import("../../../ui/toast.js")) as any;
|
|
147
|
+
const el = new ShadowClawTaskServer();
|
|
148
|
+
document.body.appendChild(el);
|
|
149
|
+
await el.connectedCallback();
|
|
150
|
+
|
|
151
|
+
mockSetTaskServerEnabled.mockRejectedValueOnce(
|
|
152
|
+
new Error("Save enabled fail"),
|
|
153
|
+
);
|
|
154
|
+
await el.saveTaskServerEnabled(true);
|
|
155
|
+
expect(showError).toHaveBeenCalledWith(
|
|
156
|
+
expect.stringContaining("Save enabled fail"),
|
|
157
|
+
6000,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
mockSetTaskServerUrl.mockRejectedValueOnce(new Error("Save url fail"));
|
|
161
|
+
await el.saveTaskServerUrl();
|
|
162
|
+
expect(showError).toHaveBeenCalledWith(
|
|
163
|
+
expect.stringContaining("Save url fail"),
|
|
164
|
+
6000,
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("handles early returns when orchestrator is missing", async () => {
|
|
169
|
+
const el = new ShadowClawTaskServer();
|
|
170
|
+
el.orchestrator = null;
|
|
171
|
+
await el.saveTaskServerEnabled(true);
|
|
172
|
+
await el.saveTaskServerUrl();
|
|
173
|
+
await el.render();
|
|
174
|
+
});
|
|
113
175
|
});
|
|
@@ -222,4 +222,108 @@ describe("shadow-claw-webvm", () => {
|
|
|
222
222
|
|
|
223
223
|
document.body.removeChild(el);
|
|
224
224
|
});
|
|
225
|
+
|
|
226
|
+
it("handles validation warnings for invalid inputs", async () => {
|
|
227
|
+
const { showWarning } = await import("../../../ui/toast.js");
|
|
228
|
+
const orch = createOrchestratorStub();
|
|
229
|
+
(orchestratorStore as any).orchestrator = orch;
|
|
230
|
+
(orchestratorStore as any).ready = true;
|
|
231
|
+
|
|
232
|
+
const el = new ShadowClawWebvm();
|
|
233
|
+
document.body.appendChild(el);
|
|
234
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
235
|
+
await el.render();
|
|
236
|
+
|
|
237
|
+
// 1. Invalid bash timeout
|
|
238
|
+
const timeoutInput = el.shadowRoot?.querySelector(
|
|
239
|
+
'[data-setting="vm-bash-timeout-input"]',
|
|
240
|
+
) as HTMLInputElement;
|
|
241
|
+
if (timeoutInput) {
|
|
242
|
+
Object.defineProperty(timeoutInput, "value", {
|
|
243
|
+
value: "invalid",
|
|
244
|
+
configurable: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
await el.saveVMBashTimeout();
|
|
248
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
249
|
+
"Please enter a valid timeout in seconds",
|
|
250
|
+
3000,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// 2. Invalid boot host
|
|
254
|
+
const bootHostInput = el.shadowRoot?.querySelector(
|
|
255
|
+
'[data-setting="vm-boot-host-input"]',
|
|
256
|
+
) as HTMLInputElement;
|
|
257
|
+
if (bootHostInput) bootHostInput.value = "ftp://invalid-protocol";
|
|
258
|
+
await el.saveVMBootHost();
|
|
259
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
260
|
+
"Please enter a valid HTTP(S) boot host URL",
|
|
261
|
+
3500,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// 3. Invalid relay URL
|
|
265
|
+
const relayInput = el.shadowRoot?.querySelector(
|
|
266
|
+
'[data-setting="vm-network-relay-url-input"]',
|
|
267
|
+
) as HTMLInputElement;
|
|
268
|
+
if (relayInput) relayInput.value = "ftp://invalid-relay";
|
|
269
|
+
await el.saveVMNetworkRelayURL();
|
|
270
|
+
expect(showWarning).toHaveBeenCalledWith(
|
|
271
|
+
"Please enter a valid ws:// or wss:// relay URL",
|
|
272
|
+
3500,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
document.body.removeChild(el);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("handles errors during saving", async () => {
|
|
279
|
+
const { showError } = await import("../../../ui/toast.js");
|
|
280
|
+
const orch = createOrchestratorStub();
|
|
281
|
+
(orchestratorStore as any).orchestrator = orch;
|
|
282
|
+
(orchestratorStore as any).ready = true;
|
|
283
|
+
|
|
284
|
+
const el = new ShadowClawWebvm();
|
|
285
|
+
document.body.appendChild(el);
|
|
286
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
287
|
+
await el.render();
|
|
288
|
+
|
|
289
|
+
mockSetVMBashTimeout.mockRejectedValueOnce(
|
|
290
|
+
new Error("Timeout save failed"),
|
|
291
|
+
);
|
|
292
|
+
const timeoutInput = el.shadowRoot?.querySelector(
|
|
293
|
+
'[data-setting="vm-bash-timeout-input"]',
|
|
294
|
+
) as HTMLInputElement;
|
|
295
|
+
if (timeoutInput) timeoutInput.value = "30";
|
|
296
|
+
await el.saveVMBashTimeout();
|
|
297
|
+
expect(showError).toHaveBeenCalledWith(
|
|
298
|
+
expect.stringContaining("Timeout save failed"),
|
|
299
|
+
6000,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
mockSetVMBootHost.mockRejectedValueOnce(new Error("Boot host save failed"));
|
|
303
|
+
await el.saveVMBootHost();
|
|
304
|
+
expect(showError).toHaveBeenCalledWith(
|
|
305
|
+
expect.stringContaining("Boot host save failed"),
|
|
306
|
+
6000,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
mockSetVMNetworkRelayURL.mockRejectedValueOnce(
|
|
310
|
+
new Error("Relay URL save failed"),
|
|
311
|
+
);
|
|
312
|
+
await el.saveVMNetworkRelayURL();
|
|
313
|
+
expect(showError).toHaveBeenCalledWith(
|
|
314
|
+
expect.stringContaining("Relay URL save failed"),
|
|
315
|
+
6000,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
document.body.removeChild(el);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("handles missing orchestrator early returns", async () => {
|
|
322
|
+
const el = new ShadowClawWebvm();
|
|
323
|
+
el.orchestrator = null;
|
|
324
|
+
await el.saveVMBashTimeout();
|
|
325
|
+
await el.saveVMBootHost();
|
|
326
|
+
await el.saveVMBootMode();
|
|
327
|
+
await el.saveVMNetworkRelayURL();
|
|
328
|
+
});
|
|
225
329
|
});
|
|
@@ -696,4 +696,116 @@ describe("shadow-claw", () => {
|
|
|
696
696
|
globalThis.matchMedia = originalMatchMedia;
|
|
697
697
|
}
|
|
698
698
|
});
|
|
699
|
+
|
|
700
|
+
it("handles disconnectedCallback cleanups", async () => {
|
|
701
|
+
const component = new ShadowClaw();
|
|
702
|
+
component.orchestrator = createOrchestratorStub();
|
|
703
|
+
document.body.appendChild(component);
|
|
704
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
705
|
+
|
|
706
|
+
const vmStatusCleanup = jest.fn();
|
|
707
|
+
const appLifecycleCleanup = jest.fn();
|
|
708
|
+
const cancelAnimSpy = jest.spyOn(globalThis, "cancelAnimationFrame");
|
|
709
|
+
|
|
710
|
+
component.vmStatusCleanup = vmStatusCleanup;
|
|
711
|
+
component.appLifecycleCleanup = appLifecycleCleanup;
|
|
712
|
+
component.terminalPlacementFrame = 42;
|
|
713
|
+
component.popstateListener = jest.fn() as any;
|
|
714
|
+
component.fallbackClickListenerAttached = true;
|
|
715
|
+
component.navigationListenerAttached = true;
|
|
716
|
+
|
|
717
|
+
component.disconnectedCallback();
|
|
718
|
+
|
|
719
|
+
expect(vmStatusCleanup).toHaveBeenCalled();
|
|
720
|
+
expect(appLifecycleCleanup).toHaveBeenCalled();
|
|
721
|
+
expect(cancelAnimSpy).toHaveBeenCalledWith(42);
|
|
722
|
+
expect(component.vmStatusCleanup).toBeNull();
|
|
723
|
+
expect(component.appLifecycleCleanup).toBeNull();
|
|
724
|
+
expect(component.terminalPlacementFrame).toBeNull();
|
|
725
|
+
expect(component.popstateListener).toBeNull();
|
|
726
|
+
expect(component.fallbackClickListenerAttached).toBe(false);
|
|
727
|
+
expect(component.navigationListenerAttached).toBe(false);
|
|
728
|
+
|
|
729
|
+
document.body.removeChild(component);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it("handles navigation handlers, shadowClawNav, and requestDialog", async () => {
|
|
733
|
+
const component = new ShadowClaw();
|
|
734
|
+
component.orchestrator = createOrchestratorStub();
|
|
735
|
+
document.body.appendChild(component);
|
|
736
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
737
|
+
|
|
738
|
+
// fallbackClickListener with no targetPath
|
|
739
|
+
const mockClickEvent = new MouseEvent("click");
|
|
740
|
+
component.fallbackClickListener(mockClickEvent);
|
|
741
|
+
|
|
742
|
+
// handleNavigationApiNavigate
|
|
743
|
+
component.handleNavigationApiNavigate(new Event("navigate"));
|
|
744
|
+
|
|
745
|
+
// shadowClawNav
|
|
746
|
+
component.shadowClawNav(
|
|
747
|
+
new CustomEvent("shadow-claw-navigate", { detail: { page: "pages" } }),
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
// requestDialog
|
|
751
|
+
const promise = component.requestDialog({
|
|
752
|
+
title: "Test Dialog",
|
|
753
|
+
message: "Please confirm",
|
|
754
|
+
});
|
|
755
|
+
const appDialog = component.shadowRoot?.querySelector(
|
|
756
|
+
".app-dialog",
|
|
757
|
+
) as HTMLDialogElement;
|
|
758
|
+
if (appDialog) {
|
|
759
|
+
appDialog.returnValue = "confirm";
|
|
760
|
+
appDialog.dispatchEvent(new Event("close"));
|
|
761
|
+
}
|
|
762
|
+
const dialogResult = await promise;
|
|
763
|
+
expect(dialogResult).toBe(true);
|
|
764
|
+
|
|
765
|
+
document.body.removeChild(component);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it("handles orchestrator vm-status error and tools events", async () => {
|
|
769
|
+
const stub = createOrchestratorStub();
|
|
770
|
+
const component = new ShadowClaw();
|
|
771
|
+
component.orchestrator = stub;
|
|
772
|
+
document.body.appendChild(component);
|
|
773
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
774
|
+
|
|
775
|
+
// Emit vm-status with error when terminalVisible is true
|
|
776
|
+
component.terminalVisible = true;
|
|
777
|
+
const vmStatusCallbacks = stub.events.on.mock.calls.filter(
|
|
778
|
+
([event]) => event === "vm-status",
|
|
779
|
+
);
|
|
780
|
+
for (const [, cb] of vmStatusCallbacks) {
|
|
781
|
+
cb({ ready: false, booting: false, error: "VM Crash", mode: "disabled" });
|
|
782
|
+
}
|
|
783
|
+
expect(component.terminalVisible).toBe(false);
|
|
784
|
+
|
|
785
|
+
// Emit provider-help
|
|
786
|
+
const providerHelpCallbacks = stub.events.on.mock.calls.filter(
|
|
787
|
+
([event]) => event === "provider-help",
|
|
788
|
+
);
|
|
789
|
+
for (const [, cb] of providerHelpCallbacks) {
|
|
790
|
+
cb({ providerId: "test-prov", reason: "quota", helpType: "rate_limit" });
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Emit room-invite
|
|
794
|
+
const roomInviteCallbacks = stub.events.on.mock.calls.filter(
|
|
795
|
+
([event]) => event === "room-invite",
|
|
796
|
+
);
|
|
797
|
+
for (const [, cb] of roomInviteCallbacks) {
|
|
798
|
+
cb({ roomId: "r1", hostPeerId: "p1" });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Emit ask-user
|
|
802
|
+
const askUserCallbacks = stub.events.on.mock.calls.filter(
|
|
803
|
+
([event]) => event === "ask-user",
|
|
804
|
+
);
|
|
805
|
+
for (const [, cb] of askUserCallbacks) {
|
|
806
|
+
cb({ id: "q1", groupId: "g1", question: "Continue?" });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
document.body.removeChild(component);
|
|
810
|
+
});
|
|
699
811
|
});
|