@@ -64,6 +64,8 @@ import {
6464import { confluencePageAcl } from '@/lib/knowledge/access/confluence-permissions'
6565import { listKnowledgeChunks } from '@/lib/knowledge/application/chunks'
6666import { readKnowledgeDocument } from '@/lib/knowledge/application/documents'
67+ import { listKnowledgeBaseCatalog } from '@/lib/knowledge/application/knowledge-bases'
68+ import { prepareSearchSource } from '@/lib/knowledge/application/sim-search'
6769import { searchScopedKnowledge } from '@/lib/knowledge/application/workspace-search'
6870import { createContentSyncLease } from '@/lib/knowledge/connectors/sync-lock'
6971import { addDocument , persistDocumentAcls } from '@/lib/knowledge/connectors/sync-persistence'
@@ -84,8 +86,9 @@ describe('organization Search MCP with real ingestion and current access', () =>
8486 groupIds,
8587 } = ids
8688 const otherOrganizationId = generateId ( )
87- const otherKnowledgeBaseId = generateId ( )
89+ const workspaceKnowledgeBaseId = generateId ( )
8890 const outsiderId = generateId ( )
91+ const otherAdminId = generateId ( )
8992 const bobMembershipId = generateId ( )
9093 const tokens = {
9194 alice : generateId ( ) ,
@@ -98,10 +101,16 @@ describe('organization Search MCP with real ingestion and current access', () =>
98101 const clients : Client [ ] = [ ]
99102 const alicePrincipal : Principal = { kind : 'session' , userId : aliceId , sessionId : generateId ( ) }
100103 const bobPrincipal : Principal = { kind : 'session' , userId : bobId , sessionId : generateId ( ) }
104+ const otherAdminPrincipal : Principal = {
105+ kind : 'session' ,
106+ userId : otherAdminId ,
107+ sessionId : generateId ( ) ,
108+ }
101109 const bobSourceMembership = {
102110 groupId : groupIds [ 2 ] ,
103111 subjectToken : `u:${ bobId } @fixture.test` ,
104112 }
113+ let otherKnowledgeBaseId : string
105114 let documentId : string
106115 let alice : Client
107116 let bob : Client
@@ -210,14 +219,16 @@ describe('organization Search MCP with real ingestion and current access', () =>
210219 } )
211220 fixtures . storageRoot = mkdtempSync ( path . join ( tmpdir ( ) , 'sim-organization-mcp-integration-' ) )
212221 await seedKnowledgeAclFixture ( ids )
213- await db . insert ( user ) . values ( {
214- id : outsiderId ,
215- name : 'Other organization fixture' ,
216- email : `${ outsiderId } @fixture.test` ,
217- emailVerified : true ,
218- createdAt : new Date ( ) ,
219- updatedAt : new Date ( ) ,
220- } )
222+ await db . insert ( user ) . values (
223+ [ outsiderId , otherAdminId ] . map ( ( id ) => ( {
224+ id,
225+ name : 'Other organization fixture' ,
226+ email : `${ id } @fixture.test` ,
227+ emailVerified : true ,
228+ createdAt : new Date ( ) ,
229+ updatedAt : new Date ( ) ,
230+ } ) )
231+ )
221232 await db . insert ( organization ) . values ( {
222233 id : otherOrganizationId ,
223234 name : 'Other organization MCP fixture' ,
@@ -228,6 +239,12 @@ describe('organization Search MCP with real ingestion and current access', () =>
228239 { id : generateId ( ) , userId : aliceId , organizationId, role : 'owner' } ,
229240 { id : bobMembershipId , userId : bobId , organizationId, role : 'member' } ,
230241 { id : generateId ( ) , userId : outsiderId , organizationId : otherOrganizationId , role : 'owner' } ,
242+ {
243+ id : generateId ( ) ,
244+ userId : otherAdminId ,
245+ organizationId : otherOrganizationId ,
246+ role : 'admin' ,
247+ } ,
231248 ] )
232249 /** Reuse source identities, but establish exclusive organization ownership before ingestion. */
233250 await db
@@ -244,12 +261,16 @@ describe('organization Search MCP with real ingestion and current access', () =>
244261 . update ( knowledgeExternalGroup )
245262 . set ( { workspaceId : null , organizationId } )
246263 . where ( inArray ( knowledgeExternalGroup . id , groupIds ) )
264+ const prepared = await prepareSearchSource . execute ( {
265+ principal : otherAdminPrincipal ,
266+ input : { organizationId : otherOrganizationId , connectorType : 'gitlab' } ,
267+ } )
268+ otherKnowledgeBaseId = prepared . knowledgeBaseId
247269 await db . insert ( knowledgeBase ) . values ( {
248- id : otherKnowledgeBaseId ,
249- userId : outsiderId ,
250- organizationId : otherOrganizationId ,
251- isSearchIndex : true ,
252- name : 'Other org index' ,
270+ id : workspaceKnowledgeBaseId ,
271+ userId : bobId ,
272+ workspaceId,
273+ name : 'Workspace documents' ,
253274 } )
254275 await db . insert ( organizationSearchIntegration ) . values ( {
255276 organizationId,
@@ -363,12 +384,56 @@ describe('organization Search MCP with real ingestion and current access', () =>
363384 . delete ( organization )
364385 . where ( inArray ( organization . id , [ organizationId , otherOrganizationId ] ) )
365386 await db . delete ( workspace ) . where ( eq ( workspace . id , workspaceId ) )
366- await db . delete ( user ) . where ( inArray ( user . id , [ aliceId , bobId , outsiderId ] ) )
387+ await db . delete ( user ) . where ( inArray ( user . id , [ aliceId , bobId , outsiderId , otherAdminId ] ) )
367388 if ( fixtures . storageRoot ) await rm ( fixtures . storageRoot , { recursive : true , force : true } )
368389 await db . $client . end ( )
369390 vi . unstubAllGlobals ( )
370391 } )
371392
393+ it ( 'creates an organization-only index, keeps it out of the workspace catalog, and separates actor from payer' , async ( ) => {
394+ const input = { organizationId : otherOrganizationId , connectorType : 'gitlab' }
395+ const results = await Promise . all ( [
396+ prepareSearchSource . execute ( { principal : otherAdminPrincipal , input } ) ,
397+ prepareSearchSource . execute ( { principal : otherAdminPrincipal , input } ) ,
398+ ] )
399+ expect ( results . map ( ( result ) => result . knowledgeBaseId ) ) . toEqual ( [
400+ otherKnowledgeBaseId ,
401+ otherKnowledgeBaseId ,
402+ ] )
403+ const indexes = await db
404+ . select ( )
405+ . from ( knowledgeBase )
406+ . where ( eq ( knowledgeBase . organizationId , otherOrganizationId ) )
407+ expect ( indexes ) . toEqual ( [
408+ expect . objectContaining ( {
409+ id : otherKnowledgeBaseId ,
410+ workspaceId : null ,
411+ organizationId : otherOrganizationId ,
412+ isSearchIndex : true ,
413+ userId : otherAdminId ,
414+ } ) ,
415+ ] )
416+ const catalog = await listKnowledgeBaseCatalog . execute ( {
417+ principal : alicePrincipal ,
418+ input : { workspaceId } ,
419+ } )
420+ expect ( catalog . knowledgeBases . map ( ( { knowledgeBase } ) => knowledgeBase . id ) ) . toEqual ( [
421+ workspaceKnowledgeBaseId ,
422+ ] )
423+ await expect (
424+ resolveOrganizationBillingAttribution ( {
425+ actorUserId : otherAdminId ,
426+ organizationId : otherOrganizationId ,
427+ } )
428+ ) . resolves . toMatchObject ( {
429+ actorUserId : otherAdminId ,
430+ workspaceId : null ,
431+ organizationId : otherOrganizationId ,
432+ billedAccountUserId : outsiderId ,
433+ billingEntity : { type : 'organization' , id : otherOrganizationId } ,
434+ } )
435+ } )
436+
372437 it ( 'finds the canonical org-owned index and applies each current member’s source ACL to all tools' , async ( ) => {
373438 const [ owner ] = await db
374439 . select ( {
0 commit comments