@@ -15,6 +15,8 @@ const mocks = vi.hoisted(() => ({
1515 refetchApps : vi . fn ( ) ,
1616 manifest : vi . fn ( ) ,
1717 install : vi . fn ( ) ,
18+ accounts : vi . fn ( ) ,
19+ refetchAccounts : vi . fn ( ) ,
1820} ) )
1921vi . mock ( '@/hooks/queries/credential-groups' , ( ) => ( {
2022 useStartSlackCredentialGroupConfiguration : ( ) => ( {
@@ -34,6 +36,11 @@ vi.mock('@/hooks/queries/slack-search', () => ({
3436 useStartSlackSearchOAuth : ( ) => ( { mutate : mocks . install , isPending : false , reset : vi . fn ( ) } ) ,
3537} ) )
3638
39+ vi . mock ( '@/hooks/queries/organization-accounts' , ( ) => ( {
40+ organizationAccountsKeys : { detail : ( id : string ) => [ 'organization-accounts' , id ] } ,
41+ useOrganizationAccounts : mocks . accounts ,
42+ } ) )
43+
3744import type { WorkspaceCredential } from '@/lib/api/contracts/credentials'
3845import {
3946 SLACK_MANAGED_USER_SCOPES ,
@@ -69,6 +76,25 @@ describe('Slack member access selection', () => {
6976 vi . spyOn ( toast , 'success' ) . mockReturnValue ( 'toast' )
7077 vi . stubGlobal ( 'IS_REACT_ACT_ENVIRONMENT' , true )
7178 mocks . create . mockResolvedValue ( undefined )
79+ mocks . accounts . mockReturnValue ( {
80+ isSuccess : true ,
81+ isPending : false ,
82+ isFetching : false ,
83+ data : {
84+ credentialGroup : {
85+ id : 'group-1' ,
86+ options : [
87+ {
88+ provider : 'slack' ,
89+ status : 'active' ,
90+ configurationStatus : 'ready' ,
91+ } ,
92+ ] ,
93+ } ,
94+ } ,
95+ error : null ,
96+ refetch : mocks . refetchAccounts ,
97+ } )
7298 mocks . apps . mockReturnValue ( {
7399 isSuccess : true ,
74100 isPending : false ,
@@ -456,6 +482,7 @@ describe('Slack member access selection', () => {
456482 {
457483 id : 'installation-1' ,
458484 appId : 'A_APP' ,
485+ appKind : 'custom' ,
459486 teamId : 'T_TEAM' ,
460487 teamName : 'sim' ,
461488 credentialId : bot . id ,
@@ -487,6 +514,137 @@ describe('Slack member access selection', () => {
487514 } )
488515 } )
489516
517+ it . each ( [ false , true ] ) (
518+ 'skips completed shared app setup entirely (refreshed installation: %s)' ,
519+ async ( refresh ) => {
520+ const installation = {
521+ id : 'installation-1' ,
522+ appId : 'A_SHARED' ,
523+ appKind : 'shared' ,
524+ teamId : 'T_TEAM' ,
525+ teamName : 'sim' ,
526+ credentialId : bot . id ,
527+ enabled : true ,
528+ needsValidation : false ,
529+ }
530+ if ( refresh ) {
531+ await render ( undefined , [ ] , 'org-1' )
532+ expect ( document . body . textContent ) . toContain ( 'Install Sim Search first' )
533+ }
534+ mocks . apps . mockReturnValue ( {
535+ isSuccess : true ,
536+ isPending : false ,
537+ data : { installations : [ installation ] , bots : [ bot ] , sharedAppAvailable : true } ,
538+ error : null ,
539+ } )
540+ await render ( undefined , [ ] , 'org-1' )
541+ expect ( document . querySelector ( '[role="dialog"]' ) ) . toBeNull ( )
542+ expect ( mocks . onOpenChange ) . toHaveBeenCalledExactlyOnceWith ( false )
543+ expect ( mocks . start ) . not . toHaveBeenCalled ( )
544+ expect ( mocks . install ) . not . toHaveBeenCalled ( )
545+ expect ( window . open ) . not . toHaveBeenCalled ( )
546+ }
547+ )
548+
549+ it . each ( [
550+ { enabled : false , needsValidation : false , sharedAppAvailable : true } ,
551+ { enabled : true , needsValidation : true , sharedAppAvailable : true } ,
552+ { enabled : true , needsValidation : false , sharedAppAvailable : false } ,
553+ ] ) ( 'keeps incomplete shared app setup actionable: %j' , async ( status ) => {
554+ mocks . apps . mockReturnValue ( {
555+ isSuccess : true ,
556+ isPending : false ,
557+ data : {
558+ installations : [
559+ {
560+ id : 'installation-1' ,
561+ appId : 'A_SHARED' ,
562+ appKind : 'shared' ,
563+ teamId : 'T_TEAM' ,
564+ teamName : 'sim' ,
565+ credentialId : bot . id ,
566+ enabled : status . enabled ,
567+ needsValidation : status . needsValidation ,
568+ } ,
569+ ] ,
570+ bots : [ bot ] ,
571+ sharedAppAvailable : status . sharedAppAvailable ,
572+ } ,
573+ error : null ,
574+ } )
575+ await render ( undefined , [ ] , 'org-1' )
576+ expect ( document . body . textContent ) . toContain ( 'Manage Sim Search app' )
577+ expect ( document . body . textContent ) . not . toContain ( 'Verify and add' )
578+ expect ( mocks . onOpenChange ) . not . toHaveBeenCalled ( )
579+ expect ( mocks . start ) . not . toHaveBeenCalled ( )
580+ } )
581+
582+ it . each ( [ 'removed' , 'needs_update' , 'pending' , 'error' , 'refreshing' ] ) (
583+ 'does not skip shared setup when member configuration is %s' ,
584+ async ( state ) => {
585+ mocks . apps . mockReturnValue ( {
586+ isSuccess : true ,
587+ isPending : false ,
588+ data : {
589+ installations : [
590+ {
591+ id : 'installation-1' ,
592+ appId : 'A_SHARED' ,
593+ appKind : 'shared' ,
594+ teamId : 'T_TEAM' ,
595+ teamName : 'sim' ,
596+ credentialId : bot . id ,
597+ enabled : true ,
598+ needsValidation : false ,
599+ } ,
600+ ] ,
601+ bots : [ bot ] ,
602+ sharedAppAvailable : true ,
603+ } ,
604+ error : null ,
605+ } )
606+ const current = mocks . accounts ( )
607+ mocks . accounts . mockReturnValue ( {
608+ ...current ,
609+ isSuccess : ! [ 'pending' , 'error' ] . includes ( state ) ,
610+ isPending : state === 'pending' ,
611+ isFetching : state === 'refreshing' ,
612+ error : state === 'error' ? new Error ( 'Could not load member setup' ) : null ,
613+ data :
614+ state === 'pending'
615+ ? undefined
616+ : {
617+ credentialGroup : {
618+ id : 'group-1' ,
619+ options :
620+ state === 'removed'
621+ ? [ ]
622+ : [
623+ {
624+ provider : 'slack' ,
625+ status : 'active' ,
626+ configurationStatus :
627+ state === 'needs_update' ? 'needs_update' : 'ready' ,
628+ } ,
629+ ] ,
630+ } ,
631+ } ,
632+ } )
633+ await render ( undefined , [ ] , 'org-1' )
634+ expect ( mocks . onOpenChange ) . not . toHaveBeenCalled ( )
635+ expect ( mocks . start ) . not . toHaveBeenCalled ( )
636+ if ( state === 'error' ) {
637+ expect ( document . body . textContent ) . toContain ( 'Could not load member setup' )
638+ await clickButton ( 'Retry' )
639+ expect ( mocks . refetchAccounts ) . toHaveBeenCalledOnce ( )
640+ } else if ( state === 'pending' || state === 'refreshing' ) {
641+ expect ( document . body . textContent ) . toContain ( 'Checking the installed Slack app' )
642+ } else {
643+ expect ( document . body . textContent ) . toContain ( 'Manage Sim Search app' )
644+ }
645+ }
646+ )
647+
490648 it ( 'only changes existing workflow access after the user selects Search documents' , async ( ) => {
491649 await render ( SLACK_MANAGED_USER_SCOPES )
492650 const access = Array . from ( document . querySelectorAll ( 'button' ) ) . find ( ( node ) =>
0 commit comments