@@ -441,6 +441,26 @@ describe('workspace-scoped vector retrieval', () => {
441441 vi . useRealTimers ( )
442442 } )
443443
444+ it . each ( [ false , undefined ] ) (
445+ 'retrieves ordinary KB results without global projection readiness (searchIndexOnly=%s)' ,
446+ async ( searchIndexOnly ) => {
447+ queueTableRows ( schemaMock . embedding , [ ...ranked ] . reverse ( ) )
448+ const result = await retrieveKnowledgeSearch ( {
449+ ...params ,
450+ searchIndexOnly,
451+ searchMode : 'vector' ,
452+ query : 'What is the capital of France?' ,
453+ } )
454+ expect ( result . retrieval ) . toEqual ( { status : 'complete' , timedOutLegs : [ ] } )
455+ expect ( result . rows . map ( ( row ) => row . id ) ) . toEqual ( [ 'near' , 'far' ] )
456+ expect ( statements ( ) . filter ( ( query ) => query . sql . includes ( 'AS unfilled' ) ) ) . toHaveLength ( 0 )
457+ expect ( statements ( ) . some ( ( query ) => isPageStatement ( query . sql ) ) ) . toBe ( true )
458+ const walk = statements ( ) . find ( ( query ) => isWalk ( query . sql ) ) !
459+ expect ( JSON . stringify ( walk ) ) . toContain ( 'required_clause' )
460+ expect ( JSON . stringify ( walk ) ) . toContain ( String ( schemaMock . document . acl ) )
461+ }
462+ )
463+
444464 it . each ( [ handleVectorOnlySearch , handleTagAndVectorSearch ] ) (
445465 'does not acquire a connection or start SQL after the KB retrieval deadline' ,
446466 async ( search ) => {
@@ -752,7 +772,7 @@ describe('workspace-scoped vector retrieval', () => {
752772 statements ( )
753773 . filter ( ( query ) => query . sql . includes ( 'statement_timeout' ) )
754774 . map ( ( query ) => query . params [ 0 ] )
755- ) . toEqual ( [ '100' , '100' , ' 40', '20' ] )
775+ ) . toEqual ( [ '100' , '40' , '20' ] )
756776 } )
757777
758778 it ( 'applies the scan settings in the deadline statement rather than one of their own' , async ( ) => {
@@ -808,49 +828,53 @@ describe('workspace-scoped vector retrieval', () => {
808828 )
809829 } )
810830
811- it ( 'reports incomplete retrieval for 18 expired pool waiters without starting their SQL later' , async ( ) => {
812- vi . useFakeTimers ( )
813- const release : Array < ( ) => void > = [ ]
814- const transactions : Array < Promise < unknown > > = [ ]
815- vi . spyOn ( db , 'transaction' ) . mockImplementation ( ( callback ) => {
816- const transaction = new Promise < void > ( ( resolve ) => release . push ( resolve ) ) . then ( ( ) =>
817- callback ( db as never )
831+ it . each ( [ false , true ] ) (
832+ 'expires pool waiters without starting SQL later (searchIndexOnly=%s)' ,
833+ async ( searchIndexOnly ) => {
834+ vi . useFakeTimers ( )
835+ const release : Array < ( ) => void > = [ ]
836+ const transactions : Array < Promise < unknown > > = [ ]
837+ vi . spyOn ( db , 'transaction' ) . mockImplementation ( ( callback ) => {
838+ const transaction = new Promise < void > ( ( resolve ) => release . push ( resolve ) ) . then ( ( ) =>
839+ callback ( db as never )
840+ )
841+ transactions . push ( transaction )
842+ return transaction as ReturnType < typeof db . transaction >
843+ } )
844+ const pending = Promise . all (
845+ Array . from ( { length : 18 } , ( _ , index ) =>
846+ retrieveKnowledgeSearch ( {
847+ ...params ,
848+ knowledgeBaseIds : [ `kb-${ index } ` ] ,
849+ searchIndexOnly,
850+ query : 'fixture policy' ,
851+ searchMode : 'vector' ,
852+ vectorBudgetMs : 50 ,
853+ } )
854+ )
818855 )
819- transactions . push ( transaction )
820- return transaction as ReturnType < typeof db . transaction >
821- } )
822- const pending = Promise . all (
823- Array . from ( { length : 18 } , ( _ , index ) =>
824- retrieveKnowledgeSearch ( {
825- ...params ,
826- knowledgeBaseIds : [ `kb-${ index } ` ] ,
827- query : 'fixture policy' ,
828- searchMode : 'vector' ,
829- vectorBudgetMs : 50 ,
856+ await vi . advanceTimersByTimeAsync ( 60 )
857+ const results = await pending
858+ expect ( results ) . toHaveLength ( 18 )
859+ for ( const result of results ) {
860+ expect ( result ) . toEqual ( {
861+ rows : [ ] ,
862+ retrieval : { status : 'partial' , timedOutLegs : [ 'vector' ] } ,
830863 } )
831- )
832- )
833- await vi . advanceTimersByTimeAsync ( 60 )
834- const results = await pending
835- expect ( results ) . toHaveLength ( 18 )
836- for ( const result of results ) {
837- expect ( result ) . toEqual ( {
838- rows : [ ] ,
839- retrieval : { status : 'partial' , timedOutLegs : [ 'vector' ] } ,
840- } )
841- }
842- for ( const resume of release ) resume ( )
843- const settled = await Promise . allSettled ( transactions )
844- /** The searches that miss the projection-fill memo together share one read; each search's own read is refused at its deadline before it starts. */
845- expect ( settled ) . toHaveLength ( 1 )
846- for ( const transaction of settled ) {
847- expect ( transaction . status ) . toBe ( 'rejected' )
848- if ( transaction . status === 'rejected' )
849- expect ( transaction . reason ) . toBeInstanceOf ( SearchDeadlineError )
864+ }
865+ for ( const resume of release ) resume ( )
866+ const settled = await Promise . allSettled ( transactions )
867+ /** Search indexes share the readiness read; ordinary KBs acquire their own candidate reads. */
868+ expect ( settled ) . toHaveLength ( searchIndexOnly ? 1 : 18 )
869+ for ( const transaction of settled ) {
870+ expect ( transaction . status ) . toBe ( 'rejected' )
871+ if ( transaction . status === 'rejected' )
872+ expect ( transaction . reason ) . toBeInstanceOf ( SearchDeadlineError )
873+ }
874+ expect ( dbChainMockFns . select ) . not . toHaveBeenCalled ( )
875+ expect ( dbChainMockFns . execute ) . not . toHaveBeenCalled ( )
850876 }
851- expect ( dbChainMockFns . select ) . not . toHaveBeenCalled ( )
852- expect ( dbChainMockFns . execute ) . not . toHaveBeenCalled ( )
853- } )
877+ )
854878} )
855879
856880describe ( 'workspace search filters before ranking' , ( ) => {
@@ -946,6 +970,7 @@ describe('hydration follows ranked candidates', () => {
946970 }
947971 const params : SearchParams = {
948972 knowledgeBaseIds : [ 'org-index' ] ,
973+ searchIndexOnly : true ,
949974 topK : 1 ,
950975 access : identity ,
951976 accessProvider : provider ,
@@ -1299,6 +1324,7 @@ describe('permitted-document planner', () => {
12991324 }
13001325 const params : SearchParams = {
13011326 knowledgeBaseIds : [ 'org-index' ] ,
1327+ searchIndexOnly : true ,
13021328 topK : 1 ,
13031329 access : reader ,
13041330 accessProvider : provider ,
@@ -2281,6 +2307,7 @@ describe('permitted-document planner', () => {
22812307
22822308 const liveSearch = {
22832309 knowledgeBaseIds : [ 'org-index' ] ,
2310+ searchIndexOnly : true ,
22842311 topK : 1 ,
22852312 searchMode : 'hybrid' as const ,
22862313 query : 'release' ,
@@ -2367,48 +2394,57 @@ describe('permitted-document planner', () => {
23672394 expect ( statements ( ) . some ( ( query ) => isPageStatement ( query . sql ) ) ) . toBe ( false )
23682395 } )
23692396
2370- it ( 'excludes a denied source through its documents while the projection is unfilled' , async ( ) => {
2371- queueTableRows ( schemaMock . knowledgeConnector , [
2372- {
2373- id : 'gated-src' ,
2374- accessMode : 'admin' ,
2375- connectorType : 'confluence' ,
2376- githubRepository : false ,
2377- } ,
2378- ] )
2379- dbChainMockFns . execute . mockImplementation ( async ( query ) => {
2380- const statement = render ( query ) . sql
2381- /** The fill has not reached every row, so a denied source cannot be read off the row. */
2382- if ( statement . includes ( 'AS unfilled' ) ) return [ { unfilled : true } ]
2383- /** The mock renders nested fragments as parameters, so the marker is found in the whole query. */
2384- const rebuilt = JSON . stringify ( query ) . includes ( '/* excluded sources */' )
2385- if ( isPageStatement ( statement ) )
2386- return JSON . stringify ( render ( query ) . params ) . includes ( '"b"' )
2387- ? [ hit ( 'b' , 'other-src' ) ]
2388- : [ hit ( 'a' , 'gated-src' ) ]
2389- if ( isWalk ( statement ) )
2390- return Array . from ( { length : 400 } , ( _ , i ) => ( {
2391- id : i === 0 ? ( rebuilt ? 'b' : 'a' ) : `w-${ i } ` ,
2392- distance : 0.1 ,
2393- } ) )
2394- return [ ]
2395- } )
2396- queueTableRows ( schemaMock . embedding , [ ] )
2397- queueTableRows ( schemaMock . embedding , [ hit ( 'b' , 'other-src' ) ] )
2398- const getForConnectors = vi . fn < KnowledgeAccessProvider [ 'getForConnectors' ] > ( async ( ) => reader )
2399- const result = await retrieveKnowledgeSearch ( {
2400- ...liveSearch ,
2401- searchMode : 'vector' ,
2402- access : reader ,
2403- accessProvider : { ...provider , getForConnectors } ,
2404- } )
2405- expect ( result . rows . map ( ( row ) => row . id ) ) . toEqual ( [ 'b' ] )
2406- const walks = statements ( ) . filter ( ( query ) => isWalk ( query . sql ) )
2407- expect ( walks ) . toHaveLength ( 2 )
2408- expect ( JSON . stringify ( walks [ 0 ] ) ) . not . toContain ( '/* excluded sources */' )
2409- expect ( JSON . stringify ( walks [ 1 ] ) ) . toContain ( 'NOT EXISTS (SELECT 1 FROM' )
2410- expect ( JSON . stringify ( walks [ 1 ] ) ) . toContain ( '/* excluded sources */' )
2411- } )
2397+ it . each ( [ false , true ] ) (
2398+ 'excludes a denied source through its documents (searchIndexOnly=%s)' ,
2399+ async ( searchIndexOnly ) => {
2400+ queueTableRows ( schemaMock . knowledgeConnector , [
2401+ {
2402+ id : 'gated-src' ,
2403+ accessMode : 'admin' ,
2404+ connectorType : 'confluence' ,
2405+ githubRepository : false ,
2406+ } ,
2407+ ] )
2408+ dbChainMockFns . execute . mockImplementation ( async ( query ) => {
2409+ const statement = render ( query ) . sql
2410+ /** The fill has not reached every row, so a denied source cannot be read off the row. */
2411+ if ( statement . includes ( 'AS unfilled' ) ) return [ { unfilled : true } ]
2412+ /** The mock renders nested fragments as parameters, so the marker is found in the whole query. */
2413+ const rebuilt = JSON . stringify ( query ) . includes ( '/* excluded sources */' )
2414+ if ( isPageStatement ( statement ) )
2415+ return JSON . stringify ( render ( query ) . params ) . includes ( '"b"' )
2416+ ? [ hit ( 'b' , 'other-src' ) ]
2417+ : [ hit ( 'a' , 'gated-src' ) ]
2418+ if ( isWalk ( statement ) )
2419+ return Array . from ( { length : 400 } , ( _ , i ) => ( {
2420+ id : i === 0 ? ( rebuilt ? 'b' : 'a' ) : `w-${ i } ` ,
2421+ distance : 0.1 ,
2422+ } ) )
2423+ return [ ]
2424+ } )
2425+ queueTableRows ( schemaMock . embedding , [ ] )
2426+ queueTableRows ( schemaMock . embedding , [ hit ( 'b' , 'other-src' ) ] )
2427+ const getForConnectors = vi . fn < KnowledgeAccessProvider [ 'getForConnectors' ] > (
2428+ async ( ) => reader
2429+ )
2430+ const result = await retrieveKnowledgeSearch ( {
2431+ ...liveSearch ,
2432+ searchIndexOnly,
2433+ searchMode : 'vector' ,
2434+ access : reader ,
2435+ accessProvider : { ...provider , getForConnectors } ,
2436+ } )
2437+ expect ( result . rows . map ( ( row ) => row . id ) ) . toEqual ( [ 'b' ] )
2438+ expect ( statements ( ) . filter ( ( query ) => query . sql . includes ( 'AS unfilled' ) ) ) . toHaveLength (
2439+ searchIndexOnly ? 1 : 0
2440+ )
2441+ const walks = statements ( ) . filter ( ( query ) => isWalk ( query . sql ) )
2442+ expect ( walks ) . toHaveLength ( 2 )
2443+ expect ( JSON . stringify ( walks [ 0 ] ) ) . not . toContain ( '/* excluded sources */' )
2444+ expect ( JSON . stringify ( walks [ 1 ] ) ) . toContain ( 'NOT EXISTS (SELECT 1 FROM' )
2445+ expect ( JSON . stringify ( walks [ 1 ] ) ) . toContain ( '/* excluded sources */' )
2446+ }
2447+ )
24122448
24132449 it ( 'hands back the unread slices of a page a denied source made it rebuild' , async ( ) => {
24142450 queueTableRows ( schemaMock . knowledgeConnector , [
@@ -2491,6 +2527,7 @@ describe('filters on a resolved scope', () => {
24912527 }
24922528 const params : SearchParams = {
24932529 knowledgeBaseIds : [ 'org-index' ] ,
2530+ searchIndexOnly : true ,
24942531 topK : 1 ,
24952532 access : reader ,
24962533 accessProvider : provider ,
0 commit comments