@@ -660,118 +660,76 @@ func TestChangesResponseLegacyRev(t *testing.T) {
660660// Setup:
661661// - Create doc rev1 on SGW
662662// - Update doc to create rev2 on SGW
663- // - Client connects with V4 subprotocol, delta sync enabled
664- // - Custom changes handler responds with [rev1ID] as knownRevs (simulating a client that only has a legacy rev revision 1)
665- // - Server computes delta from rev1 → rev2 and sends via sendDelta with remoteIsLegacyRev=true
663+ // - Client pre-populated with rev1 as a legacy revtree revision (no HLV)
664+ // - Client pulls with delta sync enabled
665+ // - Server computes delta from rev1 → rev2 and sends via sendDelta
666666//
667667// Expected rev message (buildRevHistory scenario 3):
668668//
669- // history property: [hlvHistory, rev2RevTreeID, rev1RevTreeID] (hlv history here is empty since doc has only been updated by one hlv aware peer)
669+ // history property: [hlvHistory, rev2RevTreeID, rev1RevTreeID] (no hlv pv here since doc has only been updated by one hlv aware peer)
670670// deltaSrc: rev1RevTreeID
671671// body: the delta (not full body)
672- func TestDeltaSyncSendHistoryWithLegacyClient (t * testing.T ) {
672+ func TestDeltaSyncSendRevTreeHistoryWithClientHavingLegacyRev (t * testing.T ) {
673673 if ! base .IsEnterpriseEdition () {
674674 t .Skip ("Delta sync requires EE" )
675675 }
676- base .SetUpTestLogging (t , base .LevelDebug , base .KeyHTTP , base .KeySync , base .KeySyncMsg , base .KeyChanges )
676+
677+ btcRunner := NewBlipTesterClientRunner (t )
678+ btcRunner .SkipSubtest [RevtreeSubtestName ] = true // V4-specific: tests legacy rev in HLV-aware client
677679
678680 sgUseDeltas := true
679- rt := NewRestTester (t , & RestTesterConfig {
680- GuestEnabled : true ,
681- DatabaseConfig : & DatabaseConfig {DbConfig : DbConfig {
682- DeltaSync : & DeltaSyncConfig {
683- Enabled : & sgUseDeltas ,
684- },
685- }},
686- })
687- defer rt .Close ()
681+ btcRunner .Run (func (t * testing.T ) {
682+ rt := NewRestTester (t , & RestTesterConfig {
683+ GuestEnabled : true ,
684+ DatabaseConfig : & DatabaseConfig {DbConfig : DbConfig {
685+ DeltaSync : & DeltaSyncConfig {
686+ Enabled : & sgUseDeltas ,
687+ },
688+ }},
689+ })
690+ defer rt .Close ()
688691
689- bt := NewBlipTesterFromSpecWithRT (rt , & BlipTesterSpec {
690- blipProtocols : []string {db .CBMobileReplicationV4 .SubprotocolString ()},
691- })
692- defer bt .Close ()
692+ docID := SafeDocumentName (t , t .Name ())
693693
694- // Create rev1 and rev2 on SGW
695- docVersion1 := rt .PutDoc ("doc1" , `{"key": "val1"}` )
696- rev1ID := docVersion1 .RevTreeID
697- docVersion2 := rt .UpdateDoc ("doc1" , docVersion1 , `{"key": "val2"}` )
698- rt .WaitForPendingChanges ()
694+ client := btcRunner .NewBlipTesterClientOptsWithRT (rt , & BlipTesterClientOpts {
695+ ClientDeltas : true ,
696+ })
697+ defer client .Close ()
699698
700- receivedChangesRequestWg := sync.WaitGroup {}
701- receivedChangesRequestWg .Add (2 ) // 1 for doc changes + 1 for empty "caught up" changes
699+ // Create rev1 and rev2 on SGW
700+ docVersion1 := rt .PutDoc (docID , `{"key": "val1"}` )
701+ rev1ID := docVersion1 .RevTreeID
702+ docVersion2 := rt .UpdateDoc (docID , docVersion1 , `{"key": "val2"}` )
703+ rt .WaitForPendingChanges ()
702704
703- revsFinishedWg := sync.WaitGroup {}
704- revsFinishedWg .Add (1 ) // expect 1 rev/delta message
705+ // Pre-populate the client with rev1 as a legacy revtree revision so that when SGW sends changes
706+ // for rev2, the client reports rev1ID as its known rev (triggering delta from rev1).
707+ btcRunner .AddRevTreeRev (client .id , docID , rev1ID , EmptyDocVersion (), []byte (`{"key": "val1"}` ))
705708
706- bt . blipContext . HandlerForProfile [ "rev" ] = func ( request * blip. Message ) {
707- defer revsFinishedWg . Done ( )
709+ btcRunner . StartOneshotPull ( client . id )
710+ msg := btcRunner . WaitForPullRevMessage ( client . id , docID , docVersion2 )
708711
709712 // Should be sent as delta with rev1 as the delta source
710- deltaSrc := request .Properties [db .RevMessageDeltaSrc ]
711- assert .Equal (t , rev1ID , deltaSrc , "delta source should be the legacy revID the client reported" )
713+ assert .Equal (t , rev1ID , msg .Properties [db .RevMessageDeltaSrc ], "delta source should be the legacy revID the client reported" )
712714
713715 // The rev property should be the CV (HLV-aware identifier)
714- rev := request .Properties ["rev" ]
715- assert .Equal (t , docVersion2 .CV .String (), rev )
716+ assert .Equal (t , docVersion2 .CV .String (), msg .Properties [db .RevMessageRev ])
716717
717- // History should contain: HLV history, then rev2's revTreeID, then rev1's revTreeID
718- // (scenario 3 in buildRevHistory: local HLV-aware, remote legacy)
719- history := request .Properties [db .RevMessageHistory ]
718+ // History should contain rev2's revTreeID then rev1's revTreeID
719+ // (scenario 3 in buildRevHistory: local HLV-aware, remote legacy; no HLV pv since same source )
720+ history := msg .Properties [db .RevMessageHistory ]
720721 require .NotEmpty (t , history , "history must not be empty — rev tree history is required for legacy client conflict detection" )
721722 historyList := strings .Split (history , "," )
722- // The history should be the rev tree: current revID then parent revID
723- require .Len (t , historyList , 2 , "history should rev tree entries only since hlv history is empty for this doc" )
724- assert .Equal (t , docVersion2 .RevTreeID , historyList [len (historyList )- 2 ], "second to last history entry should be current revTreeID" )
725- assert .Equal (t , docVersion1 .RevTreeID , historyList [len (historyList )- 1 ], "last history entry should be parent revTreeID" )
723+ require .Len (t , historyList , 2 , "history should have rev tree entries only since hlv history is empty for this doc" )
724+ assert .Equal (t , docVersion2 .RevTreeID , historyList [0 ], "first history entry should be current revTreeID" )
725+ assert .Equal (t , docVersion1 .RevTreeID , historyList [1 ], "second history entry should be parent revTreeID" )
726726
727727 // Should NOT have a separate revTreeHistory property (that's only for SGR2 peers)
728- assert .Empty (t , request .Properties [db .RevMessageTreeHistory ], "revTreeHistory property should not be set for non-SGR2 clients" )
729- }
730-
731- bt .blipContext .HandlerForProfile ["changes" ] = func (request * blip.Message ) {
732- defer receivedChangesRequestWg .Done ()
733- body , err := request .Body ()
734- require .NoError (t , err )
735-
736- knownRevs := []any {}
737- if string (body ) != "null" {
738- var changesReqs [][]any
739- err = base .JSONUnmarshal (body , & changesReqs )
740- require .NoError (t , err )
741-
742- knownRevs = make ([]any , len (changesReqs ))
743- for i := range changesReqs {
744- // Respond with the legacy revID as the known rev, simulating a client that has rev1
745- // but predates HLV. This triggers remoteIsLegacyRev=true and sets deltaSrc=rev1ID
746- // on the server side.
747- knownRevs [i ] = []string {rev1ID }
748- }
749- }
750-
751- if ! request .NoReply () {
752- response := request .Response ()
753- // Enable deltas so the server sends a delta rather than full body
754- response .Properties [db .ChangesResponseDeltas ] = "true"
755- emptyResponseValBytes , err := base .JSONMarshal (knownRevs )
756- require .NoError (t , err )
757- response .SetBody (emptyResponseValBytes )
758- }
759- }
760-
761- subChangesRequest := bt .newRequest ()
762- subChangesRequest .SetProfile ("subChanges" )
763- subChangesRequest .Properties ["continuous" ] = "false"
764- sent := bt .sender .Send (subChangesRequest )
765- assert .True (t , sent )
766-
767- subChangesResponse := subChangesRequest .Response ()
768- assert .Equal (t , subChangesRequest .SerialNumber (), subChangesResponse .SerialNumber ())
769-
770- base .WaitWithTimeout (t , & receivedChangesRequestWg , time .Second * 10 )
771- base .WaitWithTimeout (t , & revsFinishedWg , time .Second * 10 )
728+ assert .Empty (t , msg .Properties [db .RevMessageTreeHistory ], "revTreeHistory property should not be set for non-SGR2 clients" )
729+ })
772730}
773731
774- // TestDeltaSyncSendHistoryWithLegacyClientAndHLVHistory verifies that when a delta is sent to a client that has a
732+ // TestDeltaSyncSendRevTreeHistoryWithHLVHistoryClientHavingLegacyRev verifies that when a delta is sent to a client that has a
775733// legacy revision and the document has multi-source HLV history, the history property contains the HLV previous
776734// versions followed by the rev tree.
777735//
@@ -781,120 +739,155 @@ func TestDeltaSyncSendHistoryWithLegacyClient(t *testing.T) {
781739// Setup:
782740// - Create doc rev1 on SGW (gets revTreeID + HLV cv)
783741// - Update doc via HLV agent to simulate a different peer creating rev2 (gets a new HLV cv with rev1's cv as pv)
784- // - Client connects with V4 subprotocol, delta sync enabled
785- // - Custom changes handler responds with [rev1RevTreeID] as knownRevs (legacy client)
742+ // - Client pre-populated with rev1 as a legacy revtree revision (no HLV)
743+ // - Client pulls with delta sync enabled
786744//
787745// Expected rev message (buildRevHistory scenario 3):
788746//
789747// history property: [pv (rev1's cv), rev2RevTreeID, rev1RevTreeID]
790748// deltaSrc: rev1RevTreeID
791- func TestDeltaSyncSendHistoryWithLegacyClientAndHLVHistory (t * testing.T ) {
749+ func TestDeltaSyncSendRevTreeHistoryWithHLVHistoryClientHavingLegacyRev (t * testing.T ) {
792750 if ! base .IsEnterpriseEdition () {
793751 t .Skip ("Delta sync requires EE" )
794752 }
795- base .SetUpTestLogging (t , base .LevelDebug , base .KeyHTTP , base .KeySync , base .KeySyncMsg , base .KeyChanges )
753+
754+ btcRunner := NewBlipTesterClientRunner (t )
755+ btcRunner .SkipSubtest [RevtreeSubtestName ] = true // V4-specific: tests legacy rev in HLV-aware client
796756
797757 sgUseDeltas := true
798- rt := NewRestTester (t , & RestTesterConfig {
799- GuestEnabled : true ,
800- DatabaseConfig : & DatabaseConfig {DbConfig : DbConfig {
801- DeltaSync : & DeltaSyncConfig {
802- Enabled : & sgUseDeltas ,
803- },
804- }},
805- })
806- defer rt .Close ()
758+ btcRunner .Run (func (t * testing.T ) {
759+ rt := NewRestTester (t , & RestTesterConfig {
760+ GuestEnabled : true ,
761+ DatabaseConfig : & DatabaseConfig {DbConfig : DbConfig {
762+ DeltaSync : & DeltaSyncConfig {
763+ Enabled : & sgUseDeltas ,
764+ },
765+ }},
766+ })
767+ defer rt .Close ()
807768
808- bt := NewBlipTesterFromSpecWithRT (rt , & BlipTesterSpec {
809- blipProtocols : []string {db .CBMobileReplicationV4 .SubprotocolString ()},
810- })
811- defer bt .Close ()
769+ docID := SafeDocumentName (t , t .Name ())
812770
813- collection , ctx := rt .GetSingleTestDatabaseCollection ()
771+ client := btcRunner .NewBlipTesterClientOptsWithRT (rt , & BlipTesterClientOpts {
772+ ClientDeltas : true ,
773+ })
774+ defer client .Close ()
814775
815- // Create rev1 on SGW — gets revTreeID + HLV
816- docVersion1 := rt .PutDoc ("doc1" , `{"key": "val1"}` )
817- rev1ID := docVersion1 .RevTreeID
776+ collection , ctx := rt .GetSingleTestDatabaseCollection ()
818777
819- // Update doc via HLV agent to simulate a different peer creating rev2.
820- // This gives the doc a new CV from "peerSource" with rev1's CV as a previous version in the HLV.
821- newDoc , _ , err := collection .GetDocWithXattrs (ctx , "doc1" , db .DocUnmarshalAll )
822- require .NoError (t , err )
823- agent := db .NewHLVAgent (t , rt .GetSingleDataStore (), "peerSource" , base .VvXattrName )
824- _ = agent .UpdateWithHLV (ctx , "doc1" , newDoc .Cas , newDoc .HLV )
778+ // Create rev1 on SGW — gets revTreeID + HLV
779+ docVersion1 := rt .PutDoc (docID , `{"key": "val1"}` )
780+ rev1ID := docVersion1 .RevTreeID
825781
826- // Force import so SGW picks up the HLV agent's update
827- newDoc , err = collection .GetDocument (ctx , "doc1" , db .DocUnmarshalAll )
828- require .NoError (t , err )
829- rt .WaitForPendingChanges ()
782+ // Pre-populate the client with rev1 as a legacy revtree revision so that when SGW sends changes
783+ // for the updated doc, the client reports rev1ID as its known rev (triggering delta from rev1).
784+ btcRunner .AddRevTreeRev (client .id , docID , rev1ID , EmptyDocVersion (), []byte (`{"key": "val1"}` ))
830785
831- receivedChangesRequestWg := sync.WaitGroup {}
832- receivedChangesRequestWg .Add (2 )
833- revsFinishedWg := sync.WaitGroup {}
834- revsFinishedWg .Add (1 )
786+ // Update doc via HLV agent to simulate a different peer creating rev2.
787+ // This gives the doc a new CV from "peerSource" with rev1's CV as a previous version in the HLV.
788+ newDoc , _ , err := collection .GetDocWithXattrs (ctx , docID , db .DocUnmarshalAll )
789+ require .NoError (t , err )
790+ agent := db .NewHLVAgent (t , rt .GetSingleDataStore (), "peerSource" , base .VvXattrName )
791+ _ = agent .UpdateWithHLV (ctx , docID , newDoc .Cas , newDoc .HLV )
835792
836- bt .blipContext .HandlerForProfile ["rev" ] = func (request * blip.Message ) {
837- defer revsFinishedWg .Done ()
793+ // Force import so SGW picks up the HLV agent's update
794+ newDoc , err = collection .GetDocument (ctx , docID , db .DocUnmarshalAll )
795+ require .NoError (t , err )
796+ rt .WaitForPendingChanges ()
797+
798+ docVersion2 := newDoc .ExtractDocVersion ()
799+ btcRunner .StartOneshotPull (client .id )
800+ msg := btcRunner .WaitForPullRevMessage (client .id , docID , docVersion2 )
838801
839802 // Should be sent as delta with rev1 as the delta source
840- deltaSrc := request .Properties [db .RevMessageDeltaSrc ]
841- assert .Equal (t , rev1ID , deltaSrc , "delta source should be the legacy revID the client reported" )
803+ assert .Equal (t , rev1ID , msg .Properties [db .RevMessageDeltaSrc ], "delta source should be the legacy revID the client reported" )
842804
843805 // Rev property should be the new CV from the HLV agent update
844- rev := request .Properties ["rev" ]
845- assert .Equal (t , newDoc .HLV .GetCurrentVersionString (), rev )
806+ assert .Equal (t , docVersion2 .CV .String (), msg .Properties [db .RevMessageRev ])
846807
847808 // History should be: [pv (rev1's original cv), rev2RevTreeID, rev1RevTreeID]
848809 // - First entry is HLV previous version (rev1's cv becomes pv after the HLV agent update)
849810 // - Last two entries are the rev tree (scenario 3: local HLV-aware, remote legacy)
850- history := request .Properties [db .RevMessageHistory ]
811+ history := msg .Properties [db .RevMessageHistory ]
851812 require .NotEmpty (t , history , "history must not be empty" )
852813 historyList := strings .Split (history , "," )
853814 require .Len (t , historyList , 3 , "history should have HLV pv + 2 rev tree entries" )
854815 assert .Equal (t , docVersion1 .CV .String (), historyList [0 ], "first history entry should be HLV previous version (rev1's cv)" )
855- assert .Equal (t , newDoc . GetRevTreeID () , historyList [1 ], "second history entry should be current revTreeID" )
816+ assert .Equal (t , docVersion2 . RevTreeID , historyList [1 ], "second history entry should be current revTreeID" )
856817 assert .Equal (t , docVersion1 .RevTreeID , historyList [2 ], "third history entry should be parent revTreeID" )
857818
858- assert .Empty (t , request .Properties [db .RevMessageTreeHistory ], "revTreeHistory property should not be set for non-SGR2 clients" )
819+ assert .Empty (t , msg .Properties [db .RevMessageTreeHistory ], "revTreeHistory property should not be set for non-SGR2 clients" )
820+ })
821+ }
822+
823+ // TestBothSidesLegacyRevDeltaSync verifies that when a delta is sent to a client that has a legacy revision
824+ // and the document on SGW is also a legacy revision (no HLV on either side), the history property contains
825+ // only the parent revTreeID and the rev property is the current revTreeID (not a CV).
826+ //
827+ // Setup:
828+ // - Create doc rev1 and rev2 on SGW with no HLV (legacy revs)
829+ // - Client pre-populated with rev1 as a legacy revtree revision (no HLV)
830+ // - Client pulls with delta sync enabled
831+ //
832+ // Expected rev message (buildRevHistory scenario 1):
833+ //
834+ // history property: [rev1RevTreeID] (no hlv pv since neither side is HLV-aware)
835+ // rev property: rev2RevTreeID (not a CV, since both sides are legacy)
836+ // deltaSrc: rev1RevTreeID
837+ func TestBothSidesLegacyRevDeltaSync (t * testing.T ) {
838+ if ! base .IsEnterpriseEdition () {
839+ t .Skip ("Delta sync requires EE" )
859840 }
860841
861- bt .blipContext .HandlerForProfile ["changes" ] = func (request * blip.Message ) {
862- defer receivedChangesRequestWg .Done ()
863- body , err := request .Body ()
864- require .NoError (t , err )
842+ btcRunner := NewBlipTesterClientRunner (t )
843+ btcRunner .SkipSubtest [RevtreeSubtestName ] = true // V4-specific: tests legacy rev in HLV-aware client
865844
866- knownRevs := []any {}
867- if string (body ) != "null" {
868- var changesReqs [][]any
869- err = base .JSONUnmarshal (body , & changesReqs )
870- require .NoError (t , err )
845+ sgUseDeltas := true
846+ btcRunner .Run (func (t * testing.T ) {
847+ rt := NewRestTester (t , & RestTesterConfig {
848+ GuestEnabled : true ,
849+ DatabaseConfig : & DatabaseConfig {DbConfig : DbConfig {
850+ DeltaSync : & DeltaSyncConfig {
851+ Enabled : & sgUseDeltas ,
852+ },
853+ }},
854+ })
855+ defer rt .Close ()
871856
872- knownRevs = make ([]any , len (changesReqs ))
873- for i := range changesReqs {
874- knownRevs [i ] = []string {rev1ID }
875- }
876- }
857+ docID := SafeDocumentName (t , t .Name ())
877858
878- if ! request .NoReply () {
879- response := request .Response ()
880- response .Properties [db .ChangesResponseDeltas ] = "true"
881- emptyResponseValBytes , err := base .JSONMarshal (knownRevs )
882- require .NoError (t , err )
883- response .SetBody (emptyResponseValBytes )
884- }
885- }
859+ client := btcRunner .NewBlipTesterClientOptsWithRT (rt , & BlipTesterClientOpts {
860+ ClientDeltas : true ,
861+ })
862+ defer client .Close ()
886863
887- subChangesRequest := bt .newRequest ()
888- subChangesRequest .SetProfile ("subChanges" )
889- subChangesRequest .Properties ["continuous" ] = "false"
890- sent := bt .sender .Send (subChangesRequest )
891- assert .True (t , sent )
864+ // create rev1, rev 2 on SGW with no HLV (legacy revs)
865+ docRev1 := rt .CreateDocNoHLV (docID , db.Body {"test" : "doc" })
866+ docRev2 := rt .CreateDocNoHLV (docID , db.Body {"test" : "update" , db .BodyRev : docRev1 .GetRevTreeID ()})
867+ rt .WaitForPendingChanges ()
892868
893- subChangesResponse := subChangesRequest .Response ()
894- assert .Equal (t , subChangesRequest .SerialNumber (), subChangesResponse .SerialNumber ())
869+ // Pre-populate the client with rev1 as a legacy revtree revision so that when SGW sends changes
870+ // for rev2, the client reports rev1ID as its known rev (triggering delta from rev1).
871+ btcRunner .AddRevTreeRev (client .id , docID , docRev1 .GetRevTreeID (), EmptyDocVersion (), []byte (`{"test": "doc"}` ))
895872
896- base .WaitWithTimeout (t , & receivedChangesRequestWg , time .Second * 10 )
897- base .WaitWithTimeout (t , & revsFinishedWg , time .Second * 10 )
873+ btcRunner .StartOneshotPull (client .id )
874+ msg := btcRunner .WaitForPullRevMessage (client .id , docID , docRev2 .ExtractDocVersion ())
875+
876+ // Should be sent as delta with rev1 as the delta source
877+ assert .Equal (t , docRev1 .GetRevTreeID (), msg .Properties [db .RevMessageDeltaSrc ], "delta source should be the legacy revID the client reported" )
878+
879+ // Rev property should be rev2's rev tree ID since both sides are legacy
880+ assert .Equal (t , docRev2 .GetRevTreeID (), msg .Properties [db .RevMessageRev ])
881+
882+ // History should contain rev1's revTreeID
883+ history := msg .Properties [db .RevMessageHistory ]
884+ require .NotEmpty (t , history , "history must not be empty — rev tree history is required for legacy client conflict detection" )
885+ historyList := strings .Split (history , "," )
886+ require .Len (t , historyList , 1 , "history should have rev tree entries only since hlv history is empty for this doc" )
887+ assert .Equal (t , docRev1 .GetRevTreeID (), historyList [0 ], "only history entry should be parent revTreeID since both sides are legacy" )
888+
889+ assert .Empty (t , msg .Properties [db .RevMessageTreeHistory ], "revTreeHistory property should not be set for non-SGR2 clients" )
890+ })
898891}
899892
900893// TestChangesResponseWithHLVInHistory:
0 commit comments