@agent-native/dispatch 0.30.2 → 0.30.3

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.
@@ -2,6 +2,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  const mocks = vi.hoisted(() => ({
4
4
  consumeLinkToken: vi.fn(),
5
+ evaluateIntegrationScopePolicy: vi.fn(),
6
+ getActiveIntegrationInstallationByKey: vi.fn(),
7
+ getIntegrationScope: vi.fn(),
8
+ isOrgMember: vi.fn(),
5
9
  resolveLinkedOwner: vi.fn(),
6
10
  resolveOrgIdForEmail: vi.fn(),
7
11
  resolveSecret: vi.fn(),
@@ -19,11 +23,16 @@ vi.mock("@agent-native/core/integrations", async () => {
19
23
  >("@agent-native/core/integrations");
20
24
  return {
21
25
  ...actual,
26
+ getActiveIntegrationInstallationByKey:
27
+ mocks.getActiveIntegrationInstallationByKey,
28
+ evaluateIntegrationScopePolicy: mocks.evaluateIntegrationScopePolicy,
29
+ getIntegrationScope: mocks.getIntegrationScope,
22
30
  resolveSlackBotTokenForIncoming: mocks.resolveSlackBotTokenForIncoming,
23
31
  };
24
32
  });
25
33
 
26
34
  vi.mock("@agent-native/core/org", () => ({
35
+ isOrgMember: mocks.isOrgMember,
27
36
  resolveOrgIdForEmail: mocks.resolveOrgIdForEmail,
28
37
  }));
29
38
 
@@ -114,6 +123,10 @@ const noopAdapter: PlatformAdapter = {
114
123
  };
115
124
 
116
125
  beforeEach(() => {
126
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValue(null);
127
+ mocks.getIntegrationScope.mockResolvedValue(null);
128
+ mocks.evaluateIntegrationScopePolicy.mockReturnValue({ allowed: true });
129
+ mocks.isOrgMember.mockResolvedValue(false);
117
130
  mocks.resolveLinkedOwner.mockResolvedValue(null);
118
131
  mocks.consumeLinkToken.mockResolvedValue("owner@example.test");
119
132
  mocks.resolveOrgIdForEmail.mockResolvedValue(null);
@@ -127,6 +140,16 @@ beforeEach(() => {
127
140
  );
128
141
  });
129
142
 
143
+ function managedSlackInstallation(overrides: Record<string, unknown> = {}) {
144
+ return {
145
+ id: "installation-managed",
146
+ installationKey: "T-MANAGED",
147
+ ownerEmail: "installer@example.test",
148
+ orgId: "org-managed",
149
+ ...overrides,
150
+ };
151
+ }
152
+
130
153
  afterEach(() => {
131
154
  vi.unstubAllEnvs();
132
155
  vi.unstubAllGlobals();
@@ -374,7 +397,7 @@ describe("beforeDispatchProcess", () => {
374
397
  });
375
398
  });
376
399
 
377
- it("replies with linking guidance instead of silently dropping an unlinked Slack DM", async () => {
400
+ it("fails closed instead of silently dropping an unmatched Slack DM", async () => {
378
401
  vi.stubEnv("APP_URL", "https://dispatch.agent-native.test");
379
402
  const incoming = slackIncoming({
380
403
  triggerKind: "dm",
@@ -390,15 +413,15 @@ describe("beforeDispatchProcess", () => {
390
413
  const result = await beforeDispatchProcess(incoming, noopAdapter);
391
414
 
392
415
  expect(execution.ownerEmail).toMatch(/@integration\.local$/);
393
- expect(incoming.platformContext.identityLinkRequired).toBe(true);
416
+ expect(incoming.platformContext.identityVerificationFailed).toBe(true);
394
417
  expect(result).toEqual({
395
418
  handled: true,
396
419
  responseText:
397
- "Agent Native is ready, but this Slack account is not linked to an Agent Native user yet. Open https://dispatch.agent-native.test/identities, create a Slack link token, then send `/link <token>` in this DM.",
420
+ "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
398
421
  });
399
422
  });
400
423
 
401
- it("lets an unlinked Slack DM consume a link token before the agent gate", async () => {
424
+ it("does not let an unmatched Slack DM consume a link token", async () => {
402
425
  const incoming = slackIncoming({
403
426
  text: "/link token-123",
404
427
  triggerKind: "dm",
@@ -416,13 +439,561 @@ describe("beforeDispatchProcess", () => {
416
439
  expect(result).toEqual({
417
440
  handled: true,
418
441
  responseText:
419
- "Linked successfully. Future slack messages will use owner@example.test's personal dispatch context.",
442
+ "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
420
443
  });
444
+ expect(mocks.consumeLinkToken).not.toHaveBeenCalled();
445
+ });
446
+
447
+ it("scopes a managed Slack link claim to the installation organization", async () => {
448
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
449
+ managedSlackInstallation(),
450
+ );
451
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
452
+ "managed-token",
453
+ );
454
+ mocks.consumeLinkToken.mockRejectedValueOnce(
455
+ new Error(
456
+ "This link token belongs to a different organization. Create a token from this workspace and try again.",
457
+ ),
458
+ );
459
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
460
+ new Response(
461
+ JSON.stringify({
462
+ ok: true,
463
+ user: { profile: { email: "outside@example.test" } },
464
+ }),
465
+ ),
466
+ );
467
+ const incoming = slackIncoming({
468
+ text: "/link token-other-org",
469
+ senderId: "U-MANAGED-LINK",
470
+ triggerKind: "dm",
471
+ conversationType: "dm",
472
+ platformContext: {
473
+ teamId: "T-MANAGED",
474
+ channelId: "D-MANAGED",
475
+ channelType: "im",
476
+ },
477
+ });
478
+
479
+ await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
480
+ {
481
+ handled: true,
482
+ responseText:
483
+ "This link token belongs to a different organization. Create a token from this workspace and try again.",
484
+ },
485
+ );
421
486
  expect(mocks.consumeLinkToken).toHaveBeenCalledWith({
422
487
  platform: "slack",
423
- token: "token-123",
424
- externalUserId: "T123:U123",
488
+ token: "token-other-org",
489
+ externalUserId: "T-MANAGED:U-MANAGED-LINK",
425
490
  externalUserName: "U123",
491
+ expectedOrgId: "org-managed",
492
+ });
493
+ });
494
+
495
+ it("does not consume a managed Slack link token when identity verification fails", async () => {
496
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
497
+ managedSlackInstallation(),
498
+ );
499
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
500
+ "managed-token",
501
+ );
502
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
503
+ new Response(JSON.stringify({ ok: false })),
504
+ );
505
+ const incoming = slackIncoming({
506
+ text: "/link token-unverified",
507
+ senderId: "U-MANAGED-UNVERIFIED",
508
+ triggerKind: "dm",
509
+ conversationType: "dm",
510
+ platformContext: {
511
+ teamId: "T-MANAGED",
512
+ channelId: "D-MANAGED",
513
+ channelType: "im",
514
+ },
515
+ });
516
+
517
+ await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
518
+ {
519
+ handled: true,
520
+ responseText:
521
+ "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
522
+ },
523
+ );
524
+ expect(mocks.consumeLinkToken).not.toHaveBeenCalled();
525
+ });
526
+ });
527
+
528
+ describe("managed Slack execution identity", () => {
529
+ it("fails closed when no managed installation matches a Slack DM", async () => {
530
+ vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
531
+ const incoming = slackIncoming({
532
+ senderId: "U-UNMATCHED",
533
+ triggerKind: "dm",
534
+ conversationType: "dm",
535
+ platformContext: { teamId: "T-UNMATCHED", channelId: "D-UNMATCHED" },
536
+ });
537
+
538
+ const execution = await resolveDispatchExecutionContext(incoming);
539
+
540
+ expect(execution.ownerEmail).toMatch(/@integration\.local$/);
541
+ expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
542
+ expect(execution.orgId).toBeNull();
543
+ expect(incoming.platformContext.identityVerificationFailed).toBe(true);
544
+ });
545
+
546
+ it("uses the enterprise-scoped installation for an Enterprise Grid DM", async () => {
547
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
548
+ managedSlackInstallation(),
549
+ );
550
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
551
+ "managed-token",
552
+ );
553
+ mocks.isOrgMember.mockResolvedValueOnce(true);
554
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
555
+ new Response(
556
+ JSON.stringify({
557
+ ok: true,
558
+ user: { profile: { email: "alice@example.test" } },
559
+ }),
560
+ ),
561
+ );
562
+
563
+ await resolveDispatchExecutionContext(
564
+ slackIncoming({
565
+ senderId: "U-ENTERPRISE-ALICE",
566
+ triggerKind: "dm",
567
+ conversationType: "dm",
568
+ platformContext: {
569
+ teamId: "T-ENTERPRISE-WORKSPACE",
570
+ enterpriseId: "E-MANAGED",
571
+ isEnterpriseInstall: true,
572
+ apiAppId: "A-MANAGED",
573
+ channelId: "D-MANAGED",
574
+ channelType: "im",
575
+ },
576
+ }),
577
+ );
578
+
579
+ expect(mocks.getActiveIntegrationInstallationByKey).toHaveBeenCalledWith(
580
+ "slack",
581
+ "enterprise:E-MANAGED:app:A-MANAGED",
582
+ );
583
+ });
584
+
585
+ it("fails closed with retry guidance when managed installation lookup is unavailable", async () => {
586
+ vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
587
+ mocks.getActiveIntegrationInstallationByKey.mockRejectedValueOnce(
588
+ new Error("installation store unavailable"),
589
+ );
590
+ const incoming = slackIncoming({
591
+ senderId: "U-MANAGED-ALICE",
592
+ triggerKind: "dm",
593
+ conversationType: "dm",
594
+ platformContext: {
595
+ teamId: "T-MANAGED",
596
+ channelId: "D-MANAGED",
597
+ channelType: "im",
598
+ },
599
+ });
600
+
601
+ const execution = await resolveDispatchExecutionContext(incoming);
602
+
603
+ expect(execution.ownerEmail).toMatch(/@integration\.local$/);
604
+ expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
605
+ expect(execution.orgId).toBeNull();
606
+ await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
607
+ {
608
+ handled: true,
609
+ responseText:
610
+ "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
611
+ },
612
+ );
613
+ });
614
+
615
+ it("fails closed with retry guidance when linked identity lookup is unavailable", async () => {
616
+ vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
617
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
618
+ managedSlackInstallation(),
619
+ );
620
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
621
+ "managed-token",
622
+ );
623
+ mocks.resolveLinkedOwner.mockRejectedValueOnce(
624
+ new Error("identity link store unavailable"),
625
+ );
626
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
627
+ new Response(
628
+ JSON.stringify({
629
+ ok: true,
630
+ user: { profile: { email: "alice@example.test" } },
631
+ }),
632
+ ),
633
+ );
634
+ const incoming = slackIncoming({
635
+ senderId: "U-MANAGED-ALICE",
636
+ triggerKind: "dm",
637
+ conversationType: "dm",
638
+ platformContext: {
639
+ teamId: "T-MANAGED",
640
+ channelId: "D-MANAGED",
641
+ channelType: "im",
642
+ },
643
+ });
644
+
645
+ const execution = await resolveDispatchExecutionContext(incoming);
646
+
647
+ expect(execution.ownerEmail).toMatch(/@integration\.local$/);
648
+ expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
649
+ await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
650
+ {
651
+ handled: true,
652
+ responseText:
653
+ "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
654
+ },
655
+ );
656
+ });
657
+
658
+ it("does not let a stale identity link override the verified Slack email", async () => {
659
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
660
+ managedSlackInstallation(),
661
+ );
662
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
663
+ "managed-token",
664
+ );
665
+ mocks.resolveLinkedOwner.mockResolvedValueOnce("stale@example.test");
666
+ mocks.isOrgMember.mockImplementation(
667
+ async (_orgId, email) => email === "current@example.test",
668
+ );
669
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
670
+ new Response(
671
+ JSON.stringify({
672
+ ok: true,
673
+ user: { profile: { email: "current@example.test" } },
674
+ }),
675
+ ),
676
+ );
677
+ const incoming = slackIncoming({
678
+ senderId: "U-MANAGED-MEMBER",
679
+ triggerKind: "dm",
680
+ conversationType: "dm",
681
+ platformContext: {
682
+ teamId: "T-MANAGED",
683
+ channelId: "D-MANAGED",
684
+ channelType: "im",
685
+ },
686
+ });
687
+
688
+ await expect(
689
+ resolveDispatchExecutionContext(incoming),
690
+ ).resolves.toMatchObject({
691
+ ownerEmail: "current@example.test",
692
+ orgId: "org-managed",
693
+ principalType: "user",
694
+ });
695
+ expect(mocks.isOrgMember).not.toHaveBeenCalledWith(
696
+ "org-managed",
697
+ "stale@example.test",
698
+ );
699
+ });
700
+
701
+ it("does not borrow a user principal when managed channel installation lookup is unavailable", async () => {
702
+ vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
703
+ mocks.getActiveIntegrationInstallationByKey.mockRejectedValueOnce(
704
+ new Error("installation store unavailable"),
705
+ );
706
+
707
+ await expect(
708
+ resolveDispatchExecutionContext(
709
+ slackIncoming({
710
+ senderId: "U-MANAGED-ALICE",
711
+ triggerKind: "mention",
712
+ conversationType: "channel",
713
+ platformContext: {
714
+ teamId: "T-MANAGED",
715
+ channelId: "C-MANAGED",
716
+ channelType: "channel",
717
+ },
718
+ }),
719
+ ),
720
+ ).rejects.toThrow(
721
+ "Managed Slack installation identity is temporarily unavailable",
722
+ );
723
+ expect(mocks.resolveLinkedOwner).not.toHaveBeenCalled();
724
+ });
725
+
726
+ it("uses the verified member in the installation org and ignores the deployment default", async () => {
727
+ vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
728
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
729
+ managedSlackInstallation(),
730
+ );
731
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
732
+ "managed-token",
733
+ );
734
+ mocks.isOrgMember.mockResolvedValueOnce(true);
735
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
736
+ new Response(
737
+ JSON.stringify({
738
+ ok: true,
739
+ user: {
740
+ profile: {
741
+ email: "ALICE@EXAMPLE.TEST",
742
+ display_name: "Alice",
743
+ },
744
+ },
745
+ }),
746
+ ),
747
+ );
748
+ const incoming = slackIncoming({
749
+ senderId: "U-MANAGED-ALICE",
750
+ triggerKind: "dm",
751
+ conversationType: "dm",
752
+ platformContext: {
753
+ teamId: "T-MANAGED",
754
+ channelId: "D-MANAGED",
755
+ channelType: "im",
756
+ },
757
+ });
758
+
759
+ await expect(resolveDispatchExecutionContext(incoming)).resolves.toEqual({
760
+ ownerEmail: "alice@example.test",
761
+ orgId: "org-managed",
762
+ principalType: "user",
763
+ installationId: "installation-managed",
764
+ });
765
+ expect(mocks.isOrgMember).toHaveBeenCalledWith(
766
+ "org-managed",
767
+ "alice@example.test",
768
+ );
769
+ expect(incoming.senderVerified).toBe(true);
770
+ expect(incoming.actorTrust).toEqual({
771
+ memberType: "member",
772
+ verified: true,
773
+ });
774
+ });
775
+
776
+ it("fails closed instead of borrowing the deployment owner when Slack hydration fails", async () => {
777
+ vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
778
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
779
+ managedSlackInstallation(),
780
+ );
781
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
782
+ "managed-token",
783
+ );
784
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
785
+ new Response(JSON.stringify({ ok: false })),
786
+ );
787
+ const incoming = slackIncoming({
788
+ senderId: "U-MANAGED-UNKNOWN",
789
+ triggerKind: "dm",
790
+ conversationType: "dm",
791
+ platformContext: {
792
+ teamId: "T-MANAGED",
793
+ channelId: "D-MANAGED",
794
+ channelType: "im",
795
+ },
796
+ });
797
+
798
+ const execution = await resolveDispatchExecutionContext(incoming);
799
+ expect(execution.ownerEmail).toMatch(/@integration\.local$/);
800
+ expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
801
+ expect(execution.orgId).toBeNull();
802
+ await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
803
+ {
804
+ handled: true,
805
+ responseText:
806
+ "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
807
+ },
808
+ );
809
+ });
810
+
811
+ it("requires an explicit link when the verified identity is outside the installation org", async () => {
812
+ vi.stubEnv("APP_URL", "https://dispatch.agent-native.test");
813
+ vi.stubEnv("DISPATCH_DEFAULT_OWNER_EMAIL", "deployment-owner@example.test");
814
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
815
+ managedSlackInstallation(),
816
+ );
817
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
818
+ "managed-token",
819
+ );
820
+ mocks.isOrgMember.mockResolvedValueOnce(false);
821
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
822
+ new Response(
823
+ JSON.stringify({
824
+ ok: true,
825
+ user: { profile: { email: "outside@example.test" } },
826
+ }),
827
+ ),
828
+ );
829
+ const incoming = slackIncoming({
830
+ senderId: "U-MANAGED-OUTSIDE",
831
+ triggerKind: "dm",
832
+ conversationType: "dm",
833
+ platformContext: {
834
+ teamId: "T-MANAGED",
835
+ channelId: "D-MANAGED",
836
+ channelType: "im",
837
+ },
838
+ });
839
+
840
+ const execution = await resolveDispatchExecutionContext(incoming);
841
+ expect(execution.ownerEmail).toMatch(/@integration\.local$/);
842
+ expect(execution.ownerEmail).not.toBe("deployment-owner@example.test");
843
+ await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
844
+ {
845
+ handled: true,
846
+ responseText:
847
+ "Agent Native is ready, but this Slack account is not linked to an Agent Native user yet. Open https://dispatch.agent-native.test/identities, create a Slack link token, then send `/link <token>` in this DM.",
848
+ },
849
+ );
850
+ });
851
+
852
+ it("uses an explicitly linked in-org owner when the verified Slack email is outside the org", async () => {
853
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
854
+ managedSlackInstallation(),
855
+ );
856
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
857
+ "managed-token",
858
+ );
859
+ mocks.resolveLinkedOwner.mockResolvedValueOnce("member@example.test");
860
+ mocks.isOrgMember.mockImplementation(
861
+ async (_orgId, email) => email === "member@example.test",
862
+ );
863
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
864
+ new Response(
865
+ JSON.stringify({
866
+ ok: true,
867
+ user: { profile: { email: "outside@example.test" } },
868
+ }),
869
+ ),
870
+ );
871
+ const incoming = slackIncoming({
872
+ senderId: "U-MANAGED-LINKED",
873
+ triggerKind: "dm",
874
+ conversationType: "dm",
875
+ platformContext: {
876
+ teamId: "T-MANAGED",
877
+ channelId: "D-MANAGED",
878
+ channelType: "im",
879
+ },
880
+ });
881
+
882
+ await expect(
883
+ resolveDispatchExecutionContext(incoming),
884
+ ).resolves.toMatchObject({
885
+ ownerEmail: "member@example.test",
886
+ orgId: "org-managed",
887
+ principalType: "user",
888
+ });
889
+ expect(mocks.isOrgMember).toHaveBeenNthCalledWith(
890
+ 1,
891
+ "org-managed",
892
+ "outside@example.test",
893
+ );
894
+ expect(mocks.isOrgMember).toHaveBeenNthCalledWith(
895
+ 2,
896
+ "org-managed",
897
+ "member@example.test",
898
+ );
899
+ });
900
+
901
+ it("denies a Slack Connect stranger before linked-owner resolution", async () => {
902
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
903
+ managedSlackInstallation(),
904
+ );
905
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValueOnce(
906
+ "managed-token",
907
+ );
908
+ mocks.resolveLinkedOwner.mockResolvedValueOnce("member@example.test");
909
+ mocks.isOrgMember.mockResolvedValueOnce(true);
910
+ vi.mocked(globalThis.fetch).mockResolvedValueOnce(
911
+ new Response(
912
+ JSON.stringify({
913
+ ok: true,
914
+ user: {
915
+ is_stranger: true,
916
+ profile: { email: "stranger@example.test" },
917
+ },
918
+ }),
919
+ ),
920
+ );
921
+ const incoming = slackIncoming({
922
+ senderId: "U-SLACK-CONNECT-STRANGER",
923
+ triggerKind: "dm",
924
+ conversationType: "dm",
925
+ platformContext: {
926
+ teamId: "T-MANAGED",
927
+ channelId: "D-MANAGED",
928
+ channelType: "im",
929
+ },
930
+ });
931
+
932
+ const execution = await resolveDispatchExecutionContext(incoming);
933
+ expect(execution.orgId).toBeNull();
934
+ await expect(beforeDispatchProcess(incoming, noopAdapter)).resolves.toEqual(
935
+ {
936
+ handled: true,
937
+ responseText:
938
+ "This assistant is only available to members of this workspace's organization.",
939
+ },
940
+ );
941
+ expect(mocks.resolveLinkedOwner).not.toHaveBeenCalled();
942
+ expect(mocks.isOrgMember).not.toHaveBeenCalled();
943
+ });
944
+
945
+ it("keeps a managed channel on its service principal even when Alice is verified", async () => {
946
+ mocks.getActiveIntegrationInstallationByKey.mockResolvedValueOnce(
947
+ managedSlackInstallation(),
948
+ );
949
+ mocks.resolveSlackBotTokenForIncoming.mockResolvedValue("managed-token");
950
+ mocks.getIntegrationScope.mockResolvedValueOnce({
951
+ id: "scope-managed-channel",
952
+ serviceOwnerEmail: "scope-managed-channel@integration.local",
953
+ orgId: "org-managed",
954
+ defaultModel: null,
955
+ });
956
+ vi.mocked(globalThis.fetch)
957
+ .mockResolvedValueOnce(
958
+ new Response(
959
+ JSON.stringify({
960
+ ok: true,
961
+ user: {
962
+ profile: {
963
+ email: "alice@example.test",
964
+ display_name: "Alice",
965
+ },
966
+ },
967
+ }),
968
+ ),
969
+ )
970
+ .mockResolvedValueOnce(
971
+ new Response(
972
+ JSON.stringify({ ok: true, channel: { is_ext_shared: false } }),
973
+ ),
974
+ );
975
+
976
+ const execution = await resolveDispatchExecutionContext(
977
+ slackIncoming({
978
+ senderId: "U-MANAGED-ALICE",
979
+ triggerKind: "mention",
980
+ conversationType: "channel",
981
+ platformContext: {
982
+ teamId: "T-MANAGED",
983
+ channelId: "C-MANAGED",
984
+ channelType: "channel",
985
+ },
986
+ }),
987
+ );
988
+
989
+ expect(execution).toEqual({
990
+ ownerEmail: "scope-managed-channel@integration.local",
991
+ orgId: "org-managed",
992
+ principalType: "service",
993
+ installationId: "installation-managed",
994
+ scopeId: "scope-managed-channel",
426
995
  });
996
+ expect(execution.ownerEmail).not.toBe("alice@example.test");
997
+ expect(mocks.isOrgMember).not.toHaveBeenCalled();
427
998
  });
428
999
  });