@@ -157,4 +157,112 @@ describe('FeishuApiDelivery', () => {
157157 expect ( String ( secondInit . body ) ) . toContain ( '"root_id":"om_root_1"' ) ;
158158 expect ( String ( secondInit . body ) ) . toContain ( '"reply_to_message_id":"om_ack_1"' ) ;
159159 } ) ;
160+
161+ it ( 'retries message delivery on 429 and succeeds without infinite retry' , async ( ) => {
162+ const fetchMock = vi . fn ( )
163+ . mockResolvedValueOnce ( {
164+ ok : false ,
165+ status : 429 ,
166+ statusText : 'Too Many Requests' ,
167+ headers : new Headers ( { 'retry-after' : '0' } ) ,
168+ text : async ( ) => 'rate limited' ,
169+ } )
170+ . mockResolvedValueOnce ( {
171+ ok : true ,
172+ json : async ( ) => ( { code : 0 , msg : 'ok' , data : { message_id : 'om_retry_1' } } ) ,
173+ } ) ;
174+ global . fetch = fetchMock as typeof fetch ;
175+ const delivery = new FeishuApiDelivery ( {
176+ enabled : true ,
177+ mode : 'live' ,
178+ tenantAccessToken : 'tenant_token_xxx' ,
179+ deliveryMaxRetries : 2 ,
180+ deliveryRetryBaseMs : 0 ,
181+ deliveryRetryMaxMs : 0 ,
182+ } ) ;
183+
184+ const receipt = await delivery . send ( sampleMessage ) ;
185+
186+ expect ( receipt ) . toEqual ( { messageId : 'om_retry_1' , rootId : undefined , threadId : undefined } ) ;
187+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ;
188+ } ) ;
189+
190+ it ( 'retries message delivery on network failure and succeeds' , async ( ) => {
191+ const fetchMock = vi . fn ( )
192+ . mockRejectedValueOnce ( new Error ( 'network down' ) )
193+ . mockResolvedValueOnce ( {
194+ ok : true ,
195+ json : async ( ) => ( { code : 0 , msg : 'ok' , data : { message_id : 'om_retry_network' } } ) ,
196+ } ) ;
197+ global . fetch = fetchMock as typeof fetch ;
198+ const delivery = new FeishuApiDelivery ( {
199+ enabled : true ,
200+ mode : 'live' ,
201+ tenantAccessToken : 'tenant_token_xxx' ,
202+ deliveryMaxRetries : 2 ,
203+ deliveryRetryBaseMs : 0 ,
204+ deliveryRetryMaxMs : 0 ,
205+ } ) ;
206+
207+ const receipt = await delivery . send ( sampleMessage ) ;
208+
209+ expect ( receipt ) . toEqual ( { messageId : 'om_retry_network' , rootId : undefined , threadId : undefined } ) ;
210+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ;
211+ } ) ;
212+
213+ it ( 'does not retry non-retryable 4xx delivery errors' , async ( ) => {
214+ const fetchMock = vi . fn ( ) . mockResolvedValue ( {
215+ ok : false ,
216+ status : 400 ,
217+ statusText : 'Bad Request' ,
218+ headers : new Headers ( ) ,
219+ text : async ( ) => 'invalid payload' ,
220+ } ) ;
221+ global . fetch = fetchMock as typeof fetch ;
222+ const delivery = new FeishuApiDelivery ( {
223+ enabled : true ,
224+ mode : 'live' ,
225+ tenantAccessToken : 'tenant_token_xxx' ,
226+ deliveryMaxRetries : 3 ,
227+ deliveryRetryBaseMs : 0 ,
228+ deliveryRetryMaxMs : 0 ,
229+ } ) ;
230+
231+ await expect ( delivery . send ( sampleMessage ) ) . rejects . toThrow ( 'Failed to send Feishu message: 400 Bad Request - invalid payload' ) ;
232+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ;
233+ } ) ;
234+
235+ it ( 'retries tenant token fetch on 5xx before sending' , async ( ) => {
236+ const fetchMock = vi . fn ( )
237+ . mockResolvedValueOnce ( {
238+ ok : false ,
239+ status : 502 ,
240+ statusText : 'Bad Gateway' ,
241+ headers : new Headers ( ) ,
242+ text : async ( ) => 'upstream error' ,
243+ } )
244+ . mockResolvedValueOnce ( {
245+ ok : true ,
246+ json : async ( ) => ( { code : 0 , msg : 'ok' , tenant_access_token : 'tenant_token_retry' , expire : 7200 } ) ,
247+ } )
248+ . mockResolvedValueOnce ( {
249+ ok : true ,
250+ json : async ( ) => ( { code : 0 , msg : 'ok' , data : { message_id : 'om_retry_token' } } ) ,
251+ } ) ;
252+ global . fetch = fetchMock as typeof fetch ;
253+ const delivery = new FeishuApiDelivery ( {
254+ enabled : true ,
255+ mode : 'live' ,
256+ appId : 'cli_xxx' ,
257+ appSecret : 'sec_xxx' ,
258+ deliveryMaxRetries : 2 ,
259+ deliveryRetryBaseMs : 0 ,
260+ deliveryRetryMaxMs : 0 ,
261+ } ) ;
262+
263+ const receipt = await delivery . send ( sampleMessage ) ;
264+
265+ expect ( receipt ) . toEqual ( { messageId : 'om_retry_token' , rootId : undefined , threadId : undefined } ) ;
266+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 3 ) ;
267+ } ) ;
160268} ) ;
0 commit comments