@@ -438,6 +438,26 @@ describe('workspace-scoped vector retrieval', () => {
438438 vi . useRealTimers ( )
439439 } )
440440
441+ it . each ( [ false , undefined ] ) (
442+ 'retrieves ordinary KB results without global projection readiness (searchIndexOnly=%s)' ,
443+ async ( searchIndexOnly ) => {
444+ queueTableRows ( schemaMock . embedding , [ ...ranked ] . reverse ( ) )
445+ const result = await retrieveKnowledgeSearch ( {
446+ ...params ,
447+ searchIndexOnly,
448+ searchMode : 'vector' ,
449+ query : 'What is the capital of France?' ,
450+ } )
451+ expect ( result . retrieval ) . toEqual ( { status : 'complete' , timedOutLegs : [ ] } )
452+ expect ( result . rows . map ( ( row ) => row . id ) ) . toEqual ( [ 'near' , 'far' ] )
453+ expect ( statements ( ) . filter ( ( query ) => query . sql . includes ( 'AS unfilled' ) ) ) . toHaveLength ( 0 )
454+ expect ( statements ( ) . some ( ( query ) => isPageStatement ( query . sql ) ) ) . toBe ( true )
455+ const walk = statements ( ) . find ( ( query ) => isWalk ( query . sql ) ) !
456+ expect ( JSON . stringify ( walk ) ) . toContain ( 'required_clause' )
457+ expect ( JSON . stringify ( walk ) ) . toContain ( String ( schemaMock . document . acl ) )
458+ }
459+ )
460+
441461 it . each ( [ handleVectorOnlySearch , handleTagAndVectorSearch ] ) (
442462 'does not acquire a connection or start SQL after the KB retrieval deadline' ,
443463 async ( search ) => {
@@ -749,7 +769,7 @@ describe('workspace-scoped vector retrieval', () => {
749769 statements ( )
750770 . filter ( ( query ) => query . sql . includes ( 'statement_timeout' ) )
751771 . map ( ( query ) => query . params [ 0 ] )
752- ) . toEqual ( [ '100' , '100' , ' 40', '20' ] )
772+ ) . toEqual ( [ '100' , '40' , '20' ] )
753773 } )
754774
755775 it ( 'applies the scan settings in the deadline statement rather than one of their own' , async ( ) => {
@@ -805,49 +825,53 @@ describe('workspace-scoped vector retrieval', () => {
805825 )
806826 } )
807827
808- it ( 'reports incomplete retrieval for 18 expired pool waiters without starting their SQL later' , async ( ) => {
809- vi . useFakeTimers ( )
810- const release : Array < ( ) => void > = [ ]
811- const transactions : Array < Promise < unknown > > = [ ]
812- vi . spyOn ( db , 'transaction' ) . mockImplementation ( ( callback ) => {
813- const transaction = new Promise < void > ( ( resolve ) => release . push ( resolve ) ) . then ( ( ) =>
814- callback ( db as never )
828+ it . each ( [ false , true ] ) (
829+ 'expires pool waiters without starting SQL later (searchIndexOnly=%s)' ,
830+ async ( searchIndexOnly ) => {
831+ vi . useFakeTimers ( )
832+ const release : Array < ( ) => void > = [ ]
833+ const transactions : Array < Promise < unknown > > = [ ]
834+ vi . spyOn ( db , 'transaction' ) . mockImplementation ( ( callback ) => {
835+ const transaction = new Promise < void > ( ( resolve ) => release . push ( resolve ) ) . then ( ( ) =>
836+ callback ( db as never )
837+ )
838+ transactions . push ( transaction )
839+ return transaction as ReturnType < typeof db . transaction >
840+ } )
841+ const pending = Promise . all (
842+ Array . from ( { length : 18 } , ( _ , index ) =>
843+ retrieveKnowledgeSearch ( {
844+ ...params ,
845+ knowledgeBaseIds : [ `kb-${ index } ` ] ,
846+ searchIndexOnly,
847+ query : 'fixture policy' ,
848+ searchMode : 'vector' ,
849+ vectorBudgetMs : 50 ,
850+ } )
851+ )
815852 )
816- transactions . push ( transaction )
817- return transaction as ReturnType < typeof db . transaction >
818- } )
819- const pending = Promise . all (
820- Array . from ( { length : 18 } , ( _ , index ) =>
821- retrieveKnowledgeSearch ( {
822- ...params ,
823- knowledgeBaseIds : [ `kb-${ index } ` ] ,
824- query : 'fixture policy' ,
825- searchMode : 'vector' ,
826- vectorBudgetMs : 50 ,
853+ await vi . advanceTimersByTimeAsync ( 60 )
854+ const results = await pending
855+ expect ( results ) . toHaveLength ( 18 )
856+ for ( const result of results ) {
857+ expect ( result ) . toEqual ( {
858+ rows : [ ] ,
859+ retrieval : { status : 'partial' , timedOutLegs : [ 'vector' ] } ,
827860 } )
828- )
829- )
830- await vi . advanceTimersByTimeAsync ( 60 )
831- const results = await pending
832- expect ( results ) . toHaveLength ( 18 )
833- for ( const result of results ) {
834- expect ( result ) . toEqual ( {
835- rows : [ ] ,
836- retrieval : { status : 'partial' , timedOutLegs : [ 'vector' ] } ,
837- } )
838- }
839- for ( const resume of release ) resume ( )
840- const settled = await Promise . allSettled ( transactions )
841- /** 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. */
842- expect ( settled ) . toHaveLength ( 1 )
843- for ( const transaction of settled ) {
844- expect ( transaction . status ) . toBe ( 'rejected' )
845- if ( transaction . status === 'rejected' )
846- expect ( transaction . reason ) . toBeInstanceOf ( SearchDeadlineError )
861+ }
862+ for ( const resume of release ) resume ( )
863+ const settled = await Promise . allSettled ( transactions )
864+ /** Search indexes share the readiness read; ordinary KBs acquire their own candidate reads. */
865+ expect ( settled ) . toHaveLength ( searchIndexOnly ? 1 : 18 )
866+ for ( const transaction of settled ) {
867+ expect ( transaction . status ) . toBe ( 'rejected' )
868+ if ( transaction . status === 'rejected' )
869+ expect ( transaction . reason ) . toBeInstanceOf ( SearchDeadlineError )
870+ }
871+ expect ( dbChainMockFns . select ) . not . toHaveBeenCalled ( )
872+ expect ( dbChainMockFns . execute ) . not . toHaveBeenCalled ( )
847873 }
848- expect ( dbChainMockFns . select ) . not . toHaveBeenCalled ( )
849- expect ( dbChainMockFns . execute ) . not . toHaveBeenCalled ( )
850- } )
874+ )
851875} )
852876
853877describe ( 'workspace search filters before ranking' , ( ) => {
@@ -943,6 +967,7 @@ describe('hydration follows ranked candidates', () => {
943967 }
944968 const params : SearchParams = {
945969 knowledgeBaseIds : [ 'org-index' ] ,
970+ searchIndexOnly : true ,
946971 topK : 1 ,
947972 access : identity ,
948973 accessProvider : provider ,
@@ -1296,6 +1321,7 @@ describe('permitted-document planner', () => {
12961321 }
12971322 const params : SearchParams = {
12981323 knowledgeBaseIds : [ 'org-index' ] ,
1324+ searchIndexOnly : true ,
12991325 topK : 1 ,
13001326 access : reader ,
13011327 accessProvider : provider ,
@@ -2278,6 +2304,7 @@ describe('permitted-document planner', () => {
22782304
22792305 const liveSearch = {
22802306 knowledgeBaseIds : [ 'org-index' ] ,
2307+ searchIndexOnly : true ,
22812308 topK : 1 ,
22822309 searchMode : 'hybrid' as const ,
22832310 query : 'release' ,
@@ -2364,48 +2391,57 @@ describe('permitted-document planner', () => {
23642391 expect ( statements ( ) . some ( ( query ) => isPageStatement ( query . sql ) ) ) . toBe ( false )
23652392 } )
23662393
2367- it ( 'excludes a denied source through its documents while the projection is unfilled' , async ( ) => {
2368- queueTableRows ( schemaMock . knowledgeConnector , [
2369- {
2370- id : 'gated-src' ,
2371- accessMode : 'admin' ,
2372- connectorType : 'confluence' ,
2373- githubRepository : false ,
2374- } ,
2375- ] )
2376- dbChainMockFns . execute . mockImplementation ( async ( query ) => {
2377- const statement = render ( query ) . sql
2378- /** The fill has not reached every row, so a denied source cannot be read off the row. */
2379- if ( statement . includes ( 'AS unfilled' ) ) return [ { unfilled : true } ]
2380- /** The mock renders nested fragments as parameters, so the marker is found in the whole query. */
2381- const rebuilt = JSON . stringify ( query ) . includes ( '/* excluded sources */' )
2382- if ( isPageStatement ( statement ) )
2383- return JSON . stringify ( render ( query ) . params ) . includes ( '"b"' )
2384- ? [ hit ( 'b' , 'other-src' ) ]
2385- : [ hit ( 'a' , 'gated-src' ) ]
2386- if ( isWalk ( statement ) )
2387- return Array . from ( { length : 400 } , ( _ , i ) => ( {
2388- id : i === 0 ? ( rebuilt ? 'b' : 'a' ) : `w-${ i } ` ,
2389- distance : 0.1 ,
2390- } ) )
2391- return [ ]
2392- } )
2393- queueTableRows ( schemaMock . embedding , [ ] )
2394- queueTableRows ( schemaMock . embedding , [ hit ( 'b' , 'other-src' ) ] )
2395- const getForConnectors = vi . fn < KnowledgeAccessProvider [ 'getForConnectors' ] > ( async ( ) => reader )
2396- const result = await retrieveKnowledgeSearch ( {
2397- ...liveSearch ,
2398- searchMode : 'vector' ,
2399- access : reader ,
2400- accessProvider : { ...provider , getForConnectors } ,
2401- } )
2402- expect ( result . rows . map ( ( row ) => row . id ) ) . toEqual ( [ 'b' ] )
2403- const walks = statements ( ) . filter ( ( query ) => isWalk ( query . sql ) )
2404- expect ( walks ) . toHaveLength ( 2 )
2405- expect ( JSON . stringify ( walks [ 0 ] ) ) . not . toContain ( '/* excluded sources */' )
2406- expect ( JSON . stringify ( walks [ 1 ] ) ) . toContain ( 'NOT EXISTS (SELECT 1 FROM' )
2407- expect ( JSON . stringify ( walks [ 1 ] ) ) . toContain ( '/* excluded sources */' )
2408- } )
2394+ it . each ( [ false , true ] ) (
2395+ 'excludes a denied source through its documents (searchIndexOnly=%s)' ,
2396+ async ( searchIndexOnly ) => {
2397+ queueTableRows ( schemaMock . knowledgeConnector , [
2398+ {
2399+ id : 'gated-src' ,
2400+ accessMode : 'admin' ,
2401+ connectorType : 'confluence' ,
2402+ githubRepository : false ,
2403+ } ,
2404+ ] )
2405+ dbChainMockFns . execute . mockImplementation ( async ( query ) => {
2406+ const statement = render ( query ) . sql
2407+ /** The fill has not reached every row, so a denied source cannot be read off the row. */
2408+ if ( statement . includes ( 'AS unfilled' ) ) return [ { unfilled : true } ]
2409+ /** The mock renders nested fragments as parameters, so the marker is found in the whole query. */
2410+ const rebuilt = JSON . stringify ( query ) . includes ( '/* excluded sources */' )
2411+ if ( isPageStatement ( statement ) )
2412+ return JSON . stringify ( render ( query ) . params ) . includes ( '"b"' )
2413+ ? [ hit ( 'b' , 'other-src' ) ]
2414+ : [ hit ( 'a' , 'gated-src' ) ]
2415+ if ( isWalk ( statement ) )
2416+ return Array . from ( { length : 400 } , ( _ , i ) => ( {
2417+ id : i === 0 ? ( rebuilt ? 'b' : 'a' ) : `w-${ i } ` ,
2418+ distance : 0.1 ,
2419+ } ) )
2420+ return [ ]
2421+ } )
2422+ queueTableRows ( schemaMock . embedding , [ ] )
2423+ queueTableRows ( schemaMock . embedding , [ hit ( 'b' , 'other-src' ) ] )
2424+ const getForConnectors = vi . fn < KnowledgeAccessProvider [ 'getForConnectors' ] > (
2425+ async ( ) => reader
2426+ )
2427+ const result = await retrieveKnowledgeSearch ( {
2428+ ...liveSearch ,
2429+ searchIndexOnly,
2430+ searchMode : 'vector' ,
2431+ access : reader ,
2432+ accessProvider : { ...provider , getForConnectors } ,
2433+ } )
2434+ expect ( result . rows . map ( ( row ) => row . id ) ) . toEqual ( [ 'b' ] )
2435+ expect ( statements ( ) . filter ( ( query ) => query . sql . includes ( 'AS unfilled' ) ) ) . toHaveLength (
2436+ searchIndexOnly ? 1 : 0
2437+ )
2438+ const walks = statements ( ) . filter ( ( query ) => isWalk ( query . sql ) )
2439+ expect ( walks ) . toHaveLength ( 2 )
2440+ expect ( JSON . stringify ( walks [ 0 ] ) ) . not . toContain ( '/* excluded sources */' )
2441+ expect ( JSON . stringify ( walks [ 1 ] ) ) . toContain ( 'NOT EXISTS (SELECT 1 FROM' )
2442+ expect ( JSON . stringify ( walks [ 1 ] ) ) . toContain ( '/* excluded sources */' )
2443+ }
2444+ )
24092445
24102446 it ( 'hands back the unread slices of a page a denied source made it rebuild' , async ( ) => {
24112447 queueTableRows ( schemaMock . knowledgeConnector , [
@@ -2488,6 +2524,7 @@ describe('filters on a resolved scope', () => {
24882524 }
24892525 const params : SearchParams = {
24902526 knowledgeBaseIds : [ 'org-index' ] ,
2527+ searchIndexOnly : true ,
24912528 topK : 1 ,
24922529 access : reader ,
24932530 accessProvider : provider ,
0 commit comments