@@ -68,7 +68,9 @@ import {
6868import { confluencePageAcl } from '@/lib/knowledge/access/confluence-permissions'
6969import { listKnowledgeChunks } from '@/lib/knowledge/application/chunks'
7070import { readKnowledgeDocument } from '@/lib/knowledge/application/documents'
71+ import { listKnowledgeBaseCatalog } from '@/lib/knowledge/application/knowledge-bases'
7172import { readIndexedKnowledgeDocument } from '@/lib/knowledge/application/read-indexed-document'
73+ import { prepareSearchSource } from '@/lib/knowledge/application/sim-search'
7274import { searchScopedKnowledge } from '@/lib/knowledge/application/workspace-search'
7375import { createContentSyncLease } from '@/lib/knowledge/connectors/sync-lock'
7476import { addDocument , persistDocumentAcls } from '@/lib/knowledge/connectors/sync-persistence'
@@ -91,8 +93,9 @@ describe('organization Search MCP with real ingestion and current access', () =>
9193 groupIds,
9294 } = ids
9395 const otherOrganizationId = generateId ( )
94- const otherKnowledgeBaseId = generateId ( )
96+ const workspaceKnowledgeBaseId = generateId ( )
9597 const outsiderId = generateId ( )
98+ const otherAdminId = generateId ( )
9699 const bobMembershipId = generateId ( )
97100 const tokens = {
98101 alice : generateId ( ) ,
@@ -106,10 +109,16 @@ describe('organization Search MCP with real ingestion and current access', () =>
106109 const clients : Client [ ] = [ ]
107110 const alicePrincipal : Principal = { kind : 'session' , userId : aliceId , sessionId : generateId ( ) }
108111 const bobPrincipal : Principal = { kind : 'session' , userId : bobId , sessionId : generateId ( ) }
112+ const otherAdminPrincipal : Principal = {
113+ kind : 'session' ,
114+ userId : otherAdminId ,
115+ sessionId : generateId ( ) ,
116+ }
109117 const bobSourceMembership = {
110118 groupId : groupIds [ 2 ] ,
111119 subjectToken : `u:${ bobId } @fixture.test` ,
112120 }
121+ let otherKnowledgeBaseId : string
113122 let documentId : string
114123 let alice : Client
115124 let bob : Client
@@ -233,14 +242,16 @@ describe('organization Search MCP with real ingestion and current access', () =>
233242 } )
234243 fixtures . storageRoot = mkdtempSync ( path . join ( tmpdir ( ) , 'sim-organization-mcp-integration-' ) )
235244 await seedKnowledgeAclFixture ( ids )
236- await db . insert ( user ) . values ( {
237- id : outsiderId ,
238- name : 'Other organization fixture' ,
239- email : `${ outsiderId } @fixture.test` ,
240- emailVerified : true ,
241- createdAt : new Date ( ) ,
242- updatedAt : new Date ( ) ,
243- } )
245+ await db . insert ( user ) . values (
246+ [ outsiderId , otherAdminId ] . map ( ( id ) => ( {
247+ id,
248+ name : 'Other organization fixture' ,
249+ email : `${ id } @fixture.test` ,
250+ emailVerified : true ,
251+ createdAt : new Date ( ) ,
252+ updatedAt : new Date ( ) ,
253+ } ) )
254+ )
244255 await db . insert ( organization ) . values ( {
245256 id : otherOrganizationId ,
246257 name : 'Other organization MCP fixture' ,
@@ -251,6 +262,12 @@ describe('organization Search MCP with real ingestion and current access', () =>
251262 { id : generateId ( ) , userId : aliceId , organizationId, role : 'owner' } ,
252263 { id : bobMembershipId , userId : bobId , organizationId, role : 'member' } ,
253264 { id : generateId ( ) , userId : outsiderId , organizationId : otherOrganizationId , role : 'owner' } ,
265+ {
266+ id : generateId ( ) ,
267+ userId : otherAdminId ,
268+ organizationId : otherOrganizationId ,
269+ role : 'admin' ,
270+ } ,
254271 ] )
255272 /** Reuse source identities, but establish exclusive organization ownership before ingestion. */
256273 await db
@@ -267,12 +284,16 @@ describe('organization Search MCP with real ingestion and current access', () =>
267284 . update ( knowledgeExternalGroup )
268285 . set ( { workspaceId : null , organizationId } )
269286 . where ( inArray ( knowledgeExternalGroup . id , groupIds ) )
287+ const prepared = await prepareSearchSource . execute ( {
288+ principal : otherAdminPrincipal ,
289+ input : { organizationId : otherOrganizationId , connectorType : 'gitlab' } ,
290+ } )
291+ otherKnowledgeBaseId = prepared . knowledgeBaseId
270292 await db . insert ( knowledgeBase ) . values ( {
271- id : otherKnowledgeBaseId ,
272- userId : outsiderId ,
273- organizationId : otherOrganizationId ,
274- isSearchIndex : true ,
275- name : 'Other org index' ,
293+ id : workspaceKnowledgeBaseId ,
294+ userId : bobId ,
295+ workspaceId,
296+ name : 'Workspace documents' ,
276297 } )
277298 await db . insert ( organizationSearchIntegration ) . values ( {
278299 organizationId,
@@ -387,12 +408,56 @@ describe('organization Search MCP with real ingestion and current access', () =>
387408 . delete ( organization )
388409 . where ( inArray ( organization . id , [ organizationId , otherOrganizationId ] ) )
389410 await db . delete ( workspace ) . where ( eq ( workspace . id , workspaceId ) )
390- await db . delete ( user ) . where ( inArray ( user . id , [ aliceId , bobId , outsiderId ] ) )
411+ await db . delete ( user ) . where ( inArray ( user . id , [ aliceId , bobId , outsiderId , otherAdminId ] ) )
391412 if ( fixtures . storageRoot ) await rm ( fixtures . storageRoot , { recursive : true , force : true } )
392413 await db . $client . end ( )
393414 vi . unstubAllGlobals ( )
394415 } )
395416
417+ it ( 'creates an organization-only index, keeps it out of the workspace catalog, and separates actor from payer' , async ( ) => {
418+ const input = { organizationId : otherOrganizationId , connectorType : 'gitlab' }
419+ const results = await Promise . all ( [
420+ prepareSearchSource . execute ( { principal : otherAdminPrincipal , input } ) ,
421+ prepareSearchSource . execute ( { principal : otherAdminPrincipal , input } ) ,
422+ ] )
423+ expect ( results . map ( ( result ) => result . knowledgeBaseId ) ) . toEqual ( [
424+ otherKnowledgeBaseId ,
425+ otherKnowledgeBaseId ,
426+ ] )
427+ const indexes = await db
428+ . select ( )
429+ . from ( knowledgeBase )
430+ . where ( eq ( knowledgeBase . organizationId , otherOrganizationId ) )
431+ expect ( indexes ) . toEqual ( [
432+ expect . objectContaining ( {
433+ id : otherKnowledgeBaseId ,
434+ workspaceId : null ,
435+ organizationId : otherOrganizationId ,
436+ isSearchIndex : true ,
437+ userId : otherAdminId ,
438+ } ) ,
439+ ] )
440+ const catalog = await listKnowledgeBaseCatalog . execute ( {
441+ principal : alicePrincipal ,
442+ input : { workspaceId } ,
443+ } )
444+ expect ( catalog . knowledgeBases . map ( ( { knowledgeBase } ) => knowledgeBase . id ) ) . toEqual ( [
445+ workspaceKnowledgeBaseId ,
446+ ] )
447+ await expect (
448+ resolveOrganizationBillingAttribution ( {
449+ actorUserId : otherAdminId ,
450+ organizationId : otherOrganizationId ,
451+ } )
452+ ) . resolves . toMatchObject ( {
453+ actorUserId : otherAdminId ,
454+ workspaceId : null ,
455+ organizationId : otherOrganizationId ,
456+ billedAccountUserId : outsiderId ,
457+ billingEntity : { type : 'organization' , id : otherOrganizationId } ,
458+ } )
459+ } )
460+
396461 it ( 'finds the canonical org-owned index and applies each current member’s source ACL to all tools' , async ( ) => {
397462 const [ owner ] = await db
398463 . select ( {
@@ -674,7 +739,7 @@ describe('organization Search MCP with real ingestion and current access', () =>
674739 it ( 'returns an actionable empty index without creating one or accepting an alternate knowledge base' , async ( ) => {
675740 await db
676741 . update ( knowledgeBase )
677- . set ( { isSearchIndex : false } )
742+ . set ( { deletedAt : new Date ( ) } )
678743 . where ( eq ( knowledgeBase . id , knowledgeBaseId ) )
679744 try {
680745 const empty = await value ( alice , 'search' , { query : 'Orion' } )
@@ -688,7 +753,7 @@ describe('organization Search MCP with real ingestion and current access', () =>
688753 } finally {
689754 await db
690755 . update ( knowledgeBase )
691- . set ( { isSearchIndex : true } )
756+ . set ( { deletedAt : null } )
692757 . where ( eq ( knowledgeBase . id , knowledgeBaseId ) )
693758 }
694759 } )
0 commit comments