66 transientShouldRetry ,
77 isAppTimeoutError ,
88 runWithPolicy ,
9+ AppTimeoutError ,
10+ getHttpStatus ,
911} from './retry.js' ;
1012
1113afterEach ( ( ) => {
@@ -19,6 +21,61 @@ describe('defaultShouldRetry', () => {
1921 it ( 'does not retry on 400' , ( ) => {
2022 expect ( defaultShouldRetry ( new Error ( 'HTTP 400' ) ) ) . toBe ( false ) ;
2123 } ) ;
24+
25+ it ( 'retries on structured status 429 without 429 in message' , ( ) => {
26+ const err = Object . assign ( new Error ( 'Too many requests' ) , { status : 429 } ) ;
27+ expect ( defaultShouldRetry ( err ) ) . toBe ( true ) ;
28+ } ) ;
29+
30+ it ( 'retries on structured status 408 without status in message' , ( ) => {
31+ const err = Object . assign ( new Error ( 'Request timeout' ) , { status : 408 } ) ;
32+ expect ( defaultShouldRetry ( err ) ) . toBe ( true ) ;
33+ } ) ;
34+
35+ it ( 'retries on PineconeUnmappedHttpError with Status: 429 in message' , ( ) => {
36+ const err = Object . assign ( new Error ( 'Status: 429. Body: throttled' ) , {
37+ name : 'PineconeUnmappedHttpError' ,
38+ } ) ;
39+ expect ( defaultShouldRetry ( err ) ) . toBe ( true ) ;
40+ expect ( getHttpStatus ( err ) ) . toBe ( 429 ) ;
41+ } ) ;
42+
43+ it ( 'retries on statusCode 503 without status code in message' , ( ) => {
44+ const err = Object . assign ( new Error ( 'Service unavailable' ) , { statusCode : 503 } ) ;
45+ expect ( defaultShouldRetry ( err ) ) . toBe ( true ) ;
46+ } ) ;
47+
48+ it ( 'retries on PineconeUnavailableError with reworded message' , ( ) => {
49+ const err = Object . assign ( new Error ( 'Service is down for maintenance' ) , {
50+ name : 'PineconeUnavailableError' ,
51+ } ) ;
52+ expect ( defaultShouldRetry ( err ) ) . toBe ( true ) ;
53+ expect ( getHttpStatus ( err ) ) . toBe ( 503 ) ;
54+ } ) ;
55+
56+ it ( 'retries on ECONNRESET message without structured status' , ( ) => {
57+ expect ( defaultShouldRetry ( new Error ( 'ECONNRESET' ) ) ) . toBe ( true ) ;
58+ } ) ;
59+
60+ it ( 'retries when retryable network signal is only on cause chain' , ( ) => {
61+ const wrapped = new Error ( 'Request failed' , { cause : new Error ( 'ECONNRESET' ) } ) ;
62+ expect ( defaultShouldRetry ( wrapped ) ) . toBe ( true ) ;
63+ } ) ;
64+
65+ it ( 'does not retry wrapper with generic cause and no retry signals' , ( ) => {
66+ const wrapped = new Error ( 'Request failed' , { cause : new Error ( 'upstream failed' ) } ) ;
67+ expect ( defaultShouldRetry ( wrapped ) ) . toBe ( false ) ;
68+ } ) ;
69+
70+ it ( 'does not retry AppTimeoutError even via defaultShouldRetry' , ( ) => {
71+ expect ( defaultShouldRetry ( new AppTimeoutError ( 50 , 'search' ) ) ) . toBe ( false ) ;
72+ } ) ;
73+
74+ it ( 'does not retry when structured status is non-retryable despite retryable message' , ( ) => {
75+ const err = Object . assign ( new Error ( 'HTTP 503 upstream glitch' ) , { status : 401 } ) ;
76+ expect ( getHttpStatus ( err ) ) . toBe ( 401 ) ;
77+ expect ( defaultShouldRetry ( err ) ) . toBe ( false ) ;
78+ } ) ;
2279} ) ;
2380
2481describe ( 'transientShouldRetry' , ( ) => {
@@ -35,12 +92,37 @@ describe('transientShouldRetry', () => {
3592 false
3693 ) ;
3794 } ) ;
95+
96+ it ( 'does not retry AppTimeoutError' , ( ) => {
97+ expect ( transientShouldRetry ( new AppTimeoutError ( 50 , 'search' ) ) ) . toBe ( false ) ;
98+ } ) ;
3899} ) ;
39100
40101describe ( 'isAppTimeoutError' , ( ) => {
41102 it ( 'matches withTimeout rejection messages' , ( ) => {
42103 expect ( isAppTimeoutError ( new Error ( 'Timeout after 50ms while waiting for search' ) ) ) . toBe ( true ) ;
43104 } ) ;
105+
106+ it ( 'matches AppTimeoutError instances' , ( ) => {
107+ expect ( isAppTimeoutError ( new AppTimeoutError ( 50 , 'search' ) ) ) . toBe ( true ) ;
108+ } ) ;
109+
110+ it ( 'matches AppTimeoutError in cause chain' , ( ) => {
111+ const wrapped = new Error ( 'wrapped' , { cause : new AppTimeoutError ( 50 , 'search' ) } ) ;
112+ expect ( isAppTimeoutError ( wrapped ) ) . toBe ( true ) ;
113+ } ) ;
114+
115+ it ( 'matches legacy withTimeout message prefix in cause chain' , ( ) => {
116+ const wrapped = new Error ( 'wrapper' , {
117+ cause : new Error ( 'Timeout after 50ms while waiting for search' ) ,
118+ } ) ;
119+ expect ( isAppTimeoutError ( wrapped ) ) . toBe ( true ) ;
120+ } ) ;
121+
122+ it ( 'does not match generic wrapper without timeout in cause chain' , ( ) => {
123+ const wrapped = new Error ( 'wrapper' , { cause : new Error ( 'upstream failed' ) } ) ;
124+ expect ( isAppTimeoutError ( wrapped ) ) . toBe ( false ) ;
125+ } ) ;
44126} ) ;
45127
46128describe ( 'runWithPolicy' , ( ) => {
@@ -82,7 +164,7 @@ describe('runWithPolicy', () => {
82164 } ,
83165 { timeoutMs : 50 , label : 'test' , retries : 2 , backoffMs : 1 }
84166 ) ;
85- const assertion = expect ( p ) . rejects . toThrow ( / T i m e o u t a f t e r 5 0 m s / ) ;
167+ const assertion = expect ( p ) . rejects . toBeInstanceOf ( AppTimeoutError ) ;
86168 await vi . advanceTimersByTimeAsync ( 50 ) ;
87169 await assertion ;
88170 expect ( n ) . toBe ( 1 ) ;
@@ -99,7 +181,7 @@ describe('withTimeout', () => {
99181 } ) ,
100182 { timeoutMs : 100 , label : 'test' }
101183 ) ;
102- const assertion = expect ( p ) . rejects . toThrow ( / T i m e o u t a f t e r 1 0 0 m s / ) ;
184+ const assertion = expect ( p ) . rejects . toBeInstanceOf ( AppTimeoutError ) ;
103185 await vi . advanceTimersByTimeAsync ( 100 ) ;
104186 await assertion ;
105187 } ) ;
0 commit comments