@@ -755,15 +755,131 @@ func TestChannelCacheActiveOnlyScenarios(t *testing.T) {
755755 changes = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
756756 require .Len (t , changes , 0 )
757757 })
758+ t .Run ("cache populated, query requires pagination" , func (t * testing.T ) {
759+ cacheOptions := DefaultCacheOptions ()
760+ cacheOptions .ChannelCacheMaxLength = 5
761+ cacheOptions .ChannelQueryLimit = 5
762+ ctx , db , collection := setupDBWithChannelCacheSettings (t , cacheOptions )
763+ // seed activeChannel in the cache prior to writing docs
764+ changesOptions := ChangesOptions {Since : SequenceID {Seq : 0 }, ChangesCtx : base .TestCtx (t )}
765+ _ = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
766+ // Write 20 docs to the channel. 5 should be cached, 15 require query
767+ for i := range 20 {
768+ docID := fmt .Sprintf ("doc%d" , i )
769+ _ , _ , _ = collection .Put (ctx , docID , Body {"channels" : activeChannel })
770+ }
771+ db .WaitForPendingChanges (t )
772+ changesOptions = ChangesOptions {Since : SequenceID {Seq : 0 }, ActiveOnly : true , Limit : 0 , ChangesCtx : base .TestCtx (t )}
773+ changes := getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
774+ require .Len (t , changes , 20 )
775+ for i , change := range changes {
776+ assert .Equal (t , fmt .Sprintf ("doc%d" , i + 1 ), change .ID , "change at index %d" , i )
777+ }
778+ })
779+ t .Run ("cache populated, query requires pagination with limit" , func (t * testing.T ) {
780+ // With ChannelQueryLimit=5 and Limit=10, each GetChanges call returns 5 entries from the
781+ // query range. Without the queryHitActiveLimit guard, the first call would append the
782+ // cache (docs 16-20) immediately and skip docs 6-15.
783+ cacheOptions := DefaultCacheOptions ()
784+ cacheOptions .ChannelCacheMaxLength = 5
785+ cacheOptions .ChannelQueryLimit = 5
786+ ctx , db , collection := setupDBWithChannelCacheSettings (t , cacheOptions )
787+ changesOptions := ChangesOptions {Since : SequenceID {Seq : 0 }, ChangesCtx : base .TestCtx (t )}
788+ _ = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
789+ for i := 1 ; i <= 20 ; i ++ {
790+ _ , _ , _ = collection .Put (ctx , fmt .Sprintf ("doc%d" , i ), Body {"channels" : activeChannel })
791+ }
792+ db .WaitForPendingChanges (t )
793+ // Limit=10 spans two query batches (5+5) before reaching cache. The pagination loop must
794+ // not prematurely append cache after the first batch hits the active limit.
795+ changesOptions = ChangesOptions {Since : SequenceID {Seq : 0 }, ActiveOnly : true , Limit : 10 , ChangesCtx : base .TestCtx (t )}
796+ changes := getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
797+ require .Len (t , changes , 10 )
798+ for i , change := range changes {
799+ assert .Equal (t , fmt .Sprintf ("doc%d" , i + 1 ), change .ID , "change at index %d" , i )
800+ }
801+ })
758802}
759803
760804func setupDBWithChannelCacheSize (t * testing.T , maxLength int ) (context.Context , * Database , * DatabaseCollectionWithUser ) {
761805 cacheOptions := DefaultCacheOptions ()
762806 cacheOptions .ChannelCacheMaxLength = maxLength
807+ return setupDBWithChannelCacheSettings (t , cacheOptions )
808+ }
809+
810+ func setupDBWithChannelCacheSettings (t * testing.T , cacheOptions CacheOptions ) (context.Context , * Database , * DatabaseCollectionWithUser ) {
763811 db , ctx := setupTestDBWithCacheOptions (t , cacheOptions )
764812 t .Cleanup (func () { db .Close (ctx ) })
765813 collection , ctx := GetSingleDatabaseCollectionWithUser (ctx , t , db )
766814 _ , err := collection .UpdateSyncFun (ctx , channels .DocChannelsSyncFunction )
767815 require .NoError (t , err )
768816 return ctx , db , collection
769817}
818+
819+ // FuzzChannelCacheActiveOnly verifies that GetChanges with ActiveOnly=true returns every active entry
820+ // exactly once, in sequence order, regardless of how results are split between backing-store query
821+ // batches and the in-memory cache.
822+ func FuzzChannelCacheActiveOnly (f * testing.F ) {
823+ f .Add (uint8 (20 ), uint8 (5 ), uint8 (5 ), uint8 (10 )) // query + cache, limit spans two batches
824+ f .Add (uint8 (5 ), uint8 (5 ), uint8 (5 ), uint8 (0 )) // all docs fit in cache, no limit
825+ f .Add (uint8 (1 ), uint8 (5 ), uint8 (5 ), uint8 (1 )) // single doc, limit=1
826+ f .Add (uint8 (0 ), uint8 (5 ), uint8 (5 ), uint8 (5 )) // no docs
827+ f .Add (uint8 (10 ), uint8 (2 ), uint8 (2 ), uint8 (5 )) // tiny cache + single-entry query batches
828+ f .Add (uint8 (15 ), uint8 (5 ), uint8 (5 ), uint8 (5 )) // limit < cache boundary
829+
830+ f .Fuzz (func (t * testing.T , numDocs , cacheMaxLength , queryLimit , requestLimit uint8 ) {
831+ if cacheMaxLength == 0 {
832+ cacheMaxLength = 1
833+ }
834+ if queryLimit == 0 {
835+ queryLimit = 1
836+ }
837+ const maxDocs = 30
838+ n := int (numDocs )
839+ if n > maxDocs {
840+ n = maxDocs
841+ }
842+
843+ cacheOptions := DefaultCacheOptions ()
844+ cacheOptions .ChannelCacheMaxLength = int (cacheMaxLength )
845+ cacheOptions .ChannelQueryLimit = int (queryLimit )
846+ ctx , db , collection := setupDBWithChannelCacheSettings (t , cacheOptions )
847+
848+ // Seed the channel in the cache before writing docs so subsequent writes land
849+ // in the cache from sequence 1.
850+ _ = getChanges (t , collection , base .SetOf ("active" ), ChangesOptions {
851+ Since : SequenceID {Seq : 0 }, ChangesCtx : base .TestCtx (t ),
852+ })
853+ for i := 1 ; i <= n ; i ++ {
854+ _ , _ , _ = collection .Put (ctx , fmt .Sprintf ("doc%d" , i ), Body {"channels" : "active" })
855+ }
856+ db .WaitForPendingChanges (t )
857+
858+ changes := getChanges (t , collection , base .SetOf ("active" ), ChangesOptions {
859+ Since : SequenceID {Seq : 0 },
860+ ActiveOnly : true ,
861+ Limit : int (requestLimit ),
862+ ChangesCtx : base .TestCtx (t ),
863+ })
864+
865+ // No duplicates.
866+ seen := make (map [string ]bool , len (changes ))
867+ for _ , c := range changes {
868+ require .False (t , seen [c .ID ], "duplicate entry %s" , c .ID )
869+ seen [c .ID ] = true
870+ }
871+
872+ // Correct count: all docs, or exactly requestLimit if that's the smaller constraint.
873+ limit := int (requestLimit )
874+ expectedCount := n
875+ if limit > 0 && limit < n {
876+ expectedCount = limit
877+ }
878+ require .Len (t , changes , expectedCount )
879+
880+ // Entries must be the first expectedCount docs in insertion order.
881+ for i , c := range changes {
882+ require .Equal (t , fmt .Sprintf ("doc%d" , i + 1 ), c .ID , "wrong doc at index %d" , i )
883+ }
884+ })
885+ }
0 commit comments