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.
Files changed (112) hide show
  1. package/.well-known/mcp/server-card.json +1 -1
  2. package/.well-known/mcp.json +1 -1
  3. package/README.md +9 -9
  4. package/dist/cli/build/build.js +1 -1
  5. package/dist/cli/{chunk-DYq-orHK.js → chunk-BgTv9w0K.js} +1 -1
  6. package/dist/cli/{chunk-C6jii2Fl.js → chunk-d5hIhj8t.js} +1 -1
  7. package/dist/cli/cli.js +1 -1
  8. package/dist/lib/components/index.js +1 -1
  9. package/dist/lib/components/shadow-claw-pages.js +1 -1
  10. package/dist/lib/index.js +1 -1
  11. package/dist/lib/{shadow-claw-pages-kA2qa-4B.js → shadow-claw-pages-CjHS50HN.js} +2 -2
  12. package/dist/lib/{shadow-claw-pages-kA2qa-4B.js.map → shadow-claw-pages-CjHS50HN.js.map} +1 -1
  13. package/dist/public/.well-known/mcp/server-card.json +1 -1
  14. package/dist/public/.well-known/mcp.json +1 -1
  15. package/dist/public/AGENTS.md +1 -1
  16. package/dist/public/README.md +9 -9
  17. package/dist/public/docs/README.md +1 -1
  18. package/dist/public/docs/architecture/orchestrator.md +2 -2
  19. package/dist/public/docs/architecture/overview.md +3 -3
  20. package/dist/public/docs/architecture/worker-protocol.md +29 -29
  21. package/dist/public/docs/example/article/index.html +1 -1
  22. package/dist/public/docs/example/article.html +1 -1
  23. package/dist/public/docs/publishing/index.html +1 -1
  24. package/dist/public/docs/publishing.html +1 -1
  25. package/dist/public/docs/skill-creator/index.html +1 -1
  26. package/dist/public/docs/skill-creator.html +1 -1
  27. package/dist/public/docs/subsystems/local-models.md +1 -1
  28. package/dist/public/docs/subsystems/providers.md +11 -4
  29. package/dist/public/e2e/README.md +2 -2
  30. package/dist/public/index.html +1 -1
  31. package/dist/public/index.js +1 -1
  32. package/dist/public/llms.txt +1 -1
  33. package/dist/public/main/index.html +1 -1
  34. package/dist/public/main/memory/index.html +1 -1
  35. package/dist/public/main/memory.html +1 -1
  36. package/dist/public/main.html +1 -1
  37. package/dist/public/service-worker.js +1 -1
  38. package/dist/public/{shadow-claw-Ba2ZFE9m.js → shadow-claw-T9oPpqxT.js} +1 -1
  39. package/dist/public/{shadow-claw-pages-Cy6T0G9y.js → shadow-claw-pages-IW_LNEL_.js} +1 -1
  40. package/package.json +1 -1
  41. package/src/cli/build/build.test.ts +95 -0
  42. package/src/cli/build/build.ts +5 -1
  43. package/src/cli/commands/agent.test.ts +134 -0
  44. package/src/cli/commands/backup.test.ts +85 -0
  45. package/src/cli/commands/clients.test.ts +32 -0
  46. package/src/cli/commands/mcp.test.ts +260 -1
  47. package/src/cli/commands/send-file.test.ts +73 -0
  48. package/src/cli/commands/send.test.ts +58 -0
  49. package/src/cli/commands/tasks.test.ts +78 -0
  50. package/src/cli/commands/webrtc-listen.test.ts +74 -0
  51. package/src/cli/tools/assert-version-bump.test.ts +30 -1
  52. package/src/cli/tools/file-search-replace.test.ts +33 -1
  53. package/src/cli/tools/patch-service-worker-trusted-types.test.ts +24 -1
  54. package/src/cli/tools/touch-nojekyll.test.ts +12 -0
  55. package/src/cli/tools/version.test.ts +109 -5
  56. package/src/cli/utils/agent-core.test.ts +40 -0
  57. package/src/cli/utils/control-client.test.ts +239 -2
  58. package/src/cli/utils/load-workspace-config.test.ts +63 -0
  59. package/src/cli/utils/local-models.test.ts +98 -0
  60. package/src/cli/utils/progress-bar.test.ts +41 -2
  61. package/src/cli/utils/resolve-cache-dir.test.ts +61 -0
  62. package/src/cli/utils/resolve-project-root.test.ts +17 -0
  63. package/src/cli/utils/stdin.test.ts +38 -0
  64. package/src/cli/utils/stdio.test.ts +51 -1
  65. package/src/cli/utils/suppress-warnings.test.ts +9 -0
  66. package/src/cli/utils/webrtc-control-client.test.ts +569 -0
  67. package/src/common/utils/frontmatter.test.ts +117 -0
  68. package/src/components/common/help/providers.test.ts +67 -0
  69. package/src/components/common/shadow-claw-provider-model-picker/shadow-claw-provider-model-picker.test.ts +121 -0
  70. package/src/components/settings/shadow-claw-accounts/shadow-claw-accounts.test.ts +131 -0
  71. package/src/components/settings/shadow-claw-git/shadow-claw-git.test.ts +130 -0
  72. package/src/components/settings/shadow-claw-integrations/connection-test-auth.test.ts +12 -0
  73. package/src/components/settings/shadow-claw-integrations/shadow-claw-integrations.test.ts +328 -0
  74. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +530 -8
  75. package/src/components/settings/shadow-claw-mcp-remote/shadow-claw-mcp-remote.test.ts +459 -0
  76. package/src/components/settings/shadow-claw-networking/shadow-claw-networking.test.ts +88 -0
  77. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +92 -0
  78. package/src/components/settings/shadow-claw-peerjs/shadow-claw-peerjs.test.ts +127 -0
  79. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +173 -0
  80. package/src/components/settings/shadow-claw-task-server/shadow-claw-task-server.test.ts +62 -0
  81. package/src/components/settings/shadow-claw-webvm/shadow-claw-webvm.test.ts +104 -0
  82. package/src/components/shadow-claw/shadow-claw.test.ts +112 -0
  83. package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.test.ts +109 -0
  84. package/src/components/shadow-claw-conversations/shadow-claw-conversations.test.ts +361 -0
  85. package/src/components/shadow-claw-dialog/shadow-claw-dialog.test.ts +76 -0
  86. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +307 -0
  87. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +398 -1
  88. package/src/components/shadow-claw-pages/shadow-claw-pages.test.ts +85 -0
  89. package/src/components/shadow-claw-pages/shadow-claw-pages.ts +2 -1
  90. package/src/components/shadow-claw-pdf-viewer/shadow-claw-pdf-viewer.test.ts +229 -0
  91. package/src/components/shadow-claw-settings/shadow-claw-settings.test.ts +233 -0
  92. package/src/components/shadow-claw-tasks/shadow-claw-tasks.test.ts +223 -1
  93. package/src/components/shadow-claw-toast/shadow-claw-toast.test.ts +79 -0
  94. package/src/components/shadow-claw-tools/shadow-claw-tools.test.ts +229 -1
  95. package/src/server/logger.test.ts +57 -0
  96. package/src/server/peer.test.ts +47 -0
  97. package/src/server/routes/gemini.test.ts +244 -0
  98. package/src/server/server-peer.test.ts +245 -11
  99. package/src/server/services/llamafile-manager.test.ts +668 -0
  100. package/src/share-target/pending-shares.test.ts +90 -0
  101. package/src/shell/fs.test.ts +125 -0
  102. package/src/shell/shell.test.ts +8 -0
  103. package/src/subsystems/git/credentials.test.ts +260 -0
  104. package/src/subsystems/git/git.test.ts +64 -0
  105. package/src/subsystems/providers/executeNativeAiTask.test.ts +78 -0
  106. package/src/subsystems/providers/utils/parseLocalModelToolCall.test.ts +57 -0
  107. package/src/worker/tools/builtin-ai/builtin-ai.test.ts +88 -0
  108. package/src/worker/tools/email/email.test.ts +358 -0
  109. package/src/worker/tools/email/utils/resolveIntegrationEmailAuth.test.ts +120 -0
  110. package/src/worker/tools/email/utils/resolveUniqueAttachmentPath.test.ts +80 -0
  111. package/src/worker/tools/fetch-file/fetch-file.test.ts +111 -0
  112. 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
  });