@@ -61,15 +61,19 @@ func TestCircuitBreaker_SuccessResetsCounter(t *testing.T) {
6161// TestCircuitBreaker_HalfOpenAfterCooldown verifies OPEN → HALF-OPEN transition.
6262func TestCircuitBreaker_HalfOpenAfterCooldown (t * testing.T ) {
6363 t .Parallel ()
64- // Use a very short cooldown so the test doesn't sleep long.
65- cb := newCircuitBreaker ("test" , 1 , 10 * time .Millisecond )
64+ fakeNow := time .Date (2026 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC )
65+ cb := newCircuitBreaker ("test" , 1 , time .Minute )
66+ cb .nowFunc = func () time.Time { return fakeNow }
6667
6768 cb .RecordRateLimit (time.Time {})
6869 require .Equal (t , circuitOpen , cb .State (), "should be OPEN after 1 error" )
6970
70- // Wait for cooldown.
71- time .Sleep (20 * time .Millisecond )
71+ // Before cooldown: still OPEN.
72+ fakeNow = fakeNow .Add (30 * time .Second )
73+ require .Error (t , cb .Allow (), "should reject before cooldown elapses" )
7274
75+ // After cooldown: transitions to HALF-OPEN.
76+ fakeNow = fakeNow .Add (31 * time .Second )
7377 err := cb .Allow ()
7478 assert .NoError (t , err , "should allow probe after cooldown" )
7579 assert .Equal (t , circuitHalfOpen , cb .State (), "should be HALF-OPEN after cooldown" )
@@ -78,12 +82,14 @@ func TestCircuitBreaker_HalfOpenAfterCooldown(t *testing.T) {
7882// TestCircuitBreaker_HalfOpenClosesOnSuccess verifies HALF-OPEN → CLOSED on probe success.
7983func TestCircuitBreaker_HalfOpenClosesOnSuccess (t * testing.T ) {
8084 t .Parallel ()
81- cb := newCircuitBreaker ("test" , 1 , 10 * time .Millisecond )
85+ fakeNow := time .Date (2026 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC )
86+ cb := newCircuitBreaker ("test" , 1 , time .Minute )
87+ cb .nowFunc = func () time.Time { return fakeNow }
8288
8389 cb .RecordRateLimit (time.Time {})
8490 require .Equal (t , circuitOpen , cb .State ())
8591
86- time . Sleep ( 20 * time .Millisecond )
92+ fakeNow = fakeNow . Add ( 2 * time .Minute )
8793 require .NoError (t , cb .Allow ()) // probe allowed
8894
8995 cb .RecordSuccess ()
@@ -94,12 +100,14 @@ func TestCircuitBreaker_HalfOpenClosesOnSuccess(t *testing.T) {
94100// TestCircuitBreaker_HalfOpenReOpensOnRateLimit verifies HALF-OPEN → OPEN on probe failure.
95101func TestCircuitBreaker_HalfOpenReOpensOnRateLimit (t * testing.T ) {
96102 t .Parallel ()
97- cb := newCircuitBreaker ("test" , 1 , 10 * time .Millisecond )
103+ fakeNow := time .Date (2026 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC )
104+ cb := newCircuitBreaker ("test" , 1 , time .Minute )
105+ cb .nowFunc = func () time.Time { return fakeNow }
98106
99107 cb .RecordRateLimit (time.Time {})
100108 require .Equal (t , circuitOpen , cb .State ())
101109
102- time . Sleep ( 20 * time .Millisecond )
110+ fakeNow = fakeNow . Add ( 2 * time .Minute )
103111 require .NoError (t , cb .Allow ()) // probe allowed
104112
105113 cb .RecordRateLimit (time.Time {})
@@ -114,21 +122,54 @@ func TestCircuitBreaker_HalfOpenReOpensOnRateLimit(t *testing.T) {
114122// TestCircuitBreaker_ResetAtFromHeader verifies the reset time from upstream is used.
115123func TestCircuitBreaker_ResetAtFromHeader (t * testing.T ) {
116124 t .Parallel ()
117- cb := newCircuitBreaker ("test" , 1 , 60 * time .Second )
118- future := time .Now ().Add (5 * time .Millisecond )
119- cb .RecordRateLimit (future )
125+ fakeNow := time .Date (2026 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC )
126+ cb := newCircuitBreaker ("test" , 1 , time .Hour )
127+ cb .nowFunc = func () time.Time { return fakeNow }
128+
129+ resetAt := fakeNow .Add (30 * time .Second )
130+ cb .RecordRateLimit (resetAt )
120131 require .Equal (t , circuitOpen , cb .State ())
121132
122133 // Before the reset time: still OPEN.
134+ fakeNow = fakeNow .Add (15 * time .Second )
123135 require .Error (t , cb .Allow ())
124136
125- // After the reset time: transitions to HALF-OPEN.
126- time . Sleep ( 10 * time .Millisecond )
137+ // After the reset time: transitions to HALF-OPEN (before cooldown would elapse) .
138+ fakeNow = fakeNow . Add ( 20 * time .Second )
127139 err := cb .Allow ()
128140 assert .NoError (t , err , "should allow probe after reset time" )
129141 assert .Equal (t , circuitHalfOpen , cb .State ())
130142}
131143
144+ // TestCircuitBreaker_HalfOpenBlocksConcurrentProbes verifies that only one probe is allowed in HALF-OPEN.
145+ func TestCircuitBreaker_HalfOpenBlocksConcurrentProbes (t * testing.T ) {
146+ t .Parallel ()
147+ fakeNow := time .Date (2026 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC )
148+ cb := newCircuitBreaker ("test" , 1 , time .Minute )
149+ cb .nowFunc = func () time.Time { return fakeNow }
150+
151+ cb .RecordRateLimit (time.Time {})
152+ require .Equal (t , circuitOpen , cb .State ())
153+
154+ // Advance past cooldown to trigger HALF-OPEN.
155+ fakeNow = fakeNow .Add (2 * time .Minute )
156+
157+ // First Allow() should succeed (the probe).
158+ require .NoError (t , cb .Allow ())
159+ assert .Equal (t , circuitHalfOpen , cb .State ())
160+
161+ // Second Allow() should be rejected — probe is already in flight.
162+ err := cb .Allow ()
163+ require .Error (t , err , "concurrent requests in HALF-OPEN should be rejected" )
164+ var openErr * ErrCircuitOpen
165+ require .ErrorAs (t , err , & openErr )
166+
167+ // After the probe succeeds, requests should be allowed again.
168+ cb .RecordSuccess ()
169+ assert .Equal (t , circuitClosed , cb .State ())
170+ assert .NoError (t , cb .Allow ())
171+ }
172+
132173// TestCircuitBreaker_DefaultsApplied verifies zero-value config gets sensible defaults.
133174func TestCircuitBreaker_DefaultsApplied (t * testing.T ) {
134175 t .Parallel ()
@@ -362,3 +403,33 @@ func TestParseRateLimitResetHeader(t *testing.T) {
362403 })
363404 }
364405}
406+
407+ // TestExtractRateLimitErrorText verifies extraction of error text from backend results.
408+ func TestExtractRateLimitErrorText (t * testing.T ) {
409+ t .Parallel ()
410+
411+ t .Run ("extracts text from standard rate-limit result" , func (t * testing.T ) {
412+ t .Parallel ()
413+ result := map [string ]interface {}{
414+ "isError" : true ,
415+ "content" : []interface {}{
416+ map [string ]interface {}{
417+ "type" : "text" ,
418+ "text" : "failed to search: 403 API rate limit exceeded [rate reset in 42s]" ,
419+ },
420+ },
421+ }
422+ assert .Equal (t , "failed to search: 403 API rate limit exceeded [rate reset in 42s]" , extractRateLimitErrorText (result ))
423+ })
424+
425+ t .Run ("returns fallback for nil result" , func (t * testing.T ) {
426+ t .Parallel ()
427+ assert .Equal (t , "rate limit exceeded" , extractRateLimitErrorText (nil ))
428+ })
429+
430+ t .Run ("returns fallback for empty content" , func (t * testing.T ) {
431+ t .Parallel ()
432+ result := map [string ]interface {}{"isError" : true , "content" : []interface {}{}}
433+ assert .Equal (t , "rate limit exceeded" , extractRateLimitErrorText (result ))
434+ })
435+ }
0 commit comments