@@ -11,6 +11,7 @@ import {
1111 setMemoryCustomStatus ,
1212 setMemorySearchImpl ,
1313 setMemorySearchManagerImpl ,
14+ setMemoryStatusOverrides ,
1415} from "./memory-tool-manager.test-mocks.js" ;
1516import { createMemorySearchTool , testing as memoryToolsTesting } from "./tools.js" ;
1617import { MemoryGetSchema , MemorySearchSchema } from "./tools.shared.js" ;
@@ -58,6 +59,67 @@ describe("memory tool schemas", () => {
5859 } ) ;
5960} ) ;
6061
62+ describe ( "memory_search zero-hit sync decisions" , ( ) => {
63+ it ( "only forces sync for unavailable or unready index status" , ( ) => {
64+ const decide = memoryToolsTesting . shouldForceSyncAfterZeroResults ;
65+ const healthy = {
66+ backend : "qmd" ,
67+ dirty : false ,
68+ files : 27 ,
69+ chunks : 472 ,
70+ vector : { available : true } ,
71+ fts : { available : true } ,
72+ custom : { indexIdentity : { status : "valid" } } ,
73+ } ;
74+
75+ expect ( decide ( healthy ) ) . toEqual ( {
76+ shouldSync : false ,
77+ reason : "healthy_zero_hit" ,
78+ } ) ;
79+ expect ( decide ( { ...healthy , dirty : true } ) ) . toEqual ( {
80+ shouldSync : false ,
81+ reason : "healthy_zero_hit" ,
82+ } ) ;
83+ expect ( decide ( { ...healthy , backend : "builtin" , dirty : true } ) ) . toEqual ( {
84+ shouldSync : true ,
85+ reason : "dirty_index" ,
86+ } ) ;
87+ expect ( decide ( { ...healthy , backend : undefined , dirty : true } ) ) . toEqual ( {
88+ shouldSync : true ,
89+ reason : "dirty_index" ,
90+ } ) ;
91+ expect ( decide ( { ...healthy , vector : { enabled : false , available : false } } ) ) . toEqual ( {
92+ shouldSync : false ,
93+ reason : "healthy_zero_hit" ,
94+ } ) ;
95+ expect ( decide ( { ...healthy , fts : { enabled : false , available : false } } ) ) . toEqual ( {
96+ shouldSync : false ,
97+ reason : "healthy_zero_hit" ,
98+ } ) ;
99+ expect ( decide ( { ...healthy , custom : { indexIdentity : { status : "missing" } } } ) ) . toEqual ( {
100+ shouldSync : true ,
101+ reason : "index_identity_invalid" ,
102+ } ) ;
103+ expect ( decide ( { ...healthy , chunks : 0 } ) ) . toEqual ( {
104+ shouldSync : true ,
105+ reason : "no_indexed_chunks" ,
106+ } ) ;
107+ expect ( decide ( { ...healthy , files : 0 } ) ) . toEqual ( {
108+ shouldSync : true ,
109+ reason : "no_indexed_files" ,
110+ } ) ;
111+ expect ( decide ( { ...healthy , vector : { available : false } } ) ) . toEqual ( {
112+ shouldSync : true ,
113+ reason : "vector_unavailable" ,
114+ } ) ;
115+ expect ( decide ( { ...healthy , fts : { available : false } } ) ) . toEqual ( {
116+ shouldSync : true ,
117+ reason : "fts_unavailable" ,
118+ } ) ;
119+ expect ( decide ( null ) ) . toEqual ( { shouldSync : true , reason : "status_unavailable" } ) ;
120+ } ) ;
121+ } ) ;
122+
61123describe ( "memory_search unavailable payloads" , ( ) => {
62124 beforeEach ( ( ) => {
63125 resetMemoryToolMockState ( { searchImpl : async ( ) => [ ] } ) ;
@@ -313,8 +375,42 @@ describe("memory_search unavailable payloads", () => {
313375 expect ( getMemoryCloseMockCalls ( ) ) . toBe ( 1 ) ;
314376 } ) ;
315377
316- it ( "forces a sync and retries once when the first search has zero hits" , async ( ) => {
378+ it ( "returns a healthy zero-hit without forcing sync when the index is ready" , async ( ) => {
379+ let searchCalls = 0 ;
380+ setMemorySearchImpl ( async ( ) => {
381+ searchCalls += 1 ;
382+ if ( searchCalls === 1 ) {
383+ return [ ] ;
384+ }
385+ return [
386+ {
387+ path : "MEMORY.md" ,
388+ startLine : 1 ,
389+ endLine : 1 ,
390+ score : 0.9 ,
391+ snippet : "Thread-hidden codename: ORBIT-22." ,
392+ source : "memory" as const ,
393+ } ,
394+ ] ;
395+ } ) ;
396+
397+ const tool = createMemorySearchToolOrThrow ( {
398+ config : {
399+ agents : { list : [ { id : "main" , default : true } ] } ,
400+ memory : { citations : "off" } ,
401+ } ,
402+ } ) ;
403+ const result = await tool . execute ( "zero-hit-retry" , { query : "hidden thread codename" } ) ;
404+ const details = result . details as { results ?: Array < { path : string } > } ;
405+
406+ expect ( details . results ) . toEqual ( [ ] ) ;
407+ expect ( searchCalls ) . toBe ( 1 ) ;
408+ expect ( getMemorySyncMockCalls ( ) ) . toBe ( 0 ) ;
409+ } ) ;
410+
411+ it ( "preserves the dirty zero-hit retry for non-qmd backends" , async ( ) => {
317412 let searchCalls = 0 ;
413+ setMemoryStatusOverrides ( { dirty : true } ) ;
318414 setMemorySearchImpl ( async ( ) => {
319415 searchCalls += 1 ;
320416 if ( searchCalls === 1 ) {
@@ -339,11 +435,11 @@ describe("memory_search unavailable payloads", () => {
339435 } ,
340436 } ) ;
341437 const result = await tool . execute ( "zero-hit-retry" , { query : "hidden thread codename" } ) ;
438+ const details = result . details as { results ?: Array < { path : string } > } ;
342439
343- expect ( ( result . details as { results ?: Array < { path : string } > } ) . results ?. [ 0 ] ?. path ) . toBe (
344- "MEMORY.md" ,
345- ) ;
440+ expect ( details . results ) . toEqual ( [ expect . objectContaining ( { path : "MEMORY.md" } ) ] ) ;
346441 expect ( searchCalls ) . toBe ( 2 ) ;
442+ expect ( getMemorySyncMockCalls ( ) ) . toBe ( 1 ) ;
347443 } ) ;
348444
349445 it ( "returns unavailable metadata when the index identity is paused" , async ( ) => {
0 commit comments