@@ -27,11 +27,10 @@ const { createMemoryUpgrader } = jiti("../src/memory-upgrader.ts");
2727
2828// ============================================================================
2929// Test 1: Verify current behavior - each entry triggers store.update() separately
30- // (which means each entry acquires a lock in the real store.update() implementation)
3130// ============================================================================
3231
3332async function testCurrentBehaviorUpdatesCalledPerEntry ( ) {
34- const updateCalls : Array < { id : string ; patch: any } > = [ ] ;
33+ const updateCalls = [ ] ;
3534
3635 const store = {
3736 async list ( ) {
@@ -45,7 +44,7 @@ async function testCurrentBehaviorUpdatesCalledPerEntry() {
4544 } ,
4645
4746 // Each update call = one lock acquisition in real store.update() implementation
48- async update ( id : string , patch : any ) {
47+ async update ( id , patch ) {
4948 updateCalls . push ( { id, patch } ) ;
5049 return true ;
5150 } ,
@@ -60,7 +59,7 @@ async function testCurrentBehaviorUpdatesCalledPerEntry() {
6059 } ,
6160 } ;
6261
63- const upgrader = createMemoryUpgrader ( store as any , llm , {
62+ const upgrader = createMemoryUpgrader ( store , llm , {
6463 log : ( ) => { } ,
6564 } ) ;
6665
@@ -69,7 +68,6 @@ async function testCurrentBehaviorUpdatesCalledPerEntry() {
6968 console . log ( `[Test 1] update() called ${ updateCalls . length } times for ${ result . upgraded } entries` ) ;
7069
7170 // Current behavior: 5 update calls (one per entry) = 5 lock acquisitions in real store
72- // After fix: Should be 1 lock for the entire batch
7371 assert . equal ( updateCalls . length , 5 , `Expected 5 update calls (current buggy behavior), got ${ updateCalls . length } ` ) ;
7472 assert . equal ( result . upgraded , 5 ) ;
7573
@@ -83,8 +81,8 @@ async function testCurrentBehaviorUpdatesCalledPerEntry() {
8381// ============================================================================
8482
8583async function testLlmCallsAreSequential ( ) {
86- const llmCalls : string [ ] = [ ] ;
87- const updateCalls : string [ ] = [ ] ;
84+ const llmCalls = [ ] ;
85+ const updateCalls = [ ] ;
8886
8987 const store = {
9088 async list ( ) {
@@ -95,7 +93,7 @@ async function testLlmCallsAreSequential() {
9593 ] ;
9694 } ,
9795
98- async update ( id : string , patch : any ) {
96+ async update ( id , patch ) {
9997 updateCalls . push ( id ) ;
10098 return true ;
10199 } ,
@@ -113,38 +111,42 @@ async function testLlmCallsAreSequential() {
113111 } ,
114112 } ;
115113
116- const upgrader = createMemoryUpgrader ( store as any , llm , { log : ( ) => { } } ) ;
114+ const upgrader = createMemoryUpgrader ( store , llm , { log : ( ) => { } } ) ;
117115
118116 await upgrader . upgrade ( { batchSize : 3 , noLlm : false } ) ;
119117
120118 console . log ( `[Test 2] LLM calls: ${ llmCalls . join ( ", " ) } ` ) ;
121119 console . log ( `[Test 2] Update calls: ${ updateCalls . join ( ", " ) } ` ) ;
122120
123121 // Current behavior: LLM called sequentially, interleaved with updates
124- // In real code with lock: LLM happens inside lock, then update happens
125122 assert . equal ( llmCalls . length , 3 , `Expected 3 LLM calls, got ${ llmCalls . length } ` ) ;
126123 assert . equal ( updateCalls . length , 3 , `Expected 3 updates, got ${ updateCalls . length } ` ) ;
127124
128125 console . log ( "[Test 2] CONFIRMED: LLM called per-entry (not batched before lock)" ) ;
129126}
130127
131128// ============================================================================
132- // Test 3: Verify upgradeEntry processes one entry at a time (not batched LLM first )
129+ // Test 3: Verify upgradeEntry processes one entry at a time (extreme test: batch of 10 )
133130// ============================================================================
134131
135- async function testUpgradeProcessesOneByOne ( ) {
136- const executionLog : string [ ] = [ ] ;
132+ async function testUpgradeProcessesOneByOneExtreme ( ) {
133+ const executionLog = [ ] ;
134+ const updateCalls = [ ] ;
135+
136+ // Create 10 entries to test extreme case
137+ const entries = [ ] ;
138+ for ( let i = 1 ; i <= 10 ; i ++ ) {
139+ entries . push ( { id : `e${ i } ` , text : `Entry ${ i } ` , category : "fact" , scope : "test" , importance : 0.8 , timestamp : Date . now ( ) , metadata : "{}" } ) ;
140+ }
137141
138142 const store = {
139143 async list ( ) {
140- return [
141- { id : "e1" , text : "Entry 1" , category : "fact" , scope : "test" , importance : 0.8 , timestamp : Date . now ( ) , metadata : "{}" } ,
142- { id : "e2" , text : "Entry 2" , category : "fact" , scope : "test" , importance : 0.8 , timestamp : Date . now ( ) , metadata : "{}" } ,
143- ] ;
144+ return entries ;
144145 } ,
145146
146- async update ( id : string , patch : any ) {
147+ async update ( id , patch ) {
147148 executionLog . push ( `update:${ id } ` ) ;
149+ updateCalls . push ( id ) ;
148150 return true ;
149151 } ,
150152 } ;
@@ -157,23 +159,89 @@ async function testUpgradeProcessesOneByOne() {
157159 getLastError ( ) { return "" ; } ,
158160 } ;
159161
160- const upgrader = createMemoryUpgrader ( store as any , llm , { log : ( ) => { } } ) ;
162+ const upgrader = createMemoryUpgrader ( store , llm , { log : ( ) => { } } ) ;
161163
162- await upgrader . upgrade ( { batchSize : 2 , noLlm : false } ) ;
164+ // Extreme test: batch of 10 entries
165+ const result = await upgrader . upgrade ( { batchSize : 10 , noLlm : false } ) ;
163166
164- console . log ( `[Test 3] Execution: ${ executionLog . join ( " -> " ) } ` ) ;
167+ console . log ( `[Test 3 EXTREME] Execution (first 10): ${ executionLog . slice ( 0 , 20 ) . join ( " -> " ) } ` ) ;
168+ console . log ( `[Test 3 EXTREME] Total: ${ updateCalls . length } updates, ${ executionLog . filter ( x => x === "llm" ) . length } LLM calls` ) ;
165169
166- // Current: llm -> update -> llm -> update (one at a time, inside lock)
167- // After fix: llm -> llm -> lock -> update -> update (batch LLM first, then batch update)
170+ // Verify: 10 update calls = 10 lock acquisitions in real store
171+ assert . equal ( updateCalls . length , 10 , `Expected 10 updates, got ${ updateCalls . length } ` ) ;
172+ assert . equal ( result . upgraded , 10 ) ;
173+
174+ // Check pattern: should be interleaved (llm, update, llm, update, ...)
168175 const llmCount = executionLog . filter ( x => x === "llm" ) . length ;
169- const updateCount = executionLog . filter ( x => x . startsWith ( "update:" ) ) . length ;
176+ const isInterleaved = llmCount > 0 && updateCalls . length > 0 ;
177+ console . log ( `[Test 3 EXTREME] Pattern: ${ isInterleaved ? "interleaved (current bug - confirms issue)" : "batched (fixed)" } ` ) ;
178+
179+ console . log ( "\n[EXTREME TEST RESULT] Issue #632 CONFIRMED:" ) ;
180+ console . log ( "- 10 entries = 10 update() calls = 10 lock acquisitions" ) ;
181+ console . log ( "- Each entry: LLM + update happen inside one lock cycle" ) ;
182+ console . log ( "- Plugin must wait 10x for entire batch to complete" ) ;
183+ }
184+
185+ // ============================================================================
186+ // Test 4: Mock update() to internally call runWithFileLock (CORRECT WAY)
187+ // ============================================================================
188+
189+ async function testLockCountWithMock ( ) {
190+ let lockCount = 0 ;
191+ const updateCalls = [ ] ;
192+
193+ const store = {
194+ async list ( ) {
195+ return [
196+ { id : "e1" , text : "M1" , category : "fact" , scope : "test" , importance : 0.8 , timestamp : Date . now ( ) , metadata : "{}" } ,
197+ { id : "e2" , text : "M2" , category : "fact" , scope : "test" , importance : 0.8 , timestamp : Date . now ( ) , metadata : "{}" } ,
198+ { id : "e3" , text : "M3" , category : "fact" , scope : "test" , importance : 0.8 , timestamp : Date . now ( ) , metadata : "{}" } ,
199+ { id : "e4" , text : "M4" , category : "fact" , scope : "test" , importance : 0.8 , timestamp : Date . now ( ) , metadata : "{}" } ,
200+ { id : "e5" , text : "M5" , category : "fact" , scope : "test" , importance : 0.8 , timestamp : Date . now ( ) , metadata : "{}" } ,
201+ ] ;
202+ } ,
203+
204+ // Mock runWithFileLock to track lock acquisitions (this is NOT directly called by upgrade())
205+ async runWithFileLock ( fn ) {
206+ lockCount ++ ;
207+ return fn ( ) ; // execute the function inside lock
208+ } ,
209+
210+ // FIXED: update() internally calls runWithFileLock() - simulating real store behavior
211+ async update ( id , patch ) {
212+ updateCalls . push ( id ) ;
213+ // Simulate real store.update() behavior: wraps DB operation in lock
214+ return this . runWithFileLock ( async ( ) => {
215+ return true ;
216+ } ) ;
217+ } ,
218+ } ;
219+
220+ const llm = {
221+ async completeJson ( ) { return null ; } ,
222+ getLastError ( ) { return "" ; } ,
223+ } ;
224+
225+ const upgrader = createMemoryUpgrader ( store , llm , { log : ( ) => { } } ) ;
170226
171- assert . equal ( llmCount , 2 ) ;
172- assert . equal ( updateCount , 2 ) ;
227+ const result = await upgrader . upgrade ( { batchSize : 5 , noLlm : true } ) ;
228+
229+ console . log ( `[Test 4] update() called: ${ updateCalls . length } times` ) ;
230+ console . log ( `[Test 4] runWithFileLock() called: ${ lockCount } times` ) ;
231+ console . log ( `[Test 4] Entries upgraded: ${ result . upgraded } ` ) ;
173232
174- // Check current pattern: should be interleaved (llm, update, llm, update)
175- const isInterleaved = executionLog [ 0 ] === "llm" && executionLog [ 1 ] ?. startsWith ( "update:" ) ;
176- console . log ( `[Test 3] Pattern: ${ isInterleaved ? "interleaved (current bug)" : "batched (fixed)" } ` ) ;
233+ // Current buggy behavior: 5 lock acquisitions (one per entry = one per update())
234+ // After fix: should be 1 lock for entire batch
235+ if ( lockCount === 5 ) {
236+ console . log ( "[Test 4] BUG CONFIRMED: 5 locks for 5 entries (should be 1)" ) ;
237+ assert . equal ( lockCount , 5 , "Confirmed bug" ) ;
238+ } else if ( lockCount === 1 ) {
239+ console . log ( "[Test 4] FIXED: 1 lock for 5 entries" ) ;
240+ assert . equal ( lockCount , 1 , "Expected 1 lock after fix" ) ;
241+ } else {
242+ console . log ( `[Test 4] Unexpected lock count: ${ lockCount } ` ) ;
243+ assert . fail ( `Unexpected lock count: ${ lockCount } ` ) ;
244+ }
177245}
178246
179247// ============================================================================
@@ -201,21 +269,26 @@ async function runTests() {
201269 }
202270
203271 try {
204- console . log ( "--- Test 3: Processing happens one-by-one ---\n" ) ;
205- await testUpgradeProcessesOneByOne ( ) ;
272+ console . log ( "--- Test 3 EXTREME: Batch of 10 entries ---\n" ) ;
273+ await testUpgradeProcessesOneByOneExtreme ( ) ;
274+ console . log ( "" ) ;
275+ } catch ( err ) {
276+ console . log ( `Test 3 EXTREME ERROR: ${ err } \n` ) ;
277+ }
278+
279+ try {
280+ console . log ( "--- Test 4: Mock runWithFileLock to verify lock count ---\n" ) ;
281+ await testLockCountWithMock ( ) ;
206282 console . log ( "" ) ;
207283 } catch ( err ) {
208- console . log ( `Test 3 ERROR: ${ err } \n` ) ;
284+ console . log ( `Test 4 MOCK ERROR (expected in buggy code) : ${ err } \n` ) ;
209285 }
210286
211287 console . log ( "=== Tests Complete ===" ) ;
212- console . log ( "\nCurrent Bug Confirmed:" ) ;
213- console . log ( "- Each entry: LLM + update() in sequence" ) ;
214- console . log ( "- store.update() contains runWithFileLock()" ) ;
215- console . log ( "- Result: batch of N = N lock acquisitions" ) ;
216- console . log ( "\nFix Needed:" ) ;
217- console . log ( "- Phase 1: LLM enrichment for ALL entries (NO lock)" ) ;
218- console . log ( "- Phase 2: Single lock for ALL updates (1 lock for batch)" ) ;
288+ console . log ( "\nSummary:" ) ;
289+ console . log ( "- Tests 1-3: Verify buggy behavior (update called per entry)" ) ;
290+ console . log ( "- Test 4: Uses mock to track lock, FAILS in current code (expected)" ) ;
291+ console . log ( "- After Two-Phase fix, Test 4 should PASS (1 lock instead of 5)" ) ;
219292}
220293
221294runTests ( ) . catch ( ( err ) => {
0 commit comments