@@ -657,6 +657,144 @@ describe("Durable Task Scheduler (DTS) E2E Tests", () => {
657657 expect ( invoked ) . toBe ( true ) ;
658658 } , 31000 ) ;
659659
660+ // ==================== Retry Handler Tests ====================
661+
662+ it ( "should fail (not retry infinitely) when retry handler returns undefined" , async ( ) => {
663+ // Issue: A retry handler with a missing return statement returns undefined.
664+ // Before the fix, `undefined !== false` was truthy, so the executor treated it
665+ // as "retry", causing an infinite retry loop. The fix uses a positive check:
666+ // only `true` or a finite number triggers a retry.
667+ let attemptCount = 0 ;
668+
669+ const failingActivity = async ( _ : ActivityContext ) => {
670+ attemptCount ++ ;
671+ throw new Error ( `Failure on attempt ${ attemptCount } ` ) ;
672+ } ;
673+
674+ const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext ) : any {
675+ // Cast to any to simulate a JavaScript consumer or handler with a missing return
676+ const retryHandler = ( async ( _retryCtx : any ) => {
677+ // Intentionally no return statement — returns undefined
678+ } ) as any ;
679+ const result = yield ctx . callActivity ( failingActivity , undefined , { retry : retryHandler } ) ;
680+ return result ;
681+ } ;
682+
683+ taskHubWorker . addActivity ( failingActivity ) ;
684+ taskHubWorker . addOrchestrator ( orchestrator ) ;
685+ await taskHubWorker . start ( ) ;
686+
687+ const id = await taskHubClient . scheduleNewOrchestration ( orchestrator ) ;
688+ const state = await taskHubClient . waitForOrchestrationCompletion ( id , undefined , 30 ) ;
689+
690+ expect ( state ) . toBeDefined ( ) ;
691+ // Should fail after exactly 1 attempt — the handler returned undefined so no retry
692+ expect ( state ?. runtimeStatus ) . toEqual ( OrchestrationStatus . ORCHESTRATION_STATUS_FAILED ) ;
693+ expect ( state ?. failureDetails ) . toBeDefined ( ) ;
694+ expect ( attemptCount ) . toBe ( 1 ) ;
695+ } , 31000 ) ;
696+
697+ it ( "should fail (not retry infinitely) when retry handler returns null" , async ( ) => {
698+ let attemptCount = 0 ;
699+
700+ const failingActivity = async ( _ : ActivityContext ) => {
701+ attemptCount ++ ;
702+ throw new Error ( `Failure on attempt ${ attemptCount } ` ) ;
703+ } ;
704+
705+ const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext ) : any {
706+ const retryHandler = async ( _retryCtx : any ) => {
707+ return null as any ; // Explicitly returning null
708+ } ;
709+ const result = yield ctx . callActivity ( failingActivity , undefined , { retry : retryHandler } ) ;
710+ return result ;
711+ } ;
712+
713+ taskHubWorker . addActivity ( failingActivity ) ;
714+ taskHubWorker . addOrchestrator ( orchestrator ) ;
715+ await taskHubWorker . start ( ) ;
716+
717+ const id = await taskHubClient . scheduleNewOrchestration ( orchestrator ) ;
718+ const state = await taskHubClient . waitForOrchestrationCompletion ( id , undefined , 30 ) ;
719+
720+ expect ( state ) . toBeDefined ( ) ;
721+ expect ( state ?. runtimeStatus ) . toEqual ( OrchestrationStatus . ORCHESTRATION_STATUS_FAILED ) ;
722+ expect ( state ?. failureDetails ) . toBeDefined ( ) ;
723+ expect ( attemptCount ) . toBe ( 1 ) ;
724+ } , 31000 ) ;
725+
726+ it ( "should retry and succeed when retry handler returns true" , async ( ) => {
727+ let attemptCount = 0 ;
728+
729+ const flakyActivity = async ( _ : ActivityContext , input : number ) => {
730+ attemptCount ++ ;
731+ if ( attemptCount < 3 ) {
732+ throw new Error ( `Transient failure on attempt ${ attemptCount } ` ) ;
733+ }
734+ return input * 2 ;
735+ } ;
736+
737+ const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext , input : number ) : any {
738+ const retryHandler = async ( retryCtx : any ) => retryCtx . lastAttemptNumber < 5 ;
739+ const result = yield ctx . callActivity ( flakyActivity , input , { retry : retryHandler } ) ;
740+ return result ;
741+ } ;
742+
743+ taskHubWorker . addActivity ( flakyActivity ) ;
744+ taskHubWorker . addOrchestrator ( orchestrator ) ;
745+ await taskHubWorker . start ( ) ;
746+
747+ const id = await taskHubClient . scheduleNewOrchestration ( orchestrator , 21 ) ;
748+ const state = await taskHubClient . waitForOrchestrationCompletion ( id , undefined , 30 ) ;
749+
750+ expect ( state ) . toBeDefined ( ) ;
751+ expect ( state ?. runtimeStatus ) . toEqual ( OrchestrationStatus . ORCHESTRATION_STATUS_COMPLETED ) ;
752+ expect ( state ?. failureDetails ) . toBeUndefined ( ) ;
753+ expect ( state ?. serializedOutput ) . toEqual ( JSON . stringify ( 42 ) ) ;
754+ expect ( attemptCount ) . toBe ( 3 ) ;
755+ } , 31000 ) ;
756+
757+ it ( "should retry with delay when retry handler returns a positive number" , async ( ) => {
758+ let attemptCount = 0 ;
759+ const attemptTimes : number [ ] = [ ] ;
760+
761+ const flakyActivity = async ( _ : ActivityContext ) => {
762+ attemptCount ++ ;
763+ attemptTimes . push ( Date . now ( ) ) ;
764+ if ( attemptCount < 2 ) {
765+ throw new Error ( `Transient failure on attempt ${ attemptCount } ` ) ;
766+ }
767+ return "success" ;
768+ } ;
769+
770+ const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext ) : any {
771+ const retryHandler = async ( _retryCtx : any ) => {
772+ return 1000 ; // Retry after 1 second
773+ } ;
774+ const result = yield ctx . callActivity ( flakyActivity , undefined , { retry : retryHandler } ) ;
775+ return result ;
776+ } ;
777+
778+ taskHubWorker . addActivity ( flakyActivity ) ;
779+ taskHubWorker . addOrchestrator ( orchestrator ) ;
780+ await taskHubWorker . start ( ) ;
781+
782+ const id = await taskHubClient . scheduleNewOrchestration ( orchestrator ) ;
783+ const state = await taskHubClient . waitForOrchestrationCompletion ( id , undefined , 30 ) ;
784+
785+ expect ( state ) . toBeDefined ( ) ;
786+ expect ( state ?. runtimeStatus ) . toEqual ( OrchestrationStatus . ORCHESTRATION_STATUS_COMPLETED ) ;
787+ expect ( state ?. failureDetails ) . toBeUndefined ( ) ;
788+ expect ( state ?. serializedOutput ) . toEqual ( JSON . stringify ( "success" ) ) ;
789+ expect ( attemptCount ) . toBe ( 2 ) ;
790+
791+ // Verify there was at least ~1s delay between attempts
792+ if ( attemptTimes . length >= 2 ) {
793+ const delay = attemptTimes [ 1 ] - attemptTimes [ 0 ] ;
794+ expect ( delay ) . toBeGreaterThanOrEqual ( 900 ) ; // Allow some tolerance
795+ }
796+ } , 31000 ) ;
797+
660798 // // ==================== newGuid Tests ====================
661799
662800 it ( "should generate deterministic GUIDs with newGuid" , async ( ) => {
0 commit comments