@@ -482,3 +482,119 @@ describe("resolve() routing", () => {
482482 } )
483483 } )
484484} )
485+
486+ describe ( "sweep marker" , ( ) => {
487+ test ( "seed writes marker, second ensureShard skips seed" , async ( ) => {
488+ await scope ( async ( ) => {
489+ const session = await Session . create ( { } )
490+ const sid = session . id
491+
492+ // Write a message via Sync → goes to shard
493+ await msg ( sid , "shard-msg" )
494+
495+ // Simulate orphan: insert directly into global
496+ const src = Database . Client ( ) . $client
497+ const now = Date . now ( )
498+ src . query ( "INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)" ) . run (
499+ MessageID . ascending ( ) , sid , now , now , JSON . stringify ( { role : "user" , text : "orphan" } ) ,
500+ )
501+
502+ // First ensureShard: no marker → should seed
503+ Database . closeSession ( sid )
504+ const db1 = Database . ensureShard ( sid )
505+ expect ( db1 ) . toBe ( sid )
506+
507+ // Verify marker was written
508+ const shard = Database . session ( sid ) . $client
509+ const marker = shard . query ( "SELECT value FROM _meta WHERE key = 'swept'" ) . get ( ) as { value : string } | null
510+ expect ( marker ) . not . toBeNull ( )
511+ expect ( Number ( marker ! . value ) ) . toBeGreaterThan ( 0 )
512+
513+ // Close and reopen to reset in-memory swept set
514+ Database . closeSession ( sid )
515+ Database . resetSwept ( )
516+
517+ // Second ensureShard: marker matches global MAX → should skip seed
518+ const before = performance . now ( )
519+ const db2 = Database . ensureShard ( sid )
520+ const elapsed = performance . now ( ) - before
521+ expect ( db2 ) . toBe ( sid )
522+ // Should be fast (< 50ms) because seed was skipped
523+ expect ( elapsed ) . toBeLessThan ( 50 )
524+ } )
525+ } )
526+
527+ test ( "new orphan after sweep triggers re-seed" , async ( ) => {
528+ await scope ( async ( ) => {
529+ const session = await Session . create ( { } )
530+ const sid = session . id
531+
532+ // Write initial message and trigger first sweep
533+ await msg ( sid , "initial" )
534+ const src = Database . Client ( ) . $client
535+ const now = Date . now ( )
536+ src . query ( "INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)" ) . run (
537+ MessageID . ascending ( ) , sid , now , now , JSON . stringify ( { role : "user" , text : "orphan-1" } ) ,
538+ )
539+
540+ Database . closeSession ( sid )
541+ Database . ensureShard ( sid )
542+
543+ // Verify orphan was copied to shard
544+ const shard1 = Database . session ( sid )
545+ const count1 = shard1
546+ . select ( )
547+ . from ( MessageTable )
548+ . where ( eq ( MessageTable . session_id , sid ) )
549+ . all ( ) . length
550+ expect ( count1 ) . toBe ( 2 ) // shard-msg + orphan-1
551+
552+ // Now add ANOTHER orphan with a newer timestamp
553+ const later = now + 10000
554+ src . query ( "INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)" ) . run (
555+ MessageID . ascending ( ) , sid , later , later , JSON . stringify ( { role : "user" , text : "orphan-2" } ) ,
556+ )
557+
558+ // Reset swept set to simulate process restart
559+ Database . closeSession ( sid )
560+ Database . resetSwept ( )
561+
562+ // ensureShard should detect stale marker and re-seed
563+ Database . ensureShard ( sid )
564+
565+ const shard2 = Database . session ( sid )
566+ const count2 = shard2
567+ . select ( )
568+ . from ( MessageTable )
569+ . where ( eq ( MessageTable . session_id , sid ) )
570+ . all ( ) . length
571+ expect ( count2 ) . toBe ( 3 ) // shard-msg + orphan-1 + orphan-2
572+ } )
573+ } )
574+
575+ test ( "marker survives shard cache eviction" , async ( ) => {
576+ await scope ( async ( ) => {
577+ const session = await Session . create ( { } )
578+ const sid = session . id
579+
580+ // Plant orphan and sweep
581+ const src = Database . Client ( ) . $client
582+ const now = Date . now ( )
583+ src . query ( "INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)" ) . run (
584+ MessageID . ascending ( ) , sid , now , now , JSON . stringify ( { role : "user" , text : "orphan" } ) ,
585+ )
586+
587+ Database . ensureShard ( sid )
588+
589+ // Close the shard (simulates cache eviction)
590+ Database . closeSession ( sid )
591+ Database . resetSwept ( )
592+
593+ // Reopen — marker should still be in the sqlite file
594+ const shard = Database . session ( sid ) . $client
595+ const marker = shard . query ( "SELECT value FROM _meta WHERE key = 'swept'" ) . get ( ) as { value : string } | null
596+ expect ( marker ) . not . toBeNull ( )
597+ expect ( Number ( marker ! . value ) ) . toBe ( now )
598+ } )
599+ } )
600+ } )
0 commit comments