@@ -212,6 +212,10 @@ describe('Knowledge Search Utils', () => {
212212 describe ( 'handleTagAndVectorSearch' , ( ) => {
213213 it ( 'returns only bounded ranked rows without first materializing every matching tag ID' , async ( ) => {
214214 resetDbChainMock ( )
215+ queueTableRows (
216+ schemaMock . embedding ,
217+ Array . from ( { length : 201 } , ( _ , index ) => ( { id : `candidate-${ index } ` } ) )
218+ )
215219 queueTableRows ( schemaMock . embedding , [ makeResult ( 'second' , 0.2 ) , makeResult ( 'first' , 0.1 ) ] )
216220
217221 const results = await handleTagAndVectorSearch ( {
@@ -226,9 +230,11 @@ describe('Knowledge Search Utils', () => {
226230 } )
227231
228232 expect ( results . map ( ( row ) => row . id ) ) . toEqual ( [ 'first' , 'second' ] )
229- expect ( dbChainMockFns . select ) . toHaveBeenCalledTimes ( 2 )
233+ expect ( dbChainMockFns . select ) . toHaveBeenCalledTimes ( 3 )
230234 expect ( dbChainMockFns . as ) . toHaveBeenCalledWith ( 'ranked_embeddings' )
231- expect ( dbChainMockFns . select . mock . calls [ 0 ] [ 0 ] ) . toHaveProperty ( 'distance' )
235+ expect ( Object . keys ( dbChainMockFns . select . mock . calls [ 0 ] [ 0 ] ) ) . toEqual ( [ 'id' ] )
236+ expect ( dbChainMockFns . limit ) . toHaveBeenNthCalledWith ( 1 , 201 )
237+ expect ( dbChainMockFns . select . mock . calls [ 1 ] [ 0 ] ) . toHaveProperty ( 'distance' )
232238 expect ( dbChainMockFns . limit ) . toHaveBeenCalledWith ( 2 )
233239 } )
234240
@@ -536,6 +542,7 @@ describe('Knowledge Search Utils', () => {
536542 } )
537543
538544 it ( 'runs a single retrieval leg in vector mode' , async ( ) => {
545+ queueTableRows ( schemaMock . embedding , [ { id : 'vector-hit' } ] )
539546 queueTableRows ( schemaMock . embedding , [ makeResult ( 'vector-hit' ) ] )
540547
541548 const results = await executeKnowledgeSearch ( {
@@ -548,20 +555,19 @@ describe('Knowledge Search Utils', () => {
548555 } )
549556
550557 expect ( results . map ( ( r ) => r . id ) ) . toEqual ( [ 'vector-hit' ] )
551- expect ( dbChainMockFns . select ) . toHaveBeenCalledTimes ( 2 )
558+ expect ( dbChainMockFns . select ) . toHaveBeenCalledTimes ( 3 )
552559 expect ( dbChainMockFns . as ) . toHaveBeenCalledWith ( 'ranked_embeddings' )
553560 } )
554561
555562 it ( 'runs both legs and fuses them in hybrid mode' , async ( ) => {
556563 /**
557- * Chains dequeue in creation order. Hybrid legs over-fetch past the
558- * plain scan's candidate pool, so the vector leg opens its transaction
559- * and applies the scan settings before selecting: the keyword ranking
560- * pass is built first, then the vector select, then hydration.
564+ * Chains dequeue in creation order: keyword ranking, the budgeted vector
565+ * probe, keyword hydration, then vector ranking and hydration in one query.
561566 */
562567 queueTableRows ( schemaMock . embedding , [ { id : 'keyword-hit' , keywordRank : 0.9 } ] )
563- queueTableRows ( schemaMock . embedding , [ makeResult ( 'vector-hit' ) ] )
568+ queueTableRows ( schemaMock . embedding , [ { id : 'vector-hit' } ] )
564569 queueTableRows ( schemaMock . embedding , [ makeResult ( 'keyword-hit' ) ] )
570+ queueTableRows ( schemaMock . embedding , [ makeResult ( 'vector-hit' ) ] )
565571
566572 const results = await executeKnowledgeSearch ( {
567573 knowledgeBaseIds : [ 'kb-123' ] ,
@@ -573,39 +579,31 @@ describe('Knowledge Search Utils', () => {
573579 } )
574580
575581 expect ( results . map ( ( r ) => r . id ) . sort ( ) ) . toEqual ( [ 'keyword-hit' , 'vector-hit' ] )
576- expect ( dbChainMockFns . select ) . toHaveBeenCalledTimes ( 4 )
582+ expect ( dbChainMockFns . select ) . toHaveBeenCalledTimes ( 5 )
577583 } )
578584
579- it ( 'falls back to vector results when the keyword leg fails ' , async ( ) => {
585+ it ( 'propagates unexpected keyword errors after the vector leg finishes ' , async ( ) => {
580586 /** The failing ranking chain is still built first and takes the first queued set. */
581587 queueTableRows ( schemaMock . embedding , [ { id : 'never-ranked' , keywordRank : 0 } ] )
588+ queueTableRows ( schemaMock . embedding , [ { id : 'vector-hit' } ] )
582589 queueTableRows ( schemaMock . embedding , [ makeResult ( 'vector-hit' ) ] )
583590
584- /**
585- * Both legs share one `orderBy` spy, so target the keyword leg by its
586- * ranking expression. Calling the untouched spy first captures the
587- * sentinel that tells the mock to build its normal chain, which the
588- * vector leg still needs.
589- */
590- const chainDefault = dbChainMockFns . orderBy ( )
591- dbChainMockFns . orderBy . mockImplementation ( ( fragment : unknown ) => {
592- const text = ( fragment as { strings ?: string [ ] } ) ?. strings ?. join ( '' ) ?? ''
593- if ( text . includes ( 'ts_rank_cd' ) ) {
594- throw new Error ( 'tsquery blew up' )
595- }
596- return chainDefault
597- } )
598-
599- const results = await executeKnowledgeSearch ( {
600- knowledgeBaseIds : [ 'kb-123' ] ,
601- access : WORKSPACE_ACCESS_SCOPE ,
602- topK : 10 ,
603- searchMode : 'hybrid' ,
604- query : 'PROJ-1234' ,
605- queryVector : JSON . stringify ( [ 0.1 , 0.2 , 0.3 ] ) ,
591+ const failure = new Error ( 'tsquery failed' )
592+ dbChainMockFns . orderBy . mockImplementationOnce ( ( ) => {
593+ throw failure
606594 } )
607595
608- expect ( results . map ( ( r ) => r . id ) ) . toEqual ( [ 'vector-hit' ] )
596+ await expect (
597+ executeKnowledgeSearch ( {
598+ knowledgeBaseIds : [ 'kb-123' ] ,
599+ access : WORKSPACE_ACCESS_SCOPE ,
600+ topK : 10 ,
601+ searchMode : 'hybrid' ,
602+ query : 'PROJ-1234' ,
603+ queryVector : JSON . stringify ( [ 0.1 , 0.2 , 0.3 ] ) ,
604+ } )
605+ ) . rejects . toBe ( failure )
606+ expect ( dbChainMockFns . as ) . toHaveBeenCalledWith ( 'ranked_embeddings' )
609607 } )
610608
611609 it ( 'skips both query legs when only tag filters are provided' , async ( ) => {
0 commit comments