@@ -472,3 +472,159 @@ func TestCircuitBreaker_IntegrationWithCron(t *testing.T) {
472472 t .Errorf ("expected 2 executions (circuit should open), got %d" , got )
473473 }
474474}
475+
476+
477+ // TestCircuitBreaker_BoundaryThresholdExact tests retry.go:267 (isHalfOpen) boundary condition.
478+ // This kills CONDITIONALS_BOUNDARY mutation where `>= threshold` could become `> threshold`.
479+ // When failures == threshold exactly, isHalfOpen should return true.
480+ func TestCircuitBreaker_BoundaryThresholdExact (t * testing.T ) {
481+ var executions int32
482+ threshold := 2 // Use small threshold for clarity
483+
484+ wrapped := CircuitBreaker (DiscardLogger , threshold , time .Hour )(
485+ FuncJob (func () {
486+ atomic .AddInt32 (& executions , 1 )
487+ panic ("always fails" )
488+ }),
489+ )
490+
491+ // Fail exactly `threshold` times (2 times)
492+ for i := 0 ; i < threshold ; i ++ {
493+ func () {
494+ defer func () { recover () }()
495+ wrapped .Run ()
496+ }()
497+ }
498+
499+ // At this point: failures == threshold (2 == 2)
500+ // Circuit should be OPEN (isHalfOpen returns true for failures >= threshold)
501+ // With mutation >= → >: isHalfOpen would return false (2 > 2 is false)
502+ // and circuit would incorrectly allow execution
503+
504+ execsBefore := atomic .LoadInt32 (& executions )
505+
506+ // This run should be SKIPPED if circuit is correctly open
507+ wrapped .Run ()
508+
509+ execsAfter := atomic .LoadInt32 (& executions )
510+
511+ // If circuit is correctly open, executions should NOT increase
512+ if execsAfter != execsBefore {
513+ t .Errorf ("circuit should be open at exact threshold: failures=%d, threshold=%d, " +
514+ "expected executions to stay at %d, got %d" ,
515+ threshold , threshold , execsBefore , execsAfter )
516+ }
517+
518+ // Verify we had exactly threshold executions before circuit opened
519+ if execsBefore != int32 (threshold ) {
520+ t .Errorf ("expected %d executions before circuit opened, got %d" , threshold , execsBefore )
521+ }
522+ }
523+
524+ // TestCircuitBreaker_ResetOnSuccessBoundary tests retry.go:282 (resetOnSuccess) boundary condition.
525+ // This kills CONDITIONALS_BOUNDARY mutation where `>= threshold` could become `> threshold`.
526+ // When failures == threshold, wasOpen should return true on successful reset.
527+ func TestCircuitBreaker_ResetOnSuccessBoundary (t * testing.T ) {
528+ var executions int32
529+ var shouldFail atomic.Bool
530+ threshold := 2
531+ cooldown := 50 * time .Millisecond
532+
533+ shouldFail .Store (true )
534+
535+ wrapped := CircuitBreaker (DiscardLogger , threshold , cooldown )(
536+ FuncJob (func () {
537+ atomic .AddInt32 (& executions , 1 )
538+ if shouldFail .Load () {
539+ panic ("controlled failure" )
540+ }
541+ }),
542+ )
543+
544+ // Fail exactly threshold times to open circuit
545+ for i := 0 ; i < threshold ; i ++ {
546+ func () {
547+ defer func () { recover () }()
548+ wrapped .Run ()
549+ }()
550+ }
551+
552+ // At this point: failures == threshold (circuit open)
553+ // Wait for cooldown to allow half-open state
554+ time .Sleep (cooldown + 10 * time .Millisecond )
555+
556+ // Set up to succeed
557+ shouldFail .Store (false )
558+
559+ execsBefore := atomic .LoadInt32 (& executions )
560+
561+ // Execute in half-open state - should succeed and reset circuit
562+ wrapped .Run ()
563+
564+ execsAfter := atomic .LoadInt32 (& executions )
565+
566+ // Execution should have happened (half-open allows one attempt)
567+ if execsAfter != execsBefore + 1 {
568+ t .Errorf ("half-open state should allow execution: expected %d, got %d" ,
569+ execsBefore + 1 , execsAfter )
570+ }
571+
572+ // Now circuit should be closed, run again to verify
573+ wrapped .Run ()
574+
575+ execsFinal := atomic .LoadInt32 (& executions )
576+ if execsFinal != execsAfter + 1 {
577+ t .Errorf ("circuit should be closed after successful reset: expected %d, got %d" ,
578+ execsAfter + 1 , execsFinal )
579+ }
580+ }
581+
582+ // TestCircuitBreaker_IsOpenBoundary tests retry.go:259 (isOpen) at exact threshold.
583+ // Combined with cooldown to verify open state detection at boundary.
584+ func TestCircuitBreaker_IsOpenBoundary (t * testing.T ) {
585+ var executions int32
586+ threshold := 3
587+ cooldown := 100 * time .Millisecond
588+
589+ wrapped := CircuitBreaker (DiscardLogger , threshold , cooldown )(
590+ FuncJob (func () {
591+ atomic .AddInt32 (& executions , 1 )
592+ panic ("always fails" )
593+ }),
594+ )
595+
596+ // Fail exactly threshold times
597+ for i := 0 ; i < threshold ; i ++ {
598+ func () {
599+ defer func () { recover () }()
600+ wrapped .Run ()
601+ }()
602+ }
603+
604+ // Verify exactly threshold executions happened
605+ if got := atomic .LoadInt32 (& executions ); got != int32 (threshold ) {
606+ t .Fatalf ("expected %d executions, got %d" , threshold , got )
607+ }
608+
609+ // Circuit should be open - next call should be skipped (within cooldown)
610+ wrapped .Run ()
611+
612+ if got := atomic .LoadInt32 (& executions ); got != int32 (threshold ) {
613+ t .Errorf ("circuit should be open at exact threshold, executions should stay at %d, got %d" ,
614+ threshold , got )
615+ }
616+
617+ // Wait past cooldown - half-open state should allow one execution
618+ time .Sleep (cooldown + 20 * time .Millisecond )
619+
620+ func () {
621+ defer func () { recover () }()
622+ wrapped .Run ()
623+ }()
624+
625+ // Should have one more execution (half-open attempt)
626+ if got := atomic .LoadInt32 (& executions ); got != int32 (threshold )+ 1 {
627+ t .Errorf ("half-open state should allow execution, expected %d, got %d" ,
628+ threshold + 1 , got )
629+ }
630+ }
0 commit comments