@@ -68,6 +68,7 @@ import {
6868import { readSearchSourceOverview } from '@/lib/knowledge/application/search-source-overview'
6969import { readSearchSourceProgress } from '@/lib/knowledge/application/search-source-progress'
7070import { listSearchSources } from '@/lib/knowledge/application/search-sources'
71+ import { slackSearchMemberPrincipal } from '@/lib/knowledge/application/slack-search/member-principal'
7172
7273const principal = { kind : 'session' as const , userId : 'reader' , sessionId : 'session' }
7374const input = { workspaceId : 'workspace' }
@@ -371,6 +372,72 @@ describe('Search source summaries', () => {
371372} )
372373
373374describe ( 'organization Search source summaries' , ( ) => {
375+ function slackPrincipal ( ) {
376+ return slackSearchMemberPrincipal (
377+ { installationId : 'i1' , message : { eventId : 'Ev1' } } ,
378+ 'org-1' ,
379+ 'reader'
380+ )
381+ }
382+ it ( 'uses the Slack member’s ACL and includes their expired connection for Home' , async ( ) => {
383+ const delegated = slackPrincipal ( )
384+ mocks . context . mockResolvedValue ( { organizationId : 'org-1' } )
385+ queueTableRows ( member , [ { role : 'member' } ] )
386+ mocks . memberships . mockResolvedValue ( new Map ( [ [ 'drive' , 'needs_reauth' ] ] ) )
387+ seed ( [ source ( 'drive' , 'google_drive' , 'members' ) ] )
388+ const result = await listSearchSources . execute ( {
389+ principal : delegated ,
390+ input : { organizationId : 'org-1' } ,
391+ } )
392+ expect ( result . sources [ 0 ] ) . toMatchObject ( {
393+ viewerMembership : 'needs_reauth' ,
394+ connectionRequired : true ,
395+ } )
396+ expect ( mocks . access ) . toHaveBeenCalledWith ( delegated , { organizationId : 'org-1' } )
397+ expect ( mocks . memberships ) . toHaveBeenCalledWith (
398+ expect . objectContaining ( { userId : 'reader' , organizationId : 'org-1' } )
399+ )
400+ } )
401+ it ( 'refuses Slack delegation into another organization' , async ( ) => {
402+ mocks . context . mockResolvedValue ( { organizationId : 'org-2' } )
403+ await expect (
404+ listSearchSources . execute ( { principal : slackPrincipal ( ) , input : { organizationId : 'org-2' } } )
405+ ) . rejects . toThrow ( 'delegation' )
406+ expect ( mocks . memberships ) . not . toHaveBeenCalled ( )
407+ } )
408+ it ( 'rechecks Slack membership and delegation expiry before listing sources' , async ( ) => {
409+ mocks . context . mockResolvedValue ( { organizationId : 'org-1' } )
410+ queueTableRows ( member , [ ] )
411+ await expect (
412+ listSearchSources . execute ( { principal : slackPrincipal ( ) , input : { organizationId : 'org-1' } } )
413+ ) . rejects . toThrow ( 'Organization not found' )
414+ await expect (
415+ listSearchSources . execute ( {
416+ principal : { ...slackPrincipal ( ) , expiresAt : new Date ( 0 ) } ,
417+ input : { organizationId : 'org-1' } ,
418+ } )
419+ ) . rejects . toThrow ( 'delegation' )
420+ expect ( mocks . memberships ) . not . toHaveBeenCalled ( )
421+ } )
422+ it ( 'does not make the source list available to Copilot or Slack installation authority' , async ( ) => {
423+ await expect (
424+ listSearchSources . execute ( {
425+ principal : {
426+ kind : 'organization_delegated' ,
427+ serviceId : 'copilot' ,
428+ organizationId : 'org-1' ,
429+ subjectUserId : 'reader' ,
430+ delegationId : 'd1' ,
431+ audience : 'sim:knowledge' ,
432+ issuedAt : new Date ( ) ,
433+ expiresAt : new Date ( Date . now ( ) + 60_000 ) ,
434+ resourceScope : { chatId : 'chat1' } ,
435+ } ,
436+ input : { organizationId : 'org-1' } ,
437+ } )
438+ ) . rejects . toThrow ( 'delegation' )
439+ expect ( mocks . context ) . not . toHaveBeenCalled ( )
440+ } )
374441 it . each ( [ 'member' , 'admin' ] ) (
375442 'returns only the current %s viewer ACL counts without a workspace membership' ,
376443 async ( role ) => {
0 commit comments