@@ -526,6 +526,87 @@ func TestSync_OverwriteNewerChunkDoesNotBlockInterval(t *testing.T) {
526526 })
527527}
528528
529+ // TestSync_Start0_SkipsGapDetection confirms that when start=0 the leading-gap
530+ // check and the contiguous-frontier cap are both skipped (both are guarded by
531+ // start > 0). Even though the server's first chunk is at BinID=5, the offer is
532+ // non-empty and topmost equals the historical cursor.
533+ func TestSync_Start0_SkipsGapDetection (t * testing.T ) {
534+ synctest .Test (t , func (t * testing.T ) {
535+ ch := testingc .GenerateTestRandomChunk ()
536+ stampHash , err := ch .Stamp ().Hash ()
537+ if err != nil {
538+ t .Fatal (err )
539+ }
540+ const binID = uint64 (5 )
541+ result := []* storer.BinC {{
542+ Address : ch .Address (),
543+ BatchID : ch .Stamp ().BatchID (),
544+ BinID : binID ,
545+ StampHash : stampHash ,
546+ }}
547+
548+ var (
549+ ps , _ = newPullSync (t , nil , 10 , mock .WithSubscribeResp (result , nil ), mock .WithChunks (ch ), mock .WithCursors ([]uint64 {binID }, 0 ))
550+ recorder = streamtest .New (streamtest .WithProtocols (ps .Protocol ()))
551+ psClient , _ = newPullSync (t , recorder , 0 )
552+ )
553+
554+ topmost , count , err := psClient .Sync (context .Background (), swarm .ZeroAddress , 0 , 0 )
555+ if err != nil {
556+ t .Fatal (err )
557+ }
558+ // Neither gap check fires for start=0; topmost must equal the cursor.
559+ if topmost != binID {
560+ t .Fatalf ("topmost: got %d, want %d (gap detection must be skipped for start=0)" , topmost , binID )
561+ }
562+ if count != 1 {
563+ t .Fatalf ("count: got %d, want 1" , count )
564+ }
565+ })
566+ }
567+
568+ // TestSync_BinBeyondCursors_StallsInterval verifies the safe-stall behaviour
569+ // when bin is beyond the cursors slice returned by ReserveLastBinIDs.
570+ // historicalCursor defaults to 0, so the cursor cap sets topmost=0 for any
571+ // non-empty offer. The client stores the chunk but does not advance the
572+ // interval (top=0 < start=1 in the puller guard).
573+ func TestSync_BinBeyondCursors_StallsInterval (t * testing.T ) {
574+ synctest .Test (t , func (t * testing.T ) {
575+ ch := testingc .GenerateTestRandomChunk ()
576+ stampHash , err := ch .Stamp ().Hash ()
577+ if err != nil {
578+ t .Fatal (err )
579+ }
580+ result := []* storer.BinC {{
581+ Address : ch .Address (),
582+ BatchID : ch .Stamp ().BatchID (),
583+ BinID : 1 ,
584+ StampHash : stampHash ,
585+ }}
586+
587+ // Cursors slice has only one entry (bin=0). Syncing bin=1 leaves
588+ // historicalCursor=0, triggering the safe-stall path.
589+ var (
590+ ps , _ = newPullSync (t , nil , 10 , mock .WithSubscribeResp (result , nil ), mock .WithChunks (ch ), mock .WithCursors ([]uint64 {10 }, 0 ))
591+ recorder = streamtest .New (streamtest .WithProtocols (ps .Protocol ()))
592+ psClient , _ = newPullSync (t , recorder , 0 )
593+ )
594+
595+ topmost , count , err := psClient .Sync (context .Background (), swarm .ZeroAddress , 1 , 1 )
596+ if err != nil {
597+ t .Fatal (err )
598+ }
599+ // historicalCursor=0 for bin=1 → cursor cap fires → topmost=0.
600+ if topmost != 0 {
601+ t .Fatalf ("topmost: got %d, want 0 (bin beyond cursors must stall interval)" , topmost )
602+ }
603+ // The chunk is still stored; only interval advancement is suppressed.
604+ if count != 1 {
605+ t .Fatalf ("count: got %d, want 1 (chunk must be stored despite topmost=0)" , count )
606+ }
607+ })
608+ }
609+
529610func haveChunks (t * testing.T , s * mock.ReserveStore , chunks ... swarm.Chunk ) {
530611 t .Helper ()
531612 for _ , c := range chunks {
0 commit comments