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
|
@@ -322,4 +322,463 @@ describe("shadow-claw-mcp-remote", () => {
|
|
|
322
322
|
|
|
323
323
|
document.body.removeChild(el);
|
|
324
324
|
});
|
|
325
|
+
|
|
326
|
+
it("covers describeCredentialRef and getAuthSelectionFromCredentialRef", () => {
|
|
327
|
+
const el = new ShadowClawMcpRemote();
|
|
328
|
+
|
|
329
|
+
expect(el.describeCredentialRef(null)).toBe("No auth");
|
|
330
|
+
expect(
|
|
331
|
+
el.describeCredentialRef({
|
|
332
|
+
serviceType: "mcp_remote",
|
|
333
|
+
authType: "none",
|
|
334
|
+
providerId: "test",
|
|
335
|
+
}),
|
|
336
|
+
).toBe("No auth");
|
|
337
|
+
expect(
|
|
338
|
+
el.describeCredentialRef({
|
|
339
|
+
serviceType: "mcp_remote",
|
|
340
|
+
authType: "custom_header",
|
|
341
|
+
providerId: "test",
|
|
342
|
+
headerName: "X-Key",
|
|
343
|
+
}),
|
|
344
|
+
).toBe("Custom header (X-Key)");
|
|
345
|
+
expect(
|
|
346
|
+
el.describeCredentialRef({
|
|
347
|
+
serviceType: "mcp_remote",
|
|
348
|
+
authType: "custom_header",
|
|
349
|
+
providerId: "test",
|
|
350
|
+
}),
|
|
351
|
+
).toBe("Custom header");
|
|
352
|
+
expect(
|
|
353
|
+
el.describeCredentialRef({
|
|
354
|
+
serviceType: "mcp_remote",
|
|
355
|
+
authType: "oauth",
|
|
356
|
+
providerId: "test",
|
|
357
|
+
accountId: "acc-1",
|
|
358
|
+
}),
|
|
359
|
+
).toBe("Service account (OAUTH)");
|
|
360
|
+
expect(
|
|
361
|
+
el.describeCredentialRef({
|
|
362
|
+
serviceType: "mcp_remote",
|
|
363
|
+
authType: "token",
|
|
364
|
+
providerId: "test",
|
|
365
|
+
gitAccountId: "git-1",
|
|
366
|
+
}),
|
|
367
|
+
).toBe("Git account (TOKEN)");
|
|
368
|
+
expect(
|
|
369
|
+
el.describeCredentialRef({
|
|
370
|
+
serviceType: "mcp_remote",
|
|
371
|
+
authType: "basic" as any,
|
|
372
|
+
providerId: "test",
|
|
373
|
+
}),
|
|
374
|
+
).toBe("BASIC");
|
|
375
|
+
|
|
376
|
+
expect(el.getAuthSelectionFromCredentialRef(null)).toBe("none");
|
|
377
|
+
expect(
|
|
378
|
+
el.getAuthSelectionFromCredentialRef({
|
|
379
|
+
serviceType: "mcp_remote",
|
|
380
|
+
authType: "custom_header",
|
|
381
|
+
providerId: "test",
|
|
382
|
+
}),
|
|
383
|
+
).toBe("custom_header");
|
|
384
|
+
expect(
|
|
385
|
+
el.getAuthSelectionFromCredentialRef({
|
|
386
|
+
serviceType: "mcp_remote",
|
|
387
|
+
authType: "oauth",
|
|
388
|
+
accountId: "a1",
|
|
389
|
+
providerId: "test",
|
|
390
|
+
}),
|
|
391
|
+
).toBe("service_oauth");
|
|
392
|
+
expect(
|
|
393
|
+
el.getAuthSelectionFromCredentialRef({
|
|
394
|
+
serviceType: "mcp_remote",
|
|
395
|
+
authType: "token",
|
|
396
|
+
accountId: "a1",
|
|
397
|
+
providerId: "test",
|
|
398
|
+
}),
|
|
399
|
+
).toBe("service_pat");
|
|
400
|
+
expect(
|
|
401
|
+
el.getAuthSelectionFromCredentialRef({
|
|
402
|
+
serviceType: "mcp_remote",
|
|
403
|
+
authType: "oauth",
|
|
404
|
+
gitAccountId: "g1",
|
|
405
|
+
providerId: "test",
|
|
406
|
+
}),
|
|
407
|
+
).toBe("git_oauth");
|
|
408
|
+
expect(
|
|
409
|
+
el.getAuthSelectionFromCredentialRef({
|
|
410
|
+
serviceType: "mcp_remote",
|
|
411
|
+
authType: "token",
|
|
412
|
+
gitAccountId: "g1",
|
|
413
|
+
providerId: "test",
|
|
414
|
+
}),
|
|
415
|
+
).toBe("git_pat");
|
|
416
|
+
expect(
|
|
417
|
+
el.getAuthSelectionFromCredentialRef({
|
|
418
|
+
serviceType: "mcp_remote",
|
|
419
|
+
authType: "other" as any,
|
|
420
|
+
providerId: "test",
|
|
421
|
+
}),
|
|
422
|
+
).toBe("none");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("renders git and service account options", () => {
|
|
426
|
+
const el = new ShadowClawMcpRemote();
|
|
427
|
+
expect(el.renderGitAccountOptions()).toContain(
|
|
428
|
+
"No git accounts configured",
|
|
429
|
+
);
|
|
430
|
+
expect(el.renderServiceAccountOptions()).toContain(
|
|
431
|
+
"No service accounts configured",
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
el.gitAccounts = [
|
|
435
|
+
{
|
|
436
|
+
id: "git-1",
|
|
437
|
+
label: "GitHub Work",
|
|
438
|
+
hostPattern: "github.com",
|
|
439
|
+
username: "work",
|
|
440
|
+
createdAt: 1,
|
|
441
|
+
updatedAt: 1,
|
|
442
|
+
} as any,
|
|
443
|
+
];
|
|
444
|
+
expect(el.renderGitAccountOptions("git-1")).toContain(
|
|
445
|
+
'value="git-1" selected',
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
el.serviceAccounts = [
|
|
449
|
+
{
|
|
450
|
+
id: "srv-1",
|
|
451
|
+
label: "Srv Acc",
|
|
452
|
+
hostPattern: "api.example.com",
|
|
453
|
+
createdAt: 1,
|
|
454
|
+
updatedAt: 1,
|
|
455
|
+
} as any,
|
|
456
|
+
];
|
|
457
|
+
expect(el.renderServiceAccountOptions("srv-1")).toContain(
|
|
458
|
+
'value="srv-1" selected',
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("handles event listeners for adding, editing, reconnecting and closing", async () => {
|
|
463
|
+
mockListConnections.mockResolvedValue([
|
|
464
|
+
{
|
|
465
|
+
id: "conn-1",
|
|
466
|
+
label: "Remote 1",
|
|
467
|
+
serviceType: "mcp_remote",
|
|
468
|
+
serverUrl: "https://mcp1.com",
|
|
469
|
+
transport: "streamable_http",
|
|
470
|
+
enabled: true,
|
|
471
|
+
createdAt: 1,
|
|
472
|
+
updatedAt: 1,
|
|
473
|
+
credentialRef: {
|
|
474
|
+
serviceType: "mcp_remote",
|
|
475
|
+
authType: "oauth",
|
|
476
|
+
accountId: "acc-1",
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
]);
|
|
480
|
+
|
|
481
|
+
const el = new ShadowClawMcpRemote();
|
|
482
|
+
document.body.appendChild(el);
|
|
483
|
+
await el.connectedCallback();
|
|
484
|
+
|
|
485
|
+
// Click add-connection
|
|
486
|
+
const addBtn = el.shadowRoot?.querySelector(
|
|
487
|
+
'[data-action="add-connection"]',
|
|
488
|
+
) as HTMLElement;
|
|
489
|
+
addBtn?.click();
|
|
490
|
+
expect(el.editingConnectionId).toBe("new");
|
|
491
|
+
|
|
492
|
+
// Click cancel-connection-form
|
|
493
|
+
const cancelBtn = el.shadowRoot?.querySelector(
|
|
494
|
+
'[data-action="cancel-connection-form"]',
|
|
495
|
+
) as HTMLElement;
|
|
496
|
+
cancelBtn?.click();
|
|
497
|
+
expect(el.editingConnectionId).toBeNull();
|
|
498
|
+
|
|
499
|
+
// Trigger settings-action edit-connection
|
|
500
|
+
const listEl = el.shadowRoot?.querySelector(
|
|
501
|
+
'[data-region="connection-list"]',
|
|
502
|
+
);
|
|
503
|
+
listEl?.dispatchEvent(
|
|
504
|
+
new CustomEvent("settings-action", {
|
|
505
|
+
detail: { action: "edit-connection", id: "conn-1" },
|
|
506
|
+
bubbles: true,
|
|
507
|
+
}),
|
|
508
|
+
);
|
|
509
|
+
expect(el.editingConnectionId).toBe("conn-1");
|
|
510
|
+
|
|
511
|
+
// Trigger settings-action with empty id
|
|
512
|
+
listEl?.dispatchEvent(
|
|
513
|
+
new CustomEvent("settings-action", {
|
|
514
|
+
detail: { action: "edit-connection", id: "" },
|
|
515
|
+
bubbles: true,
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
// Trigger reconnect click
|
|
520
|
+
const reconnectBtn = el.shadowRoot?.querySelector(
|
|
521
|
+
'[data-reconnect-connection="conn-1"]',
|
|
522
|
+
) as HTMLElement;
|
|
523
|
+
reconnectBtn?.click();
|
|
524
|
+
|
|
525
|
+
// Click something other than HTMLButtonElement in connection-list
|
|
526
|
+
listEl?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
527
|
+
|
|
528
|
+
document.body.removeChild(el);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("handles auth selection changes and visibility updates", async () => {
|
|
532
|
+
const el = new ShadowClawMcpRemote();
|
|
533
|
+
document.body.appendChild(el);
|
|
534
|
+
await el.connectedCallback();
|
|
535
|
+
|
|
536
|
+
el.showConnectionForm("new");
|
|
537
|
+
const slot = el.shadowRoot?.querySelector(
|
|
538
|
+
'[data-region="connection-form-slot"]',
|
|
539
|
+
) as HTMLElement;
|
|
540
|
+
const authSelect = slot.querySelector(
|
|
541
|
+
'[data-field="auth-selection"]',
|
|
542
|
+
) as HTMLSelectElement;
|
|
543
|
+
|
|
544
|
+
authSelect.value = "service_pat";
|
|
545
|
+
authSelect.dispatchEvent(new Event("change"));
|
|
546
|
+
expect(
|
|
547
|
+
(
|
|
548
|
+
slot.querySelector(
|
|
549
|
+
'[data-region="service-account-region"]',
|
|
550
|
+
) as HTMLElement
|
|
551
|
+
).style.display,
|
|
552
|
+
).toBe("block");
|
|
553
|
+
|
|
554
|
+
authSelect.value = "git_oauth";
|
|
555
|
+
authSelect.dispatchEvent(new Event("change"));
|
|
556
|
+
expect(
|
|
557
|
+
(slot.querySelector('[data-region="git-account-region"]') as HTMLElement)
|
|
558
|
+
.style.display,
|
|
559
|
+
).toBe("block");
|
|
560
|
+
|
|
561
|
+
authSelect.value = "custom_header";
|
|
562
|
+
authSelect.dispatchEvent(new Event("change"));
|
|
563
|
+
expect(
|
|
564
|
+
(
|
|
565
|
+
slot.querySelector(
|
|
566
|
+
'[data-region="custom-header-region"]',
|
|
567
|
+
) as HTMLElement
|
|
568
|
+
).style.display,
|
|
569
|
+
).toBe("block");
|
|
570
|
+
|
|
571
|
+
document.body.removeChild(el);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("handles buildCredentialRef errors and variations", async () => {
|
|
575
|
+
const el = new ShadowClawMcpRemote();
|
|
576
|
+
const slot = document.createElement("div");
|
|
577
|
+
slot.innerHTML = `
|
|
578
|
+
<select data-field="service-account-id"><option value="">None</option></select>
|
|
579
|
+
<select data-field="git-account-id"><option value="">None</option></select>
|
|
580
|
+
<input data-field="custom-header-name" value="" />
|
|
581
|
+
<input data-field="custom-header-value" value="" />
|
|
582
|
+
`;
|
|
583
|
+
|
|
584
|
+
// None
|
|
585
|
+
expect(await el.buildCredentialRef(slot, "none", null)).toBeNull();
|
|
586
|
+
|
|
587
|
+
// Service PAT without account
|
|
588
|
+
await expect(
|
|
589
|
+
el.buildCredentialRef(slot, "service_pat", null),
|
|
590
|
+
).rejects.toThrow("Select a service account");
|
|
591
|
+
|
|
592
|
+
// Service PAT with account
|
|
593
|
+
slot.querySelector("select")!.innerHTML =
|
|
594
|
+
'<option value="srv-1">Srv</option>';
|
|
595
|
+
slot.querySelector("select")!.value = "srv-1";
|
|
596
|
+
const ref1 = await el.buildCredentialRef(slot, "service_pat", null);
|
|
597
|
+
expect(ref1?.authType).toBe("token");
|
|
598
|
+
expect(ref1?.accountId).toBe("srv-1");
|
|
599
|
+
|
|
600
|
+
// Git PAT without account
|
|
601
|
+
await expect(el.buildCredentialRef(slot, "git_pat", null)).rejects.toThrow(
|
|
602
|
+
"Select a git account",
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// Git OAuth with account
|
|
606
|
+
const gitSelect = slot.querySelector(
|
|
607
|
+
'[data-field="git-account-id"]',
|
|
608
|
+
) as HTMLSelectElement;
|
|
609
|
+
gitSelect.innerHTML = '<option value="git-1">Git</option>';
|
|
610
|
+
gitSelect.value = "git-1";
|
|
611
|
+
const ref2 = await el.buildCredentialRef(slot, "git_oauth", null);
|
|
612
|
+
expect(ref2?.authType).toBe("oauth");
|
|
613
|
+
expect(ref2?.gitAccountId).toBe("git-1");
|
|
614
|
+
|
|
615
|
+
// Custom header missing name
|
|
616
|
+
await expect(
|
|
617
|
+
el.buildCredentialRef(slot, "custom_header", null),
|
|
618
|
+
).rejects.toThrow("Header name is required");
|
|
619
|
+
|
|
620
|
+
// Custom header missing value
|
|
621
|
+
(
|
|
622
|
+
slot.querySelector(
|
|
623
|
+
'[data-field="custom-header-name"]',
|
|
624
|
+
) as HTMLInputElement
|
|
625
|
+
).value = "X-API-Key";
|
|
626
|
+
await expect(
|
|
627
|
+
el.buildCredentialRef(slot, "custom_header", null),
|
|
628
|
+
).rejects.toThrow("Header value is required");
|
|
629
|
+
|
|
630
|
+
// Custom header reusing existing encrypted value
|
|
631
|
+
const existing = {
|
|
632
|
+
id: "c1",
|
|
633
|
+
label: "C1",
|
|
634
|
+
serviceType: "mcp_remote" as const,
|
|
635
|
+
serverUrl: "http://example.com",
|
|
636
|
+
transport: "streamable_http" as const,
|
|
637
|
+
enabled: true,
|
|
638
|
+
autoReconnectOAuth: false,
|
|
639
|
+
createdAt: 1,
|
|
640
|
+
updatedAt: 1,
|
|
641
|
+
credentialRef: {
|
|
642
|
+
serviceType: "mcp_remote" as const,
|
|
643
|
+
authType: "custom_header" as const,
|
|
644
|
+
providerId: "custom_mcp",
|
|
645
|
+
headerName: "X-API-Key",
|
|
646
|
+
encryptedValue: "existing-encrypted",
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
const ref3 = await el.buildCredentialRef(slot, "custom_header", existing);
|
|
650
|
+
expect(ref3?.encryptedValue).toBe("existing-encrypted");
|
|
651
|
+
|
|
652
|
+
// Custom header with new raw value
|
|
653
|
+
(
|
|
654
|
+
slot.querySelector(
|
|
655
|
+
'[data-field="custom-header-value"]',
|
|
656
|
+
) as HTMLInputElement
|
|
657
|
+
).value = "secret123";
|
|
658
|
+
const ref4 = await el.buildCredentialRef(slot, "custom_header", null);
|
|
659
|
+
expect(ref4?.encryptedValue).toBe("enc-secret");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("handles form validation, save errors, and updating existing connection", async () => {
|
|
663
|
+
const el = new ShadowClawMcpRemote();
|
|
664
|
+
document.body.appendChild(el);
|
|
665
|
+
await el.connectedCallback();
|
|
666
|
+
|
|
667
|
+
el.showConnectionForm("new");
|
|
668
|
+
// Missing label & URL
|
|
669
|
+
await el.saveConnectionForm();
|
|
670
|
+
expect(mockUpsertConnection).not.toHaveBeenCalled();
|
|
671
|
+
|
|
672
|
+
// Fill label & URL but mockUpsertConnection fails
|
|
673
|
+
const labelInput = el.shadowRoot?.querySelector(
|
|
674
|
+
'[data-field="connection-label"]',
|
|
675
|
+
) as HTMLInputElement;
|
|
676
|
+
const urlInput = el.shadowRoot?.querySelector(
|
|
677
|
+
'[data-field="connection-url"]',
|
|
678
|
+
) as HTMLInputElement;
|
|
679
|
+
labelInput.value = "Test MCP";
|
|
680
|
+
urlInput.value = "https://example.com";
|
|
681
|
+
|
|
682
|
+
mockUpsertConnection.mockRejectedValueOnce(new Error("Database error"));
|
|
683
|
+
await el.saveConnectionForm();
|
|
684
|
+
|
|
685
|
+
// Update existing connection
|
|
686
|
+
el.connections = [
|
|
687
|
+
{
|
|
688
|
+
id: "existing-1",
|
|
689
|
+
label: "Old",
|
|
690
|
+
serviceType: "mcp_remote",
|
|
691
|
+
serverUrl: "https://old.com",
|
|
692
|
+
transport: "streamable_http",
|
|
693
|
+
enabled: true,
|
|
694
|
+
autoReconnectOAuth: false,
|
|
695
|
+
createdAt: 1,
|
|
696
|
+
updatedAt: 1,
|
|
697
|
+
credentialRef: null,
|
|
698
|
+
},
|
|
699
|
+
];
|
|
700
|
+
el.showConnectionForm("existing-1");
|
|
701
|
+
mockUpsertConnection.mockResolvedValueOnce({
|
|
702
|
+
id: "existing-1",
|
|
703
|
+
label: "New",
|
|
704
|
+
serviceType: "mcp_remote",
|
|
705
|
+
serverUrl: "https://new.com",
|
|
706
|
+
transport: "streamable_http",
|
|
707
|
+
enabled: true,
|
|
708
|
+
createdAt: 1,
|
|
709
|
+
updatedAt: 2,
|
|
710
|
+
});
|
|
711
|
+
await el.saveConnectionForm();
|
|
712
|
+
expect(mockUpsertConnection).toHaveBeenCalledWith(
|
|
713
|
+
{},
|
|
714
|
+
expect.objectContaining({ id: "existing-1" }),
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
document.body.removeChild(el);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("handles failure branches in deleteConnection, reconnectOAuth, and testConnection", async () => {
|
|
721
|
+
const el = new ShadowClawMcpRemote() as any;
|
|
722
|
+
document.body.appendChild(el);
|
|
723
|
+
await el.connectedCallback();
|
|
724
|
+
el.db = {} as any;
|
|
725
|
+
el.connections = [];
|
|
726
|
+
|
|
727
|
+
// Delete connection not found
|
|
728
|
+
mockDeleteConnection.mockResolvedValueOnce(false);
|
|
729
|
+
await el.deleteConnection("missing-id");
|
|
730
|
+
|
|
731
|
+
// Delete connection throws
|
|
732
|
+
mockDeleteConnection.mockRejectedValueOnce(new Error("Delete crash"));
|
|
733
|
+
await el.deleteConnection("err-id");
|
|
734
|
+
|
|
735
|
+
// Reconnect failure
|
|
736
|
+
mockReconnectMcpOAuth.mockResolvedValueOnce({
|
|
737
|
+
success: false,
|
|
738
|
+
error: "OAuth expired",
|
|
739
|
+
});
|
|
740
|
+
await el.reconnectOAuth("conn-fail");
|
|
741
|
+
|
|
742
|
+
// testConnection with existing diagnostic element & dismiss button
|
|
743
|
+
mockTestConnection.mockResolvedValueOnce({
|
|
744
|
+
success: false,
|
|
745
|
+
error: "Timeout",
|
|
746
|
+
toolCount: 0,
|
|
747
|
+
toolNames: [],
|
|
748
|
+
steps: [{ step: "Handshake", status: "error", detail: "Timeout" }],
|
|
749
|
+
});
|
|
750
|
+
const card = document.createElement("div");
|
|
751
|
+
card.className = "connection-card";
|
|
752
|
+
card.setAttribute("data-connection-id", "conn-test");
|
|
753
|
+
const existingDiag = document.createElement("div");
|
|
754
|
+
existingDiag.className = "connection-diagnostic";
|
|
755
|
+
card.appendChild(existingDiag);
|
|
756
|
+
el.shadowRoot.appendChild(card);
|
|
757
|
+
|
|
758
|
+
await el.testConnection("conn-test");
|
|
759
|
+
expect(card.querySelector(".diagnostic-header")?.textContent).toContain(
|
|
760
|
+
"Connection Failed",
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
const closeBtn = card.querySelector(
|
|
764
|
+
'[data-action="close-diagnostic"]',
|
|
765
|
+
) as HTMLElement;
|
|
766
|
+
closeBtn?.click();
|
|
767
|
+
expect(card.querySelector(".connection-diagnostic")).toBeNull();
|
|
768
|
+
|
|
769
|
+
document.body.removeChild(el);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("catches render errors gracefully", async () => {
|
|
773
|
+
const spy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
774
|
+
mockListConnections.mockRejectedValueOnce(new Error("DB locked"));
|
|
775
|
+
|
|
776
|
+
const el = new ShadowClawMcpRemote();
|
|
777
|
+
document.body.appendChild(el);
|
|
778
|
+
await el.connectedCallback();
|
|
779
|
+
|
|
780
|
+
expect(spy).toHaveBeenCalled();
|
|
781
|
+
spy.mockRestore();
|
|
782
|
+
document.body.removeChild(el);
|
|
783
|
+
});
|
|
325
784
|
});
|
|
@@ -141,4 +141,92 @@ describe("shadow-claw-networking", () => {
|
|
|
141
141
|
|
|
142
142
|
document.body.removeChild(el);
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
it("handles onProxyToggle success and error", async () => {
|
|
146
|
+
const { showError, showSuccess } = await import("../../../ui/toast.js");
|
|
147
|
+
const { setUseProxy } =
|
|
148
|
+
await import("../../../core/orchestrator/utils/settings.js");
|
|
149
|
+
|
|
150
|
+
const orch = createOrchestratorStub();
|
|
151
|
+
(orchestratorStore as any).orchestrator = orch;
|
|
152
|
+
|
|
153
|
+
const el = new ShadowClawNetworking();
|
|
154
|
+
document.body.appendChild(el);
|
|
155
|
+
await waitForNetworkingReady(el);
|
|
156
|
+
|
|
157
|
+
// Toggle enabled
|
|
158
|
+
await el.onProxyToggle(true);
|
|
159
|
+
expect(setUseProxy).toHaveBeenCalledWith(orch, expect.anything(), true);
|
|
160
|
+
expect(showSuccess).toHaveBeenCalledWith("CORS Proxy enabled", 2500);
|
|
161
|
+
|
|
162
|
+
// Toggle disabled
|
|
163
|
+
await el.onProxyToggle(false);
|
|
164
|
+
expect(setUseProxy).toHaveBeenCalledWith(orch, expect.anything(), false);
|
|
165
|
+
expect(showSuccess).toHaveBeenCalledWith("CORS Proxy disabled", 2500);
|
|
166
|
+
|
|
167
|
+
// Error
|
|
168
|
+
(setUseProxy as any).mockRejectedValueOnce(new Error("Failed to write DB"));
|
|
169
|
+
await el.onProxyToggle(true);
|
|
170
|
+
expect(showError).toHaveBeenCalledWith(
|
|
171
|
+
"Error saving proxy setting: Failed to write DB",
|
|
172
|
+
6000,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Early return without orchestrator
|
|
176
|
+
el.orchestrator = null;
|
|
177
|
+
await el.onProxyToggle(true);
|
|
178
|
+
|
|
179
|
+
document.body.removeChild(el);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("handles saveProxyUrl error, event listener click, and early returns", async () => {
|
|
183
|
+
const { showError } = await import("../../../ui/toast.js");
|
|
184
|
+
const { setProxyUrl } =
|
|
185
|
+
await import("../../../core/orchestrator/utils/settings.js");
|
|
186
|
+
|
|
187
|
+
const orch = createOrchestratorStub();
|
|
188
|
+
(orchestratorStore as any).orchestrator = orch;
|
|
189
|
+
|
|
190
|
+
const el = new ShadowClawNetworking();
|
|
191
|
+
document.body.appendChild(el);
|
|
192
|
+
await waitForNetworkingReady(el);
|
|
193
|
+
|
|
194
|
+
const input = el.shadowRoot?.querySelector(
|
|
195
|
+
'[data-setting="proxy-url-input"]',
|
|
196
|
+
) as HTMLInputElement;
|
|
197
|
+
input.value = "https://proxy.example.com";
|
|
198
|
+
|
|
199
|
+
// Error saving
|
|
200
|
+
(setProxyUrl as any).mockRejectedValueOnce(
|
|
201
|
+
new Error("Network config error"),
|
|
202
|
+
);
|
|
203
|
+
await el.saveProxyUrl();
|
|
204
|
+
expect(showError).toHaveBeenCalledWith(
|
|
205
|
+
"Error saving proxy URL: Network config error",
|
|
206
|
+
6000,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Trigger save via button click
|
|
210
|
+
const saveBtn = el.shadowRoot?.querySelector(
|
|
211
|
+
'[data-action="save-proxy-url"]',
|
|
212
|
+
) as HTMLButtonElement;
|
|
213
|
+
input.value = "https://new-proxy.com";
|
|
214
|
+
saveBtn.click();
|
|
215
|
+
|
|
216
|
+
// Trigger toggle via checkbox change
|
|
217
|
+
const toggle = el.shadowRoot?.querySelector(
|
|
218
|
+
'[data-setting="proxy-toggle"]',
|
|
219
|
+
) as HTMLInputElement;
|
|
220
|
+
toggle.checked = true;
|
|
221
|
+
toggle.dispatchEvent(new Event("change"));
|
|
222
|
+
|
|
223
|
+
// Early returns in saveProxyUrl
|
|
224
|
+
el.orchestrator = null;
|
|
225
|
+
await el.saveProxyUrl();
|
|
226
|
+
|
|
227
|
+
// Render early return when orchestrator is null
|
|
228
|
+
await el.render();
|
|
229
|
+
|
|
230
|
+
document.body.removeChild(el);
|
|
231
|
+
});
|
|
144
232
|
});
|
|
@@ -235,4 +235,96 @@ describe("shadow-claw-notifications", () => {
|
|
|
235
235
|
|
|
236
236
|
document.body.removeChild(el);
|
|
237
237
|
});
|
|
238
|
+
|
|
239
|
+
it("handles push toggle error branch", async () => {
|
|
240
|
+
const el = new ShadowClawNotifications();
|
|
241
|
+
document.body.appendChild(el);
|
|
242
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
243
|
+
|
|
244
|
+
el._subscribed = false;
|
|
245
|
+
(
|
|
246
|
+
(pushClient as any).subscribeToPush as jest.Mock<any>
|
|
247
|
+
).mockRejectedValueOnce(new Error("Permission denied"));
|
|
248
|
+
|
|
249
|
+
const toggle = el.shadowRoot?.querySelector('[data-action="toggle-push"]');
|
|
250
|
+
toggle?.dispatchEvent(new Event("click"));
|
|
251
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
252
|
+
|
|
253
|
+
document.body.removeChild(el);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("handles refreshState failure branch", async () => {
|
|
257
|
+
(
|
|
258
|
+
(pushClient as any).getCurrentSubscription as jest.Mock<any>
|
|
259
|
+
).mockRejectedValue(new Error("SW failed"));
|
|
260
|
+
const el = new ShadowClawNotifications();
|
|
261
|
+
document.body.appendChild(el);
|
|
262
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
263
|
+
await el.refreshState();
|
|
264
|
+
|
|
265
|
+
expect(el._subscribed).toBe(false);
|
|
266
|
+
(
|
|
267
|
+
(pushClient as any).getCurrentSubscription as jest.Mock<any>
|
|
268
|
+
).mockResolvedValue(null);
|
|
269
|
+
document.body.removeChild(el);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("handles loadSubscriptions non-ok response", async () => {
|
|
273
|
+
const el = new ShadowClawNotifications();
|
|
274
|
+
document.body.appendChild(el);
|
|
275
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
276
|
+
|
|
277
|
+
global.fetch = jest.fn<any>().mockResolvedValue({
|
|
278
|
+
ok: false,
|
|
279
|
+
status: 500,
|
|
280
|
+
});
|
|
281
|
+
await el.loadSubscriptions();
|
|
282
|
+
expect(el._subscriptions.length).toBe(0);
|
|
283
|
+
expect(el._backendAvailable).toBe(false);
|
|
284
|
+
|
|
285
|
+
document.body.removeChild(el);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("handles delete subscription error and failure", async () => {
|
|
289
|
+
const el = new ShadowClawNotifications();
|
|
290
|
+
document.body.appendChild(el);
|
|
291
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
292
|
+
|
|
293
|
+
el._selectedId = 1;
|
|
294
|
+
global.fetch = jest.fn<any>().mockResolvedValue({ ok: false });
|
|
295
|
+
|
|
296
|
+
const deleteBtn = el.shadowRoot?.querySelector(
|
|
297
|
+
'[data-action="delete-subscription"]',
|
|
298
|
+
);
|
|
299
|
+
deleteBtn?.dispatchEvent(new Event("click"));
|
|
300
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
301
|
+
|
|
302
|
+
// Also throw
|
|
303
|
+
global.fetch = jest.fn<any>().mockRejectedValue(new Error("Network fail"));
|
|
304
|
+
deleteBtn?.dispatchEvent(new Event("click"));
|
|
305
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
306
|
+
|
|
307
|
+
document.body.removeChild(el);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("handles send notification failure", async () => {
|
|
311
|
+
const el = new ShadowClawNotifications();
|
|
312
|
+
document.body.appendChild(el);
|
|
313
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
314
|
+
|
|
315
|
+
el._subscriptions = [{ id: 1, endpoint: "test-end", created_at: "" }];
|
|
316
|
+
el._selectedId = 1;
|
|
317
|
+
global.fetch = jest.fn<any>().mockResolvedValue({
|
|
318
|
+
ok: false,
|
|
319
|
+
text: () => Promise.resolve("Bad request"),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const sendBtn = el.shadowRoot?.querySelector(
|
|
323
|
+
'[data-action="send-notification"]',
|
|
324
|
+
);
|
|
325
|
+
sendBtn?.dispatchEvent(new Event("click"));
|
|
326
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
327
|
+
|
|
328
|
+
document.body.removeChild(el);
|
|
329
|
+
});
|
|
238
330
|
});
|