@@ -27,11 +27,12 @@ describe("QdrantVectorStore - Branch Isolation", () => {
2727 // Setup mock Qdrant client
2828 mockQdrantClient = {
2929 getCollections : vi . fn ( ) . mockResolvedValue ( { collections : [ ] } ) ,
30- getCollection : vi . fn ( ) . mockResolvedValue ( null ) ,
30+ getCollection : vi . fn ( ) . mockResolvedValue ( { vectors_count : 1 } ) ,
3131 createCollection : vi . fn ( ) . mockResolvedValue ( true ) ,
3232 deleteCollection : vi . fn ( ) . mockResolvedValue ( true ) ,
3333 upsert : vi . fn ( ) . mockResolvedValue ( { status : "completed" } ) ,
3434 search : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
35+ query : vi . fn ( ) . mockResolvedValue ( { points : [ ] } ) ,
3536 delete : vi . fn ( ) . mockResolvedValue ( { status : "completed" } ) ,
3637 }
3738
@@ -93,6 +94,28 @@ describe("QdrantVectorStore - Branch Isolation", () => {
9394 } )
9495
9596 describe ( "collection naming with branch isolation" , ( ) => {
97+ beforeEach ( ( ) => {
98+ // Clear all mocks before each test in this suite to prevent cache pollution
99+ vi . clearAllMocks ( )
100+ mockedGetCurrentBranch . mockClear ( )
101+
102+ // Ensure vectorStore is undefined to force new instance creation
103+ vectorStore = undefined as any
104+
105+ // Reset the mock Qdrant client to ensure clean state for each test
106+ mockQdrantClient = {
107+ getCollections : vi . fn ( ) . mockResolvedValue ( { collections : [ ] } ) ,
108+ getCollection : vi . fn ( ) . mockResolvedValue ( null ) ,
109+ createCollection : vi . fn ( ) . mockResolvedValue ( true ) ,
110+ deleteCollection : vi . fn ( ) . mockResolvedValue ( true ) ,
111+ upsert : vi . fn ( ) . mockResolvedValue ( { status : "completed" } ) ,
112+ search : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
113+ query : vi . fn ( ) . mockResolvedValue ( { points : [ ] } ) ,
114+ delete : vi . fn ( ) . mockResolvedValue ( { status : "completed" } ) ,
115+ }
116+ vi . mocked ( QdrantClient ) . mockImplementation ( ( ) => mockQdrantClient )
117+ } )
118+
96119 it ( "should create branch-specific collection name when branch is provided" , async ( ) => {
97120 mockedGetCurrentBranch . mockResolvedValue ( "feature-branch" )
98121
@@ -167,6 +190,10 @@ describe("QdrantVectorStore - Branch Isolation", () => {
167190 } )
168191
169192 it ( "should handle detached HEAD (undefined branch)" , async ( ) => {
193+ // Clear any cached branch from previous tests and reset mock
194+ mockedGetCurrentBranch . mockClear ( )
195+ mockedGetCurrentBranch . mockResolvedValue ( undefined as any )
196+
170197 vectorStore = new QdrantVectorStore (
171198 testWorkspacePath ,
172199 testQdrantUrl ,
@@ -248,7 +275,7 @@ describe("QdrantVectorStore - Branch Isolation", () => {
248275 } )
249276
250277 describe ( "getCurrentBranch method" , ( ) => {
251- it ( "should return current branch when branch isolation is enabled" , ( ) => {
278+ it ( "should return current branch when branch isolation is enabled" , async ( ) => {
252279 vectorStore = new QdrantVectorStore (
253280 testWorkspacePath ,
254281 testQdrantUrl ,
@@ -258,6 +285,10 @@ describe("QdrantVectorStore - Branch Isolation", () => {
258285 "main" ,
259286 )
260287
288+ // Need to initialize to set currentBranch
289+ mockQdrantClient . getCollection . mockResolvedValue ( { vectors_count : 0 } )
290+ await vectorStore . initialize ( )
291+
261292 expect ( vectorStore . getCurrentBranch ( ) ) . toBe ( "main" )
262293 } )
263294
@@ -326,4 +357,200 @@ describe("QdrantVectorStore - Branch Isolation", () => {
326357 expect ( mockedGetCurrentBranch ) . toHaveBeenCalledWith ( testWorkspacePath )
327358 } )
328359 } )
360+
361+ describe ( "cross-branch search isolation" , ( ) => {
362+ it ( "should not return results from other branch collections when searching" , async ( ) => {
363+ // Setup: Create vector store on main branch
364+ mockedGetCurrentBranch . mockResolvedValue ( "main" )
365+ vectorStore = new QdrantVectorStore (
366+ testWorkspacePath ,
367+ testQdrantUrl ,
368+ testVectorSize ,
369+ undefined ,
370+ true ,
371+ "main" ,
372+ )
373+
374+ // Mock collection doesn't exist initially, then exists after creation
375+ mockQdrantClient . getCollection . mockResolvedValueOnce ( null )
376+ mockQdrantClient . getCollection . mockResolvedValue ( { vectors_count : 1 } )
377+ await vectorStore . initialize ( )
378+
379+ // Capture the collection name used for main branch
380+ const mainCollectionCall = mockQdrantClient . createCollection . mock . calls [ 0 ]
381+ const mainCollectionName = mainCollectionCall [ 0 ]
382+ expect ( mainCollectionName ) . toMatch ( / ^ w s - [ a - f 0 - 9 ] + - b r - m a i n $ / )
383+
384+ // Index documents on main branch
385+ const mainDocs = [
386+ {
387+ id : "main-doc-1" ,
388+ vector : [ 1 , 0 , 0 ] ,
389+ payload : { path : "main.ts" , content : "main branch code" } ,
390+ } ,
391+ ]
392+ await vectorStore . upsertPoints ( mainDocs )
393+
394+ // Verify upsert was called with main collection
395+ expect ( mockQdrantClient . upsert ) . toHaveBeenCalledWith (
396+ mainCollectionName ,
397+ expect . objectContaining ( {
398+ points : expect . arrayContaining ( [
399+ expect . objectContaining ( {
400+ id : "main-doc-1" ,
401+ payload : expect . objectContaining ( { path : "main.ts" } ) ,
402+ } ) ,
403+ ] ) ,
404+ } ) ,
405+ )
406+
407+ // Switch to feature branch
408+ vi . clearAllMocks ( )
409+ vectorStore . invalidateBranchCache ( )
410+ mockedGetCurrentBranch . mockResolvedValue ( "feature-branch" )
411+
412+ // Re-initialize for feature branch - collection doesn't exist, then exists
413+ mockQdrantClient . getCollection . mockResolvedValueOnce ( null )
414+ mockQdrantClient . getCollection . mockResolvedValue ( { vectors_count : 1 } )
415+ await vectorStore . initialize ( )
416+
417+ // Capture the collection name used for feature branch
418+ const featureCollectionCall = mockQdrantClient . createCollection . mock . calls [ 0 ]
419+ const featureCollectionName = featureCollectionCall [ 0 ]
420+ expect ( featureCollectionName ) . toMatch ( / ^ w s - [ a - f 0 - 9 ] + - b r - f e a t u r e - b r a n c h $ / )
421+
422+ // Verify different collection names
423+ expect ( featureCollectionName ) . not . toBe ( mainCollectionName )
424+
425+ // Index different documents on feature branch
426+ const featureDocs = [
427+ {
428+ id : "feature-doc-1" ,
429+ vector : [ 0 , 1 , 0 ] ,
430+ payload : { path : "feature.ts" , content : "feature branch code" } ,
431+ } ,
432+ ]
433+ await vectorStore . upsertPoints ( featureDocs )
434+
435+ // Verify upsert was called with feature collection
436+ expect ( mockQdrantClient . upsert ) . toHaveBeenCalledWith (
437+ featureCollectionName ,
438+ expect . objectContaining ( {
439+ points : expect . arrayContaining ( [
440+ expect . objectContaining ( {
441+ id : "feature-doc-1" ,
442+ payload : expect . objectContaining ( { path : "feature.ts" } ) ,
443+ } ) ,
444+ ] ) ,
445+ } ) ,
446+ )
447+
448+ // Mock search results - feature branch should only return feature docs
449+ mockQdrantClient . query . mockResolvedValue ( {
450+ points : [
451+ {
452+ id : "feature-doc-1" ,
453+ score : 0.95 ,
454+ payload : {
455+ filePath : "feature.ts" ,
456+ codeChunk : "feature branch code" ,
457+ startLine : 1 ,
458+ endLine : 10 ,
459+ } ,
460+ } ,
461+ ] ,
462+ } )
463+
464+ // Search on feature branch
465+ const searchResults = await vectorStore . search ( [ 0 , 1 , 0 ] )
466+
467+ // Verify search was called with feature collection, not main
468+ expect ( mockQdrantClient . query ) . toHaveBeenCalledWith (
469+ featureCollectionName ,
470+ expect . objectContaining ( {
471+ query : [ 0 , 1 , 0 ] ,
472+ } ) ,
473+ )
474+
475+ // Verify results are from feature branch only
476+ expect ( searchResults ) . toHaveLength ( 1 )
477+ expect ( searchResults [ 0 ] . payload . filePath ) . toBe ( "feature.ts" )
478+ expect ( searchResults [ 0 ] . payload . codeChunk ) . toBe ( "feature branch code" )
479+
480+ // Verify main branch document is NOT in results
481+ expect ( searchResults ) . not . toContainEqual (
482+ expect . objectContaining ( {
483+ payload : expect . objectContaining ( { filePath : "main.ts" } ) ,
484+ } ) ,
485+ )
486+ } )
487+
488+ it ( "should maintain separate indexes when switching back to previous branch" , async ( ) => {
489+ // Start on main branch
490+ mockedGetCurrentBranch . mockResolvedValue ( "main" )
491+ vectorStore = new QdrantVectorStore (
492+ testWorkspacePath ,
493+ testQdrantUrl ,
494+ testVectorSize ,
495+ undefined ,
496+ true ,
497+ "main" ,
498+ )
499+
500+ // Collection doesn't exist initially, then exists after creation
501+ mockQdrantClient . getCollection . mockResolvedValueOnce ( null )
502+ mockQdrantClient . getCollection . mockResolvedValue ( { vectors_count : 1 } )
503+ await vectorStore . initialize ( )
504+ const mainCollectionName = mockQdrantClient . createCollection . mock . calls [ 0 ] [ 0 ]
505+
506+ // Index on main
507+ await vectorStore . upsertPoints ( [ { id : "main-1" , vector : [ 1 , 0 , 0 ] , payload : { path : "main.ts" } } ] )
508+
509+ // Switch to feature branch
510+ vi . clearAllMocks ( )
511+ vectorStore . invalidateBranchCache ( )
512+ mockedGetCurrentBranch . mockResolvedValue ( "feature" )
513+ // Collection doesn't exist initially, then exists after creation
514+ mockQdrantClient . getCollection . mockResolvedValueOnce ( null )
515+ mockQdrantClient . getCollection . mockResolvedValue ( { vectors_count : 1 } )
516+ await vectorStore . initialize ( )
517+ const featureCollectionName = mockQdrantClient . createCollection . mock . calls [ 0 ] [ 0 ]
518+
519+ // Index on feature
520+ await vectorStore . upsertPoints ( [ { id : "feature-1" , vector : [ 0 , 1 , 0 ] , payload : { path : "feature.ts" } } ] )
521+
522+ // Switch back to main
523+ vi . clearAllMocks ( )
524+ vectorStore . invalidateBranchCache ( )
525+ mockedGetCurrentBranch . mockResolvedValue ( "main" )
526+
527+ // Mock that main collection already exists
528+ mockQdrantClient . getCollection . mockResolvedValue ( { vectors_count : 1 } )
529+ await vectorStore . initialize ( )
530+
531+ // Mock search returns main branch docs
532+ mockQdrantClient . query . mockResolvedValue ( {
533+ points : [
534+ {
535+ id : "main-1" ,
536+ score : 0.95 ,
537+ payload : {
538+ filePath : "main.ts" ,
539+ codeChunk : "main branch code" ,
540+ startLine : 1 ,
541+ endLine : 10 ,
542+ } ,
543+ } ,
544+ ] ,
545+ } )
546+
547+ const results = await vectorStore . search ( [ 1 , 0 , 0 ] )
548+
549+ // Should search in main collection
550+ expect ( mockQdrantClient . query ) . toHaveBeenCalledWith ( mainCollectionName , expect . any ( Object ) )
551+
552+ // Should get main branch results
553+ expect ( results [ 0 ] . payload . filePath ) . toBe ( "main.ts" )
554+ } )
555+ } )
329556} )
0 commit comments