@@ -435,6 +435,34 @@ func TestBootstrapStallCycleGuardPersistsAndFailsHard(t *testing.T) {
435435 }
436436}
437437
438+ func TestBootstrapStallCycleGuardIgnoresCanceledContext (t * testing.T ) {
439+ s := newBootstrapSyncer (t , newBootstrapClient (1 , 1 ), t .TempDir (), SyncerOptions {
440+ RootCtx : context .Background (),
441+ BootstrapStallCycles : 2 ,
442+ })
443+ s .state .BootstrapCursor = "resume-cursor"
444+ s .state .BootstrapStallCycles = 1
445+
446+ wrappedCanceled := fmt .Errorf ("mount shutting down: %w" , context .Canceled )
447+ if err := s .recordBootstrapCycle ("resume-cursor" , nil , wrappedCanceled ); err != nil {
448+ t .Fatalf ("canceled context must not trip the stall guard: %v" , err )
449+ }
450+ if got := s .state .BootstrapStallCycles ; got != 1 {
451+ t .Fatalf ("canceled context changed stall count to %d, want 1" , got )
452+ }
453+
454+ // DeadlineExceeded is a failed retry at the same checkpoint, so it still
455+ // consumes the remaining attempt and produces the typed hard stop.
456+ err := s .recordBootstrapCycle ("resume-cursor" , nil , context .DeadlineExceeded )
457+ var stalled * BootstrapStalledError
458+ if ! errors .As (err , & stalled ) {
459+ t .Fatalf ("deadline exceeded must count toward the stall limit, got %v" , err )
460+ }
461+ if stalled .Cycles != 2 || stalled .Limit != 2 {
462+ t .Fatalf ("BootstrapStalledError = %#v, want cycles=2 limit=2" , stalled )
463+ }
464+ }
465+
438466// TestBootstrapStallCycleGuardResetsOnCheckpointAdvanceAndCompletion proves
439467// that the counter reflects lack of traversal progress, not merely errors.
440468func TestBootstrapStallCycleGuardResetsOnCheckpointAdvanceAndCompletion (t * testing.T ) {
0 commit comments