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
@@ -500,4 +500,332 @@ describe("shadow-claw-integrations", () => {
500
500
 
501
501
  document.body.removeChild(el);
502
502
  });
503
+
504
+ it("connectOAuthFromForm handles successful OAuth flow through polling and token exchange", async () => {
505
+ const el = new ShadowClawIntegrations();
506
+ document.body.appendChild(el);
507
+ await el.connectedCallback();
508
+
509
+ const oauthSlot = document.createElement("div");
510
+ oauthSlot.innerHTML = `
511
+ <select id="int-oauth-provider"><option value="google" selected>Google</option></select>
512
+ <input id="int-oauth-client-id" value="client-id-123" />
513
+ <input id="int-oauth-client-secret" value="secret-xyz" />
514
+ <input id="int-oauth-scope" value="https://mail.google.com/" />
515
+ <div data-region="oauth-status"></div>
516
+ <button data-action="connect-oauth"></button>
517
+ `;
518
+ el.shadowRoot?.appendChild(oauthSlot);
519
+
520
+ (global.fetch as any) = jest.fn((url: string) => {
521
+ if (url === "/oauth/authorize") {
522
+ return Promise.resolve({
523
+ ok: true,
524
+ status: 200,
525
+ json: async () => ({
526
+ state: "state-123",
527
+ authorizeUrl:
528
+ "https://accounts.google.com/o/oauth2/auth?state=state-123",
529
+ }),
530
+ });
531
+ }
532
+ if (url.startsWith("/oauth/session/")) {
533
+ return Promise.resolve({
534
+ ok: true,
535
+ status: 200,
536
+ json: async () => ({ status: "authorized" }),
537
+ });
538
+ }
539
+ if (url === "/oauth/token") {
540
+ return Promise.resolve({
541
+ ok: true,
542
+ status: 200,
543
+ json: async () => ({
544
+ accessToken: "test-access-token",
545
+ refreshToken: "test-refresh-token",
546
+ expiresIn: 3600,
547
+ scope: "mail.read",
548
+ tokenType: "Bearer",
549
+ }),
550
+ });
551
+ }
552
+ return Promise.reject(new Error("Unknown URL: " + url));
553
+ });
554
+
555
+ const windowOpenSpy = jest
556
+ .spyOn(window, "open")
557
+ .mockImplementation(() => null);
558
+ await el.connectOAuthFromForm(oauthSlot);
559
+
560
+ expect(windowOpenSpy).toHaveBeenCalledWith(
561
+ "https://accounts.google.com/o/oauth2/auth?state=state-123",
562
+ expect.any(String),
563
+ expect.any(String),
564
+ );
565
+ expect(el.pendingOauthResult).toEqual(
566
+ expect.objectContaining({
567
+ providerId: "google",
568
+ accessToken: "test-access-token",
569
+ refreshToken: "test-refresh-token",
570
+ }),
571
+ );
572
+ expect(
573
+ oauthSlot.querySelector('[data-region="oauth-status"]')?.textContent,
574
+ ).toBe("OAuth connected");
575
+
576
+ // Also test missing client ID
577
+ (
578
+ oauthSlot.querySelector("#int-oauth-client-id") as HTMLInputElement
579
+ ).value = "";
580
+ await el.connectOAuthFromForm(oauthSlot);
581
+
582
+ windowOpenSpy.mockRestore();
583
+ document.body.removeChild(el);
584
+ });
585
+
586
+ it("upsertLinkedOAuthAccount handles full account linking and validation errors", async () => {
587
+ const el = new ShadowClawIntegrations();
588
+ document.body.appendChild(el);
589
+ await el.connectedCallback();
590
+ el.db = {} as any;
591
+
592
+ const savedConn = {
593
+ id: "conn-1",
594
+ label: "My IMAP",
595
+ pluginId: "imap",
596
+ enabled: true,
597
+ config: {},
598
+ credentialRef: null,
599
+ createdAt: 0,
600
+ updatedAt: 0,
601
+ } as any;
602
+
603
+ const slot = document.createElement("div");
604
+ slot.innerHTML = `
605
+ <select id="int-auth-mode"><option value="oauth" selected>OAuth</option></select>
606
+ <input id="int-username" value="user@gmail.com" />
607
+ <select id="int-oauth-provider"><option value="google" selected>Google</option></select>
608
+ <input id="int-oauth-client-id" value="client-id-123" />
609
+ <input id="int-oauth-client-secret" value="secret-xyz" />
610
+ <input id="int-oauth-scope" value="https://mail.google.com/" />
611
+ `;
612
+
613
+ // 1. Missing token without pendingOauthResult throws
614
+ el.pendingOauthResult = null;
615
+ await expect(el.upsertLinkedOAuthAccount(slot, savedConn)).rejects.toThrow(
616
+ "Connect OAuth first to obtain an access token.",
617
+ );
618
+
619
+ // 2. Missing username throws
620
+ (slot.querySelector("#int-username") as HTMLInputElement).value = "";
621
+ await expect(el.upsertLinkedOAuthAccount(slot, savedConn)).rejects.toThrow(
622
+ "Email login address is required for OAuth authentication.",
623
+ );
624
+ (slot.querySelector("#int-username") as HTMLInputElement).value =
625
+ "user@gmail.com";
626
+
627
+ // 3. Missing client ID throws
628
+ (slot.querySelector("#int-oauth-client-id") as HTMLInputElement).value = "";
629
+ await expect(el.upsertLinkedOAuthAccount(slot, savedConn)).rejects.toThrow(
630
+ "OAuth client ID is required for OAuth authentication.",
631
+ );
632
+ (slot.querySelector("#int-oauth-client-id") as HTMLInputElement).value =
633
+ "client-id-123";
634
+
635
+ // 4. Success path with pendingOauthResult
636
+ el.pendingOauthResult = {
637
+ providerId: "google",
638
+ accessToken: "plain-access-token",
639
+ refreshToken: "plain-refresh-token",
640
+ expiresAt: Date.now() + 3600000,
641
+ scope: "https://mail.google.com/",
642
+ tokenType: "Bearer",
643
+ };
644
+
645
+ const account = await el.upsertLinkedOAuthAccount(slot, savedConn);
646
+ expect(account).toBeDefined();
647
+ expect(account.authMode).toBe("oauth");
648
+ expect(account.oauthClientId).toBe("client-id-123");
649
+ expect(account.token).toBe("encrypted");
650
+
651
+ // 5. Calling buildCredentialRef with oauth returns correct structure
652
+ const credRef = await el.buildCredentialRef(slot, savedConn);
653
+ expect(credRef).toEqual({
654
+ serviceType: "http_api",
655
+ authType: "oauth",
656
+ providerId: "google",
657
+ accountId: account.id,
658
+ username: "user@gmail.com",
659
+ });
660
+
661
+ document.body.removeChild(el);
662
+ });
663
+
664
+ it("buildCredentialRef enforces username and password rules for basic_userpass", async () => {
665
+ const el = new ShadowClawIntegrations();
666
+ document.body.appendChild(el);
667
+ await el.connectedCallback();
668
+
669
+ const savedConn = { id: "conn-1" } as any;
670
+ const slot = document.createElement("div");
671
+ slot.innerHTML = `
672
+ <select id="int-auth-mode"><option value="basic_userpass" selected>Password</option></select>
673
+ <input id="int-username" value="" />
674
+ <input id="int-password" value="" />
675
+ `;
676
+
677
+ // 1. Both empty -> returns null
678
+ const refNull = await el.buildCredentialRef(slot, savedConn);
679
+ expect(refNull).toBeNull();
680
+
681
+ // 2. Password without username -> throws
682
+ (slot.querySelector("#int-password") as HTMLInputElement).value = "pass123";
683
+ await expect(el.buildCredentialRef(slot, savedConn)).rejects.toThrow(
684
+ "Auth username is required when a password is configured.",
685
+ );
686
+
687
+ // 3. Username without password -> throws
688
+ (slot.querySelector("#int-password") as HTMLInputElement).value = "";
689
+ (slot.querySelector("#int-username") as HTMLInputElement).value =
690
+ "user@test.com";
691
+ await expect(el.buildCredentialRef(slot, savedConn)).rejects.toThrow(
692
+ "Auth password is required when username is configured.",
693
+ );
694
+
695
+ document.body.removeChild(el);
696
+ });
697
+
698
+ it("renderConnectionList handles card action button clicks and delete confirmation cancel", async () => {
699
+ const el = new ShadowClawIntegrations();
700
+ document.body.appendChild(el);
701
+ await el.connectedCallback();
702
+ el.db = {} as any;
703
+
704
+ el.connections = [
705
+ {
706
+ id: "conn-1",
707
+ label: "My IMAP",
708
+ pluginId: "imap",
709
+ enabled: true,
710
+ config: { executionMode: "auto", pollIntervalSec: 120 },
711
+ credentialRef: { encryptedSecret: "enc" },
712
+ },
713
+ ] as any;
714
+
715
+ el.renderConnectionList();
716
+
717
+ const list = el.shadowRoot?.querySelector(
718
+ '[data-region="connection-list"]',
719
+ );
720
+ const editBtn = list?.querySelector(
721
+ 'button[data-action="edit"]',
722
+ ) as HTMLButtonElement;
723
+ const toggleBtn = list?.querySelector(
724
+ 'button[data-action="toggle"]',
725
+ ) as HTMLButtonElement;
726
+ const deleteBtn = list?.querySelector(
727
+ 'button[data-action="delete"]',
728
+ ) as HTMLButtonElement;
729
+
730
+ expect(editBtn).not.toBeNull();
731
+ expect(toggleBtn).not.toBeNull();
732
+ expect(deleteBtn).not.toBeNull();
733
+
734
+ // Click edit -> calls showForm
735
+ const showFormSpy = jest.spyOn(el, "showForm");
736
+ editBtn.click();
737
+ expect(showFormSpy).toHaveBeenCalledWith("conn-1");
738
+
739
+ // Click toggle
740
+ const toggleSpy = jest.spyOn(el, "toggleConnection");
741
+ toggleBtn.click();
742
+ expect(toggleSpy).toHaveBeenCalledWith("conn-1");
743
+
744
+ // Click delete with confirmation cancelled
745
+ el.requestConfirmation = jest.fn<any>().mockResolvedValue(false);
746
+ const { deleteEmailConnection } =
747
+ await import("../../../subsystems/email/connections.js");
748
+ (deleteEmailConnection as jest.Mock).mockClear();
749
+ await el.deleteConnection("conn-1");
750
+ expect(deleteEmailConnection).not.toHaveBeenCalled();
751
+
752
+ // toggleConnection with unknown id
753
+ await el.toggleConnection("non-existent-id");
754
+
755
+ document.body.removeChild(el);
756
+ });
757
+
758
+ it("saveForm parses form values, handles validation errors, extra config JSON, and cancel action", async () => {
759
+ const el = new ShadowClawIntegrations();
760
+ document.body.appendChild(el);
761
+ await el.connectedCallback();
762
+ el.db = {} as any;
763
+
764
+ el.manifests = [
765
+ {
766
+ id: "imap",
767
+ name: "IMAP",
768
+ configurableFields: ["port", "secure", "mailboxPath"],
769
+ description: "",
770
+ } as any,
771
+ ];
772
+ (getEmailPluginManifest as jest.Mock).mockReturnValue(el.manifests[0]);
773
+ el.showForm(null);
774
+
775
+ const slot = el.shadowRoot?.querySelector(
776
+ '[data-region="connection-form"]',
777
+ ) as HTMLElement;
778
+
779
+ // 1. Missing label shows error
780
+ (slot.querySelector("#int-label") as HTMLInputElement).value = "";
781
+ await el.saveForm();
782
+
783
+ // 2. Invalid extra config JSON shows error
784
+ (slot.querySelector("#int-label") as HTMLInputElement).value =
785
+ "Valid Label";
786
+ (slot.querySelector("#int-extra-config") as HTMLTextAreaElement).value =
787
+ "{ not-json";
788
+ await el.saveForm();
789
+
790
+ // 3. Valid config with data-config-fields and extra config JSON
791
+ (slot.querySelector("#int-extra-config") as HTMLTextAreaElement).value =
792
+ '{"customProp": true}';
793
+ const portInput = slot.querySelector("#cfg-port") as HTMLInputElement;
794
+ if (portInput) portInput.value = "993";
795
+ const secureInput = slot.querySelector("#cfg-secure") as HTMLInputElement;
796
+ if (secureInput) secureInput.value = "true";
797
+ const mailboxInput = slot.querySelector(
798
+ "#cfg-mailboxPath",
799
+ ) as HTMLInputElement;
800
+ if (mailboxInput) mailboxInput.value = "INBOX";
801
+
802
+ el.buildCredentialRef = jest.fn<any>().mockResolvedValue(null);
803
+ await el.saveForm();
804
+
805
+ const { upsertEmailConnection } =
806
+ await import("../../../subsystems/email/connections.js");
807
+ expect(upsertEmailConnection).toHaveBeenCalledWith(
808
+ el.db,
809
+ expect.objectContaining({
810
+ label: "Valid Label",
811
+ config: expect.objectContaining({
812
+ port: 993,
813
+ secure: true,
814
+ mailboxPath: "INBOX",
815
+ customProp: true,
816
+ }),
817
+ }),
818
+ );
819
+
820
+ // Cancel action hides form
821
+ el.showForm(null);
822
+ expect(slot.hidden).toBe(false);
823
+ const cancelBtn = slot.querySelector(
824
+ 'button[data-action="cancel"]',
825
+ ) as HTMLButtonElement;
826
+ cancelBtn.click();
827
+ expect(slot.hidden).toBe(true);
828
+
829
+ document.body.removeChild(el);
830
+ });
503
831
  });