@@ -10,6 +10,7 @@ namespace Temporalio.Tests.Worker;
10
10
using Temporalio . Activities ;
11
11
using Temporalio . Api . Common . V1 ;
12
12
using Temporalio . Api . Enums . V1 ;
13
+ using Temporalio . Api . Failure . V1 ;
13
14
using Temporalio . Api . History . V1 ;
14
15
using Temporalio . Client ;
15
16
using Temporalio . Client . Schedules ;
@@ -5792,6 +5793,79 @@ await ExecuteWorkerAsync<NullWithCodecWorkflow>(
5792
5793
client ) ;
5793
5794
}
5794
5795
5796
+ [ Workflow ]
5797
+ public class ActivityFailToFailWorkflow
5798
+ {
5799
+ public static TaskCompletionSource WaitingForCancel { get ; } = new ( ) ;
5800
+
5801
+ [ Activity ]
5802
+ public static async Task WaitForCancelAsync ( )
5803
+ {
5804
+ WaitingForCancel . SetResult ( ) ;
5805
+ while ( ! ActivityExecutionContext . Current . CancellationToken . IsCancellationRequested )
5806
+ {
5807
+ ActivityExecutionContext . Current . Heartbeat ( ) ;
5808
+ await Task . Delay ( 100 ) ;
5809
+ }
5810
+ throw new InvalidOperationException ( "Intentional exception" ) ;
5811
+ }
5812
+
5813
+ [ WorkflowRun ]
5814
+ public Task RunAsync ( ) =>
5815
+ Workflow . ExecuteActivityAsync (
5816
+ ( ) => WaitForCancelAsync ( ) ,
5817
+ new ( )
5818
+ {
5819
+ StartToCloseTimeout = TimeSpan . FromSeconds ( 10 ) ,
5820
+ CancellationType = ActivityCancellationType . WaitCancellationCompleted ,
5821
+ RetryPolicy = new ( ) { MaximumAttempts = 1 } ,
5822
+ HeartbeatTimeout = TimeSpan . FromSeconds ( 1 ) ,
5823
+ } ) ;
5824
+ }
5825
+
5826
+ public class CannotSerializeIntentionalFailureConverter : DefaultFailureConverter
5827
+ {
5828
+ public override Failure ToFailure ( Exception exception , IPayloadConverter payloadConverter )
5829
+ {
5830
+ if ( exception . Message == "Intentional exception" )
5831
+ {
5832
+ throw new InvalidOperationException ( "Intentional conversion failure" ) ;
5833
+ }
5834
+ return base . ToFailure ( exception , payloadConverter ) ;
5835
+ }
5836
+ }
5837
+
5838
+ [ Fact ]
5839
+ public async Task ExecuteWorkflowAsync_ActivityFailToFail_ProperlyHandled ( )
5840
+ {
5841
+ // Need client with failure converter
5842
+ var newOptions = ( TemporalClientOptions ) Client . Options . Clone ( ) ;
5843
+ newOptions . DataConverter = DataConverter . Default with
5844
+ {
5845
+ FailureConverter = new CannotSerializeIntentionalFailureConverter ( ) ,
5846
+ } ;
5847
+ var client = new TemporalClient ( Client . Connection , newOptions ) ;
5848
+ await ExecuteWorkerAsync < ActivityFailToFailWorkflow > (
5849
+ async worker =>
5850
+ {
5851
+ var handle = await client . StartWorkflowAsync (
5852
+ ( ActivityFailToFailWorkflow wf ) => wf . RunAsync ( ) ,
5853
+ new ( id : $ "workflow-{ Guid . NewGuid ( ) } ", taskQueue : worker . Options . TaskQueue ! ) ) ;
5854
+ // Wait until activity started
5855
+ await ActivityFailToFailWorkflow . WaitingForCancel . Task ;
5856
+ // Issue cancel and wait result
5857
+ await handle . CancelAsync ( ) ;
5858
+ var err = await Assert . ThrowsAsync < WorkflowFailedException > ( ( ) =>
5859
+ handle . GetResultAsync ( ) ) ;
5860
+ var errAct = Assert . IsType < ActivityFailureException > ( err . InnerException ) ;
5861
+ var errFail = Assert . IsType < ApplicationFailureException > ( errAct . InnerException ) ;
5862
+ Assert . Contains ( "Failed building completion" , errFail . Message ) ;
5863
+ Assert . Contains ( "Intentional conversion failure" , errFail . Message ) ;
5864
+ } ,
5865
+ new TemporalWorkerOptions ( ) . AddAllActivities < ActivityFailToFailWorkflow > ( null ) ,
5866
+ client ) ;
5867
+ }
5868
+
5795
5869
[ Workflow ]
5796
5870
public class DetachedCancellationWorkflow
5797
5871
{
0 commit comments