@@ -288,6 +288,203 @@ await restartAction.Should().ThrowAsync<ArgumentException>()
288288 . WithMessage ( "*An orchestration with the instanceId non-existent-instance-id was not found*" ) ;
289289 }
290290
291+ [ Fact ]
292+ public async Task SuspendAndResumeInstance_EndToEnd ( )
293+ {
294+ // Arrange
295+ await using HostTestLifetime server = await this . StartLongRunningAsync ( ) ;
296+
297+ string instanceId = await server . Client . ScheduleNewOrchestrationInstanceAsync (
298+ OrchestrationName , input : false ) ;
299+
300+ // Wait for the orchestration to start
301+ await server . Client . WaitForInstanceStartAsync ( instanceId , default ) ;
302+
303+ // Act - Suspend the orchestration
304+ await server . Client . SuspendInstanceAsync ( instanceId , "Test suspension" , default ) ;
305+
306+ // Poll for suspended status (up to 5 seconds)
307+ OrchestrationMetadata ? suspendedMetadata = null ;
308+ DateTime deadline = DateTime . UtcNow . AddSeconds ( 5 ) ;
309+ while ( DateTime . UtcNow < deadline )
310+ {
311+ suspendedMetadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
312+ if ( suspendedMetadata ? . RuntimeStatus == OrchestrationRuntimeStatus . Suspended )
313+ {
314+ break ;
315+ }
316+
317+ await Task . Delay ( TimeSpan . FromMilliseconds ( 100 ) ) ;
318+ }
319+
320+ // Assert - Verify orchestration is suspended
321+ suspendedMetadata . Should ( ) . NotBeNull ( ) ;
322+ suspendedMetadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Suspended ) ;
323+ suspendedMetadata . InstanceId . Should ( ) . Be ( instanceId ) ;
324+
325+ // Act - Resume the orchestration
326+ await server . Client . ResumeInstanceAsync ( instanceId , "Test resumption" , default ) ;
327+
328+ // Poll for running status (up to 5 seconds)
329+ OrchestrationMetadata ? resumedMetadata = null ;
330+ deadline = DateTime . UtcNow . AddSeconds ( 5 ) ;
331+ while ( DateTime . UtcNow < deadline )
332+ {
333+ resumedMetadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
334+ if ( resumedMetadata ? . RuntimeStatus == OrchestrationRuntimeStatus . Running )
335+ {
336+ break ;
337+ }
338+
339+ await Task . Delay ( TimeSpan . FromMilliseconds ( 100 ) ) ;
340+ }
341+
342+ // Assert - Verify orchestration is running again
343+ resumedMetadata . Should ( ) . NotBeNull ( ) ;
344+ resumedMetadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Running ) ;
345+
346+ // Complete the orchestration
347+ await server . Client . RaiseEventAsync ( instanceId , "event" , default ) ;
348+ await server . Client . WaitForInstanceCompletionAsync ( instanceId , default ) ;
349+
350+ // Verify the orchestration completed successfully
351+ OrchestrationMetadata ? completedMetadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
352+ completedMetadata . Should ( ) . NotBeNull ( ) ;
353+ completedMetadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Completed ) ;
354+ }
355+
356+ [ Fact ]
357+ public async Task SuspendInstance_WithoutReason_Succeeds ( )
358+ {
359+ // Arrange
360+ await using HostTestLifetime server = await this . StartLongRunningAsync ( ) ;
361+
362+ string instanceId = await server . Client . ScheduleNewOrchestrationInstanceAsync (
363+ OrchestrationName , input : false ) ;
364+
365+ await server . Client . WaitForInstanceStartAsync ( instanceId , default ) ;
366+
367+ // Act - Suspend without a reason
368+ await server . Client . SuspendInstanceAsync ( instanceId , cancellation : default ) ;
369+
370+ // Poll for suspended status (up to 5 seconds)
371+ OrchestrationMetadata ? suspendedMetadata = null ;
372+ DateTime deadline = DateTime . UtcNow . AddSeconds ( 5 ) ;
373+ while ( DateTime . UtcNow < deadline )
374+ {
375+ suspendedMetadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
376+ if ( suspendedMetadata ? . RuntimeStatus == OrchestrationRuntimeStatus . Suspended )
377+ {
378+ break ;
379+ }
380+
381+ await Task . Delay ( TimeSpan . FromMilliseconds ( 100 ) ) ;
382+ }
383+
384+ // Assert
385+ suspendedMetadata . Should ( ) . NotBeNull ( ) ;
386+ suspendedMetadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Suspended ) ;
387+ }
388+
389+ [ Fact ]
390+ public async Task ResumeInstance_WithoutReason_Succeeds ( )
391+ {
392+ // Arrange
393+ await using HostTestLifetime server = await this . StartLongRunningAsync ( ) ;
394+
395+ string instanceId = await server . Client . ScheduleNewOrchestrationInstanceAsync (
396+ OrchestrationName , input : false ) ;
397+
398+ await server . Client . WaitForInstanceStartAsync ( instanceId , default ) ;
399+ await server . Client . SuspendInstanceAsync ( instanceId , "Test suspension" , default ) ;
400+
401+ // Wait for suspension
402+ DateTime deadline = DateTime . UtcNow . AddSeconds ( 5 ) ;
403+ while ( DateTime . UtcNow < deadline )
404+ {
405+ OrchestrationMetadata ? metadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
406+ if ( metadata ? . RuntimeStatus == OrchestrationRuntimeStatus . Suspended )
407+ {
408+ break ;
409+ }
410+
411+ await Task . Delay ( TimeSpan . FromMilliseconds ( 100 ) ) ;
412+ }
413+
414+ // Act - Resume without a reason
415+ await server . Client . ResumeInstanceAsync ( instanceId , cancellation : default ) ;
416+
417+ // Poll for running status (up to 5 seconds)
418+ OrchestrationMetadata ? resumedMetadata = null ;
419+ deadline = DateTime . UtcNow . AddSeconds ( 5 ) ;
420+ while ( DateTime . UtcNow < deadline )
421+ {
422+ resumedMetadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
423+ if ( resumedMetadata ? . RuntimeStatus == OrchestrationRuntimeStatus . Running )
424+ {
425+ break ;
426+ }
427+
428+ await Task . Delay ( TimeSpan . FromMilliseconds ( 100 ) ) ;
429+ }
430+
431+ // Assert
432+ resumedMetadata . Should ( ) . NotBeNull ( ) ;
433+ resumedMetadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Running ) ;
434+ }
435+
436+ [ Fact ]
437+ public async Task SuspendInstance_AlreadyCompleted_NoError ( )
438+ {
439+ // Arrange
440+ await using HostTestLifetime server = await this . StartAsync ( ) ;
441+
442+ string instanceId = await server . Client . ScheduleNewOrchestrationInstanceAsync (
443+ OrchestrationName , input : false ) ;
444+
445+ await server . Client . WaitForInstanceStartAsync ( instanceId , default ) ;
446+ await server . Client . RaiseEventAsync ( instanceId , "event" , default ) ;
447+ await server . Client . WaitForInstanceCompletionAsync ( instanceId , default ) ;
448+
449+ // Verify it's completed
450+ OrchestrationMetadata ? completedMetadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
451+ completedMetadata . Should ( ) . NotBeNull ( ) ;
452+ completedMetadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Completed ) ;
453+
454+ // Act - Try to suspend a completed orchestration (should not throw)
455+ await server . Client . SuspendInstanceAsync ( instanceId , "Test suspension" , default ) ;
456+
457+ // Assert - Status should remain completed
458+ OrchestrationMetadata ? metadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
459+ metadata . Should ( ) . NotBeNull ( ) ;
460+ metadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Completed ) ;
461+ }
462+
463+ [ Fact ]
464+ public async Task ResumeInstance_NotSuspended_NoError ( )
465+ {
466+ // Arrange
467+ await using HostTestLifetime server = await this . StartLongRunningAsync ( ) ;
468+
469+ string instanceId = await server . Client . ScheduleNewOrchestrationInstanceAsync (
470+ OrchestrationName , input : false ) ;
471+
472+ await server . Client . WaitForInstanceStartAsync ( instanceId , default ) ;
473+
474+ // Verify it's running
475+ OrchestrationMetadata ? runningMetadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
476+ runningMetadata . Should ( ) . NotBeNull ( ) ;
477+ runningMetadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Running ) ;
478+
479+ // Act - Try to resume an already running orchestration (should not throw)
480+ await server . Client . ResumeInstanceAsync ( instanceId , "Test resumption" , default ) ;
481+
482+ // Assert - Status should remain running
483+ OrchestrationMetadata ? metadata = await server . Client . GetInstanceAsync ( instanceId , false ) ;
484+ metadata . Should ( ) . NotBeNull ( ) ;
485+ metadata ! . RuntimeStatus . Should ( ) . Be ( OrchestrationRuntimeStatus . Running ) ;
486+ }
487+
291488 Task < HostTestLifetime > StartAsync ( )
292489 {
293490 static async Task < string > Orchestration ( TaskOrchestrationContext context , bool shouldThrow )
@@ -308,6 +505,30 @@ static async Task<string> Orchestration(TaskOrchestrationContext context, bool s
308505 } ) ;
309506 }
310507
508+ Task < HostTestLifetime > StartLongRunningAsync ( )
509+ {
510+ static async Task < string > LongRunningOrchestration ( TaskOrchestrationContext context , bool shouldThrow )
511+ {
512+ context . SetCustomStatus ( "waiting" ) ;
513+ // Wait for external event or a long timer (5 minutes) to allow suspend/resume operations
514+ Task < string > eventTask = context . WaitForExternalEvent < string > ( "event" ) ;
515+ Task timerTask = context . CreateTimer ( TimeSpan . FromMinutes ( 5 ) , CancellationToken . None ) ;
516+ await Task . WhenAny ( eventTask , timerTask ) ;
517+
518+ if ( shouldThrow )
519+ {
520+ throw new InvalidOperationException ( "Orchestration failed" ) ;
521+ }
522+
523+ return $ "{ shouldThrow } -> output";
524+ }
525+
526+ return this . StartWorkerAsync ( b =>
527+ {
528+ b . AddTasks ( tasks => tasks . AddOrchestratorFunc < bool , string > ( OrchestrationName , LongRunningOrchestration ) ) ;
529+ } ) ;
530+ }
531+
311532 class DateTimeToleranceComparer : IEqualityComparer < DateTimeOffset >
312533 {
313534 public bool Equals ( DateTimeOffset x , DateTimeOffset y ) => ( x - y ) . Duration ( ) < TimeSpan . FromMilliseconds ( 100 ) ;
0 commit comments