Skip to main content

temporalio_common_wasm/data_converters/
failure_converter.rs

1//! Failure conversion sits at the normalized boundary between Rust-side error surfaces and
2//! Temporal's proto [`Failure`] transport object.
3//!
4//! - [`FailureConverter`] owns translation between proto [`Failure`] and the SDK's shared
5//!   normalized error model.
6//! - encode-side call sites adapt caller-facing errors into [`OutgoingError`] before reaching this
7//!   module.
8//! - decode-side call sites first normalize proto failures into [`IncomingError`], then
9//!   [`FailureDecodeHint`] implementations adapt that normalized value into the caller-facing error
10//!   type they expect.
11
12use super::{PayloadConversionError, PayloadConverter, SerializationContextData};
13use crate::{
14    error::{
15        ActivityExecutionError, ActivityFailureError, ApplicationFailure, CancelledError,
16        ChildWorkflowExecutionError, ChildWorkflowFailureError, ChildWorkflowStartError,
17        IncomingError, IncomingNexusHandlerError, IncomingNexusOperationExecutionError,
18        OutgoingActivityError, OutgoingError, OutgoingWorkflowError, ResetWorkflowError,
19        ServerError, TerminatedError, TimeoutError, WorkflowSignalError,
20        WorkflowSignalFailureError,
21    },
22    protos::temporal::api::{
23        enums::v1::ApplicationErrorCategory as ProtoApplicationErrorCategory,
24        failure::v1::{
25            ActivityFailureInfo, ApplicationFailureInfo, CanceledFailureInfo,
26            ChildWorkflowExecutionFailureInfo, Failure, failure::FailureInfo,
27        },
28    },
29};
30
31/// Converts between Rust errors and Temporal [`Failure`] protobufs.
32pub trait FailureConverter {
33    /// Convert an error into a Temporal failure protobuf.
34    fn to_failure(
35        &self,
36        error: OutgoingError,
37        payload_converter: &PayloadConverter,
38        context: &SerializationContextData,
39    ) -> Failure;
40
41    /// Convert a Temporal failure protobuf back into a Rust error.
42    fn to_error(
43        &self,
44        failure: Failure,
45        payload_converter: &PayloadConverter,
46        context: &SerializationContextData,
47    ) -> Result<IncomingError, PayloadConversionError>;
48}
49
50/// Default failure converter.
51pub struct DefaultFailureConverter;
52
53/// Adapts a normalized incoming failure into a caller-facing error surface.
54pub trait FailureDecodeHint {
55    /// The caller-facing error type produced by this hint.
56    type Output;
57
58    /// Adapt a normalized incoming error to the caller-facing output.
59    fn adapt(self, normalized: IncomingError) -> Self::Output;
60}
61
62/// Decode hint for activity execution results.
63#[derive(Debug, Clone, Copy)]
64pub struct ActivityExecutionDecodeHint {
65    /// Whether the workflow-side resolution was cancelled rather than failed.
66    pub cancelled: bool,
67}
68
69impl FailureDecodeHint for ActivityExecutionDecodeHint {
70    type Output = ActivityExecutionError;
71
72    fn adapt(self, normalized: IncomingError) -> Self::Output {
73        match normalized {
74            IncomingError::Activity(activity) => {
75                if self.cancelled && matches!(activity.cause(), Some(IncomingError::Cancelled(_))) {
76                    // We collapse to the inner cancellation error so callers do not see a cancel
77                    // caused by another cancel.
78                    let (_, cause) = activity.into_parts();
79                    let Some(IncomingError::Cancelled(cancelled)) = cause else {
80                        unreachable!("checked above");
81                    };
82                    ActivityExecutionError::Cancelled(cancelled)
83                } else {
84                    ActivityExecutionError::Failed(activity)
85                }
86            }
87            other => match other {
88                IncomingError::Cancelled(cancelled) if self.cancelled => {
89                    ActivityExecutionError::Cancelled(cancelled)
90                }
91                other => {
92                    let activity = ActivityFailureError::new(
93                        other.into_failure(),
94                        ActivityFailureInfo::default(),
95                        None,
96                    );
97                    ActivityExecutionError::Failed(activity)
98                }
99            },
100        }
101    }
102}
103
104/// Decode hint for child-workflow start results.
105#[derive(Debug, Clone, Copy)]
106pub struct ChildWorkflowStartDecodeHint;
107
108impl FailureDecodeHint for ChildWorkflowStartDecodeHint {
109    type Output = ChildWorkflowStartError;
110
111    fn adapt(self, normalized: IncomingError) -> Self::Output {
112        match normalized {
113            IncomingError::Cancelled(cancelled) => {
114                ChildWorkflowStartError::Cancelled(Box::new(cancelled))
115            }
116            other => {
117                let payload_converter = PayloadConverter::default();
118                ChildWorkflowStartError::Cancelled(Box::new(CancelledError::new(
119                    other.into_failure(),
120                    CanceledFailureInfo::default(),
121                    None,
122                    &payload_converter,
123                    &SerializationContextData::None,
124                )))
125            }
126        }
127    }
128}
129
130/// Decode hint for child-workflow execution results.
131#[derive(Debug, Clone, Copy)]
132pub struct ChildWorkflowExecutionDecodeHint;
133
134impl FailureDecodeHint for ChildWorkflowExecutionDecodeHint {
135    type Output = ChildWorkflowExecutionError;
136
137    fn adapt(self, normalized: IncomingError) -> Self::Output {
138        match normalized {
139            IncomingError::ChildWorkflowExecution(child) => {
140                ChildWorkflowExecutionError::Failed(Box::new(child))
141            }
142            other => ChildWorkflowExecutionError::Failed(Box::new(ChildWorkflowFailureError::new(
143                other.into_failure(),
144                ChildWorkflowExecutionFailureInfo::default(),
145                None,
146            ))),
147        }
148    }
149}
150
151/// Decode hint for workflow signal failures.
152#[derive(Debug, Clone, Copy)]
153pub struct WorkflowSignalDecodeHint;
154
155impl FailureDecodeHint for WorkflowSignalDecodeHint {
156    type Output = WorkflowSignalError;
157
158    fn adapt(self, normalized: IncomingError) -> Self::Output {
159        let failure = normalized.failure().clone();
160        WorkflowSignalError::Failed(Box::new(WorkflowSignalFailureError::new(
161            failure, normalized,
162        )))
163    }
164}
165
166impl FailureConverter for DefaultFailureConverter {
167    fn to_failure(
168        &self,
169        error: OutgoingError,
170        payload_converter: &PayloadConverter,
171        context: &SerializationContextData,
172    ) -> Failure {
173        let original_error = error.to_string();
174        let encoded = match error {
175            OutgoingError::Activity(activity) => {
176                encode_outgoing_activity_error(activity, payload_converter, context)
177            }
178            OutgoingError::Workflow(OutgoingWorkflowError::Application(app)) => {
179                app.encode_failure(payload_converter, context)
180            }
181            OutgoingError::Workflow(OutgoingWorkflowError::ActivityExecution(activity)) => {
182                activity.encode_failure(payload_converter, context)
183            }
184            OutgoingError::Workflow(OutgoingWorkflowError::ChildWorkflowExecution(child)) => {
185                child.encode_failure(payload_converter, context)
186            }
187            OutgoingError::Workflow(OutgoingWorkflowError::ChildWorkflowStart(child)) => {
188                child.encode_failure(payload_converter, context)
189            }
190            OutgoingError::Workflow(OutgoingWorkflowError::WorkflowSignal(signal)) => {
191                signal.encode_failure(payload_converter, context)
192            }
193        };
194        encoded.unwrap_or_else(|converter_error| {
195            Failure::application_failure(
196                failed_error_conversion_message(&original_error, &converter_error),
197                false,
198            )
199        })
200    }
201
202    fn to_error(
203        &self,
204        failure: Failure,
205        payload_converter: &PayloadConverter,
206        context: &SerializationContextData,
207    ) -> Result<IncomingError, PayloadConversionError> {
208        Ok(decode_failure(failure, payload_converter, context))
209    }
210}
211
212/// Trait for expressing that a type has a known conversion to a Failure proto
213trait EncodeFailure {
214    fn encode_failure(
215        &self,
216        payload_converter: &PayloadConverter,
217        context: &SerializationContextData,
218    ) -> Result<Failure, PayloadConversionError>;
219}
220
221enum ClassifiedFailure<'a> {
222    Application(&'a ApplicationFailure),
223    ActivityExecution(&'a ActivityExecutionError),
224    ChildWorkflowExecution(&'a ChildWorkflowExecutionError),
225    ChildWorkflowStart(&'a ChildWorkflowStartError),
226    WorkflowSignal(&'a WorkflowSignalError),
227    Generic(&'a (dyn std::error::Error + 'static)),
228}
229
230fn failed_error_conversion_message(
231    original_error: impl std::fmt::Display,
232    converter_error: &PayloadConversionError,
233) -> String {
234    format!(
235        "Failed converting error to failure: {converter_error}, original error message: \
236         {original_error}"
237    )
238}
239
240impl<'a> ClassifiedFailure<'a> {
241    fn from_error(err: &'a (dyn std::error::Error + 'static)) -> Self {
242        if let Some(app) = err.downcast_ref::<ApplicationFailure>() {
243            Self::Application(app)
244        } else if let Some(activity) = err.downcast_ref::<ActivityExecutionError>() {
245            Self::ActivityExecution(activity)
246        } else if let Some(child) = err.downcast_ref::<ChildWorkflowExecutionError>() {
247            Self::ChildWorkflowExecution(child)
248        } else if let Some(child) = err.downcast_ref::<ChildWorkflowStartError>() {
249            Self::ChildWorkflowStart(child)
250        } else if let Some(child_signal) = err.downcast_ref::<WorkflowSignalError>() {
251            Self::WorkflowSignal(child_signal)
252        } else {
253            Self::Generic(err)
254        }
255    }
256
257    fn encode(self) -> Failure {
258        match self {
259            Self::Application(app) => app
260                .encode_failure(
261                    &PayloadConverter::default(),
262                    &SerializationContextData::None,
263                )
264                .unwrap_or_else(|converter_error| {
265                    encode_failed_error_conversion(app, converter_error)
266                }),
267            Self::ActivityExecution(activity) => activity
268                .encode_failure(
269                    &PayloadConverter::default(),
270                    &SerializationContextData::None,
271                )
272                .unwrap_or_else(|converter_error| {
273                    encode_failed_error_conversion(activity, converter_error)
274                }),
275            Self::ChildWorkflowExecution(child) => child
276                .encode_failure(
277                    &PayloadConverter::default(),
278                    &SerializationContextData::None,
279                )
280                .unwrap_or_else(|converter_error| {
281                    encode_failed_error_conversion(child, converter_error)
282                }),
283            Self::ChildWorkflowStart(child) => child
284                .encode_failure(
285                    &PayloadConverter::default(),
286                    &SerializationContextData::None,
287                )
288                .unwrap_or_else(|converter_error| {
289                    encode_failed_error_conversion(child, converter_error)
290                }),
291            Self::WorkflowSignal(signal) => signal
292                .encode_failure(
293                    &PayloadConverter::default(),
294                    &SerializationContextData::None,
295                )
296                .unwrap_or_else(|converter_error| {
297                    encode_failed_error_conversion(signal, converter_error)
298                }),
299            Self::Generic(err) => encode_generic_application_failure(err),
300        }
301    }
302}
303
304impl EncodeFailure for ApplicationFailure {
305    fn encode_failure(
306        &self,
307        payload_converter: &PayloadConverter,
308        context: &SerializationContextData,
309    ) -> Result<Failure, PayloadConversionError> {
310        let details = self
311            .failure_payloads()
312            .map(|details| details.encode(payload_converter, context))
313            .transpose()?;
314        Ok(Failure {
315            message: self.to_string(),
316            cause: self
317                .cause()
318                .map(|cause| Box::new(cause.failure().clone()))
319                .or_else(|| encode_application_failure_cause(self.source_error())),
320            failure_info: Some(FailureInfo::ApplicationFailureInfo(
321                ApplicationFailureInfo {
322                    r#type: self.type_name().unwrap_or_default().to_owned(),
323                    non_retryable: self.is_non_retryable(),
324                    details,
325                    next_retry_delay: self.next_retry_delay().and_then(|d| d.try_into().ok()),
326                    category: ProtoApplicationErrorCategory::from(self.category()) as i32,
327                },
328            )),
329            ..Default::default()
330        })
331    }
332}
333
334fn encode_application_failure_cause(
335    source: &(dyn std::error::Error + 'static),
336) -> Option<Box<Failure>> {
337    if matches!(
338        ClassifiedFailure::from_error(source),
339        ClassifiedFailure::Application(_) | ClassifiedFailure::Generic(_)
340    ) {
341        source.source().map(encode_error_as_failure).map(Box::new)
342    } else {
343        Some(Box::new(encode_error_as_failure(source)))
344    }
345}
346
347fn encode_error_as_failure(err: &(dyn std::error::Error + 'static)) -> Failure {
348    ClassifiedFailure::from_error(err).encode()
349}
350
351impl EncodeFailure for ActivityExecutionError {
352    fn encode_failure(
353        &self,
354        _: &PayloadConverter,
355        _: &SerializationContextData,
356    ) -> Result<Failure, PayloadConversionError> {
357        Ok(match self {
358            Self::Failed(failure) => failure.failure().clone(),
359            Self::Cancelled(failure) => failure.failure().clone(),
360            Self::Serialization(err) => encode_generic_application_failure(err),
361        })
362    }
363}
364
365impl EncodeFailure for ChildWorkflowExecutionError {
366    fn encode_failure(
367        &self,
368        _: &PayloadConverter,
369        _: &SerializationContextData,
370    ) -> Result<Failure, PayloadConversionError> {
371        Ok(match self {
372            Self::Failed(failure) => failure.failure().clone(),
373            Self::Serialization(_) => encode_generic_application_failure(self),
374        })
375    }
376}
377
378impl EncodeFailure for ChildWorkflowStartError {
379    fn encode_failure(
380        &self,
381        _: &PayloadConverter,
382        _: &SerializationContextData,
383    ) -> Result<Failure, PayloadConversionError> {
384        Ok(match self {
385            Self::Cancelled(failure) => failure.failure().clone(),
386            Self::StartFailed { .. } | Self::Serialization(_) => {
387                encode_generic_application_failure(self)
388            }
389        })
390    }
391}
392
393impl EncodeFailure for WorkflowSignalError {
394    fn encode_failure(
395        &self,
396        _: &PayloadConverter,
397        _: &SerializationContextData,
398    ) -> Result<Failure, PayloadConversionError> {
399        Ok(match self {
400            Self::Failed(failure) => failure.failure().clone(),
401            Self::Serialization(err) => encode_generic_application_failure(err),
402        })
403    }
404}
405
406fn encode_outgoing_activity_error(
407    err: OutgoingActivityError,
408    payload_converter: &PayloadConverter,
409    context: &SerializationContextData,
410) -> Result<Failure, PayloadConversionError> {
411    Ok(match err {
412        OutgoingActivityError::Application(app) => {
413            app.encode_failure(payload_converter, context)?
414        }
415        OutgoingActivityError::Cancelled { details } => Failure {
416            message: "Activity cancelled".to_string(),
417            failure_info: Some(FailureInfo::CanceledFailureInfo(CanceledFailureInfo {
418                details: details
419                    .map(|details| details.encode(payload_converter, context))
420                    .transpose()?,
421                identity: Default::default(),
422            })),
423            ..Default::default()
424        },
425    })
426}
427
428fn encode_generic_application_failure(err: &(dyn std::error::Error + 'static)) -> Failure {
429    Failure {
430        message: err.to_string(),
431        cause: err.source().map(encode_error_as_failure).map(Box::new),
432        failure_info: Some(FailureInfo::ApplicationFailureInfo(
433            ApplicationFailureInfo::default(),
434        )),
435        ..Default::default()
436    }
437}
438
439fn encode_failed_error_conversion(
440    err: &(dyn std::error::Error + 'static),
441    converter_error: PayloadConversionError,
442) -> Failure {
443    Failure {
444        message: failed_error_conversion_message(err, &converter_error),
445        cause: err.source().map(encode_error_as_failure).map(Box::new),
446        failure_info: Some(FailureInfo::ApplicationFailureInfo(
447            ApplicationFailureInfo::default(),
448        )),
449        ..Default::default()
450    }
451}
452
453fn decode_failure(
454    failure: Failure,
455    payload_converter: &PayloadConverter,
456    context: &SerializationContextData,
457) -> IncomingError {
458    let cause = failure
459        .cause
460        .clone()
461        .map(|cause| decode_failure(*cause, payload_converter, context));
462    match failure.failure_info.clone() {
463        Some(FailureInfo::ApplicationFailureInfo(_)) | None => IncomingError::Application(
464            ApplicationFailure::from_failure(failure, cause, payload_converter, context),
465        ),
466        Some(FailureInfo::TimeoutFailureInfo(failure_info)) => IncomingError::Timeout(
467            TimeoutError::new(failure, failure_info, cause, payload_converter, context),
468        ),
469        Some(FailureInfo::CanceledFailureInfo(failure_info)) => IncomingError::Cancelled(
470            CancelledError::new(failure, failure_info, cause, payload_converter, context),
471        ),
472        Some(FailureInfo::TerminatedFailureInfo(_)) => {
473            IncomingError::Terminated(TerminatedError::new(failure, cause))
474        }
475        Some(FailureInfo::ServerFailureInfo(_)) => {
476            IncomingError::Server(ServerError::new(failure, cause))
477        }
478        Some(FailureInfo::ResetWorkflowFailureInfo(_)) => {
479            IncomingError::ResetWorkflow(ResetWorkflowError::new(failure, cause))
480        }
481        Some(FailureInfo::ActivityFailureInfo(failure_info)) => {
482            IncomingError::Activity(ActivityFailureError::new(failure, failure_info, cause))
483        }
484        Some(FailureInfo::ChildWorkflowExecutionFailureInfo(failure_info)) => {
485            IncomingError::ChildWorkflowExecution(ChildWorkflowFailureError::new(
486                failure,
487                failure_info,
488                cause,
489            ))
490        }
491        Some(FailureInfo::NexusOperationExecutionFailureInfo(_)) => {
492            IncomingError::NexusOperationExecution(IncomingNexusOperationExecutionError::new(
493                failure, cause,
494            ))
495        }
496        Some(FailureInfo::NexusHandlerFailureInfo(_)) => {
497            IncomingError::NexusHandler(IncomingNexusHandlerError::new(failure, cause))
498        }
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::{
506        data_converters::{GenericPayloadConverter, SerializationContext},
507        error::ApplicationErrorCategory,
508        protos::temporal::api::{
509            common::v1::{Payload, Payloads},
510            failure::v1::{
511                ActivityFailureInfo, ChildWorkflowExecutionFailureInfo, NexusHandlerFailureInfo,
512                NexusOperationFailureInfo, ResetWorkflowFailureInfo, ServerFailureInfo,
513                TerminatedFailureInfo, TimeoutFailureInfo, failure::FailureInfo,
514            },
515        },
516    };
517    use rstest::rstest;
518    use std::fmt;
519
520    #[derive(Debug, Clone, Copy)]
521    enum IncomingKind {
522        Application,
523        Timeout,
524        Cancelled,
525        Terminated,
526        Server,
527        ResetWorkflow,
528        Activity,
529        ChildWorkflowExecution,
530        NexusOperationExecution,
531        NexusHandler,
532    }
533
534    #[derive(Debug, Clone, Copy)]
535    enum ActivityExecutionKind {
536        Failed,
537        Cancelled,
538    }
539
540    #[derive(Debug)]
541    struct TestError {
542        message: &'static str,
543        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
544    }
545
546    impl TestError {
547        fn new(
548            message: &'static str,
549            source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
550        ) -> Self {
551            Self { message, source }
552        }
553    }
554
555    impl fmt::Display for TestError {
556        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557            write!(f, "{}", self.message)
558        }
559    }
560
561    impl std::error::Error for TestError {
562        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
563            self.source
564                .as_deref()
565                .map(|source| source as &(dyn std::error::Error + 'static))
566        }
567    }
568
569    struct AlwaysFailsSerialize;
570
571    impl serde::Serialize for AlwaysFailsSerialize {
572        fn serialize<S: serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
573            Err(serde::ser::Error::custom("serialize boom"))
574        }
575    }
576
577    fn assert_incoming_kind(decoded: &IncomingError, expected: IncomingKind) {
578        match expected {
579            IncomingKind::Application => assert!(matches!(decoded, IncomingError::Application(_))),
580            IncomingKind::Timeout => assert!(matches!(decoded, IncomingError::Timeout(_))),
581            IncomingKind::Cancelled => assert!(matches!(decoded, IncomingError::Cancelled(_))),
582            IncomingKind::Terminated => assert!(matches!(decoded, IncomingError::Terminated(_))),
583            IncomingKind::Server => assert!(matches!(decoded, IncomingError::Server(_))),
584            IncomingKind::ResetWorkflow => {
585                assert!(matches!(decoded, IncomingError::ResetWorkflow(_)))
586            }
587            IncomingKind::Activity => assert!(matches!(decoded, IncomingError::Activity(_))),
588            IncomingKind::ChildWorkflowExecution => {
589                assert!(matches!(decoded, IncomingError::ChildWorkflowExecution(_)))
590            }
591            IncomingKind::NexusOperationExecution => {
592                assert!(matches!(decoded, IncomingError::NexusOperationExecution(_)))
593            }
594            IncomingKind::NexusHandler => {
595                assert!(matches!(decoded, IncomingError::NexusHandler(_)))
596            }
597        }
598    }
599
600    fn convert(err: OutgoingWorkflowError) -> Failure {
601        DefaultFailureConverter.to_failure(
602            OutgoingError::Workflow(err),
603            &PayloadConverter::default(),
604            &SerializationContextData::Workflow,
605        )
606    }
607
608    fn data_converter() -> crate::data_converters::DataConverter {
609        crate::data_converters::DataConverter::new(
610            PayloadConverter::default(),
611            DefaultFailureConverter,
612            crate::data_converters::DefaultPayloadCodec,
613        )
614    }
615
616    fn cancelled_failure(message: &str) -> Failure {
617        Failure {
618            message: message.to_owned(),
619            failure_info: Some(FailureInfo::CanceledFailureInfo(
620                CanceledFailureInfo::default(),
621            )),
622            ..Default::default()
623        }
624    }
625
626    fn timeout_failure(message: &str) -> Failure {
627        Failure {
628            message: message.to_owned(),
629            failure_info: Some(FailureInfo::TimeoutFailureInfo(
630                TimeoutFailureInfo::default(),
631            )),
632            ..Default::default()
633        }
634    }
635
636    #[test]
637    fn application_failures_preserve_metadata() {
638        let failure = convert(OutgoingWorkflowError::Application(Box::new(
639            ApplicationFailure::builder(anyhow::anyhow!("app boom"))
640                .type_name("MyType".to_owned())
641                .non_retryable(true)
642                .category(ApplicationErrorCategory::Benign)
643                .details(crate::data_converters::RawValue::new(vec![Payload {
644                    data: b"details".to_vec(),
645                    ..Default::default()
646                }]))
647                .build(),
648        )));
649        let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
650            panic!("expected application failure info");
651        };
652        assert_eq!(failure.message, "app boom");
653        assert_eq!(info.r#type, "MyType");
654        assert!(info.non_retryable);
655        assert_eq!(info.category(), ProtoApplicationErrorCategory::Benign);
656        assert_eq!(info.details.unwrap().payloads[0].data, b"details".to_vec());
657    }
658
659    #[test]
660    fn application_failures_encode_serializable_details_with_payload_converter() {
661        let failure = convert(OutgoingWorkflowError::Application(Box::new(
662            ApplicationFailure::builder(anyhow::anyhow!("app boom"))
663                .details("detail")
664                .build(),
665        )));
666        let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
667            panic!("expected application failure info");
668        };
669        let payloads = info.details.expect("details should be present").payloads;
670        let converter = PayloadConverter::default();
671        let details: String = converter
672            .from_payloads(
673                &SerializationContext {
674                    data: &SerializationContextData::Workflow,
675                    converter: &converter,
676                },
677                payloads,
678            )
679            .unwrap();
680        assert_eq!(details, "detail");
681    }
682
683    #[test]
684    fn application_failures_surface_detail_encoding_errors_with_original_message() {
685        let failure = DefaultFailureConverter.to_failure(
686            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
687                ApplicationFailure::builder(anyhow::anyhow!("app boom"))
688                    .details(AlwaysFailsSerialize)
689                    .build(),
690            ))),
691            &PayloadConverter::default(),
692            &SerializationContextData::Workflow,
693        );
694
695        assert_eq!(
696            failure.message,
697            "Failed converting error to failure: Encoding error: serialize boom, original error message: app boom"
698        );
699    }
700
701    #[test]
702    fn application_failures_decode_details_through_payload_converter() {
703        let converter = PayloadConverter::default();
704        let payloads = converter
705            .to_payloads(
706                &SerializationContext {
707                    data: &SerializationContextData::Workflow,
708                    converter: &converter,
709                },
710                &"detail",
711            )
712            .unwrap();
713        let failure = Failure {
714            message: "app boom".to_owned(),
715            failure_info: Some(FailureInfo::ApplicationFailureInfo(
716                ApplicationFailureInfo {
717                    details: Some(Payloads { payloads }),
718                    ..Default::default()
719                },
720            )),
721            ..Default::default()
722        };
723
724        let decoded = DefaultFailureConverter
725            .to_error(failure, &converter, &SerializationContextData::Workflow)
726            .unwrap();
727
728        let IncomingError::Application(app) = decoded else {
729            panic!("expected application error");
730        };
731        assert_eq!(app.details::<String>().unwrap(), Some("detail".to_string()));
732    }
733
734    #[test]
735    fn nested_application_failures_surface_detail_encoding_errors_in_fallback_failure() {
736        let app = ApplicationFailure::new(anyhow::Error::new(TestError::new(
737            "outer wrapper",
738            Some(Box::new(
739                ApplicationFailure::builder(anyhow::anyhow!("inner boom"))
740                    .details(AlwaysFailsSerialize)
741                    .build(),
742            )),
743        )));
744
745        let converted = convert(OutgoingWorkflowError::Application(Box::new(app)));
746        let cause = converted.cause.expect("expected nested fallback failure");
747        assert_eq!(
748            cause.message,
749            "Failed converting error to failure: Encoding error: serialize boom, original error message: inner boom"
750        );
751        assert!(matches!(
752            cause.failure_info,
753            Some(FailureInfo::ApplicationFailureInfo(_))
754        ));
755    }
756
757    #[test]
758    fn application_failures_do_not_duplicate_their_source_as_cause() {
759        let failure = convert(OutgoingWorkflowError::Application(Box::new(
760            ApplicationFailure::new(anyhow::anyhow!("app boom")),
761        )));
762
763        assert_eq!(failure.message, "app boom");
764        assert!(failure.cause.is_none());
765    }
766
767    #[test]
768    fn application_failures_keep_special_causes_nested() {
769        let activity_failure = Failure {
770            message: "activity failed".to_owned(),
771            failure_info: Some(FailureInfo::ActivityFailureInfo(
772                ActivityFailureInfo::default(),
773            )),
774            ..Default::default()
775        };
776        let app =
777            ApplicationFailure::new(ActivityExecutionError::Failed(ActivityFailureError::new(
778                activity_failure.clone(),
779                ActivityFailureInfo::default(),
780                None,
781            )));
782        let converted = convert(OutgoingWorkflowError::Application(Box::new(app)));
783        assert!(matches!(
784            converted.failure_info,
785            Some(FailureInfo::ApplicationFailureInfo(_))
786        ));
787        assert_eq!(converted.cause.unwrap().as_ref(), &activity_failure);
788    }
789
790    #[test]
791    fn application_failures_fall_back_to_source_error() {
792        let activity_failure = Failure {
793            message: "activity failed".to_owned(),
794            failure_info: Some(FailureInfo::ActivityFailureInfo(
795                ActivityFailureInfo::default(),
796            )),
797            ..Default::default()
798        };
799        let app =
800            ApplicationFailure::new(ActivityExecutionError::Failed(ActivityFailureError::new(
801                activity_failure.clone(),
802                ActivityFailureInfo::default(),
803                None,
804            )));
805
806        assert!(app.cause().is_none());
807
808        let converted = convert(OutgoingWorkflowError::Application(Box::new(app)));
809
810        assert_eq!(converted.cause.unwrap().as_ref(), &activity_failure);
811    }
812
813    #[test]
814    fn application_failures_skip_generic_wrappers_around_known_causes() {
815        let activity_failure = Failure {
816            message: "activity failed".to_owned(),
817            failure_info: Some(FailureInfo::ActivityFailureInfo(
818                ActivityFailureInfo::default(),
819            )),
820            ..Default::default()
821        };
822        let app = ApplicationFailure::new(anyhow::Error::new(TestError::new(
823            "outer wrapper",
824            Some(Box::new(ActivityExecutionError::Failed(
825                ActivityFailureError::new(
826                    activity_failure.clone(),
827                    ActivityFailureInfo::default(),
828                    None,
829                ),
830            ))),
831        )));
832
833        let converted = convert(OutgoingWorkflowError::Application(Box::new(app)));
834
835        assert!(matches!(
836            converted.failure_info,
837            Some(FailureInfo::ApplicationFailureInfo(_))
838        ));
839        assert_eq!(converted.message, "outer wrapper");
840        assert_eq!(converted.cause.unwrap().as_ref(), &activity_failure);
841    }
842
843    #[test]
844    fn application_failures_serialize_unknown_nested_causes_as_application_failures() {
845        let app = ApplicationFailure::new(anyhow::Error::new(TestError::new(
846            "outer wrapper",
847            Some(Box::new(TestError::new("generic inner cause", None))),
848        )));
849
850        let converted = convert(OutgoingWorkflowError::Application(Box::new(app)));
851
852        assert!(matches!(
853            converted.failure_info,
854            Some(FailureInfo::ApplicationFailureInfo(_))
855        ));
856        assert_eq!(converted.message, "outer wrapper",);
857        let cause = converted
858            .cause
859            .clone()
860            .expect("expected nested generic cause");
861        assert_eq!(cause.message, "generic inner cause");
862        assert!(matches!(
863            cause.failure_info,
864            Some(FailureInfo::ApplicationFailureInfo(_))
865        ));
866        assert!(cause.cause.is_none());
867
868        let decoded = DefaultFailureConverter
869            .to_error(
870                converted.clone(),
871                &PayloadConverter::default(),
872                &SerializationContextData::Workflow,
873            )
874            .unwrap();
875
876        let IncomingError::Application(decoded_app) = decoded else {
877            panic!("expected application error");
878        };
879        assert_eq!(decoded_app.failure(), Some(&converted));
880        let Some(IncomingError::Application(wrapper)) = decoded_app.cause() else {
881            panic!("expected application cause");
882        };
883        assert_eq!(
884            wrapper.failure().map(|failure| failure.message.as_str()),
885            Some("generic inner cause")
886        );
887        assert!(wrapper.cause().is_none());
888    }
889
890    #[test]
891    fn start_failed_child_workflow_errors_fall_back_to_application_failures() {
892        let failure = convert(OutgoingWorkflowError::ChildWorkflowStart(Box::new(
893            ChildWorkflowStartError::StartFailed {
894                workflow_id: "wf-id".to_owned(),
895                workflow_type: "wf-type".to_owned(),
896                cause: crate::protos::coresdk::child_workflow::StartChildWorkflowExecutionFailedCause::WorkflowAlreadyExists,
897            },
898        )));
899        assert!(matches!(
900            failure.failure_info,
901            Some(FailureInfo::ApplicationFailureInfo(_))
902        ));
903        assert!(failure.message.contains("Child workflow start failed"));
904    }
905
906    #[test]
907    fn application_failures_decode_with_metadata_and_proto() {
908        let failure = Failure {
909            message: "app boom".to_owned(),
910            failure_info: Some(FailureInfo::ApplicationFailureInfo(
911                ApplicationFailureInfo {
912                    r#type: "MyType".to_owned(),
913                    non_retryable: true,
914                    ..Default::default()
915                },
916            )),
917            ..Default::default()
918        };
919
920        let decoded = DefaultFailureConverter
921            .to_error(
922                failure.clone(),
923                &PayloadConverter::default(),
924                &SerializationContextData::Workflow,
925            )
926            .unwrap();
927
928        let IncomingError::Application(app) = decoded else {
929            panic!("expected application error");
930        };
931        assert_eq!(app.type_name(), Some("MyType"));
932        assert!(app.is_non_retryable());
933        assert_eq!(app.failure(), Some(&failure));
934    }
935
936    #[test]
937    fn application_failures_decode_with_normalized_cause() {
938        let failure = Failure {
939            message: "app boom".to_owned(),
940            cause: Some(Box::new(Failure {
941                message: "timed out".to_owned(),
942                failure_info: Some(FailureInfo::TimeoutFailureInfo(
943                    TimeoutFailureInfo::default(),
944                )),
945                ..Default::default()
946            })),
947            failure_info: Some(FailureInfo::ApplicationFailureInfo(
948                ApplicationFailureInfo::default(),
949            )),
950            ..Default::default()
951        };
952
953        let decoded = DefaultFailureConverter
954            .to_error(
955                failure.clone(),
956                &PayloadConverter::default(),
957                &SerializationContextData::Workflow,
958            )
959            .unwrap();
960
961        let IncomingError::Application(app) = decoded else {
962            panic!("expected application error");
963        };
964        assert_eq!(app.failure(), Some(&failure));
965        assert!(matches!(app.cause(), Some(IncomingError::Timeout(_))));
966    }
967
968    #[test]
969    fn decoded_application_failures_preserve_cause() {
970        let failure = Failure {
971            message: "app boom".to_owned(),
972            cause: Some(Box::new(timeout_failure("timed out"))),
973            failure_info: Some(FailureInfo::ApplicationFailureInfo(
974                ApplicationFailureInfo::default(),
975            )),
976            ..Default::default()
977        };
978
979        let decoded = DefaultFailureConverter
980            .to_error(
981                failure.clone(),
982                &PayloadConverter::default(),
983                &SerializationContextData::Workflow,
984            )
985            .unwrap();
986
987        let IncomingError::Application(app) = decoded else {
988            panic!("expected application error");
989        };
990        assert!(app.as_timeout().is_some());
991
992        let reencoded = convert(OutgoingWorkflowError::Application(Box::new(app)));
993
994        assert_eq!(reencoded.message, failure.message);
995        assert_eq!(reencoded.cause.as_deref(), failure.cause.as_deref());
996
997        let decoded_reencoded = DefaultFailureConverter
998            .to_error(
999                reencoded,
1000                &PayloadConverter::default(),
1001                &SerializationContextData::Workflow,
1002            )
1003            .unwrap();
1004        let IncomingError::Application(roundtripped) = decoded_reencoded else {
1005            panic!("expected application error");
1006        };
1007        assert!(roundtripped.as_timeout().is_some());
1008    }
1009
1010    #[test]
1011    fn application_failures_decode_wrapped_known_causes_without_collapsing_wrapper() {
1012        let failure = Failure {
1013            message: "app boom".to_owned(),
1014            cause: Some(Box::new(Failure {
1015                message: "activity failed".to_owned(),
1016                failure_info: Some(FailureInfo::ActivityFailureInfo(
1017                    ActivityFailureInfo::default(),
1018                )),
1019                ..Default::default()
1020            })),
1021            failure_info: Some(FailureInfo::ApplicationFailureInfo(
1022                ApplicationFailureInfo::default(),
1023            )),
1024            ..Default::default()
1025        };
1026
1027        let decoded = DefaultFailureConverter
1028            .to_error(
1029                failure.clone(),
1030                &PayloadConverter::default(),
1031                &SerializationContextData::Workflow,
1032            )
1033            .unwrap();
1034
1035        let IncomingError::Application(app) = decoded else {
1036            panic!("expected application error");
1037        };
1038        assert_eq!(app.failure(), Some(&failure));
1039        let Some(IncomingError::Activity(activity)) = app.cause() else {
1040            panic!("expected activity cause");
1041        };
1042        assert_eq!(activity.failure().message, "activity failed");
1043    }
1044
1045    #[rstest]
1046    #[case(
1047        FailureInfo::ApplicationFailureInfo(ApplicationFailureInfo::default()),
1048        IncomingKind::Application
1049    )]
1050    #[case(
1051        FailureInfo::TimeoutFailureInfo(TimeoutFailureInfo::default()),
1052        IncomingKind::Timeout
1053    )]
1054    #[case(
1055        FailureInfo::CanceledFailureInfo(CanceledFailureInfo::default()),
1056        IncomingKind::Cancelled
1057    )]
1058    #[case(
1059        FailureInfo::TerminatedFailureInfo(TerminatedFailureInfo::default()),
1060        IncomingKind::Terminated
1061    )]
1062    #[case(
1063        FailureInfo::ServerFailureInfo(ServerFailureInfo::default()),
1064        IncomingKind::Server
1065    )]
1066    #[case(
1067        FailureInfo::ResetWorkflowFailureInfo(ResetWorkflowFailureInfo::default()),
1068        IncomingKind::ResetWorkflow
1069    )]
1070    #[case(
1071        FailureInfo::ActivityFailureInfo(ActivityFailureInfo::default()),
1072        IncomingKind::Activity
1073    )]
1074    #[case(
1075        FailureInfo::ChildWorkflowExecutionFailureInfo(
1076            ChildWorkflowExecutionFailureInfo::default()
1077        ),
1078        IncomingKind::ChildWorkflowExecution
1079    )]
1080    #[case(
1081        FailureInfo::NexusOperationExecutionFailureInfo(NexusOperationFailureInfo::default()),
1082        IncomingKind::NexusOperationExecution
1083    )]
1084    #[case(
1085        FailureInfo::NexusHandlerFailureInfo(NexusHandlerFailureInfo::default()),
1086        IncomingKind::NexusHandler
1087    )]
1088    fn failure_info_decodes_to_expected_incoming_error(
1089        #[case] failure_info: FailureInfo,
1090        #[case] expected: IncomingKind,
1091    ) {
1092        let failure = Failure {
1093            message: "boom".to_owned(),
1094            failure_info: Some(failure_info),
1095            ..Default::default()
1096        };
1097
1098        let decoded = DefaultFailureConverter
1099            .to_error(
1100                failure.clone(),
1101                &PayloadConverter::default(),
1102                &SerializationContextData::Workflow,
1103            )
1104            .unwrap();
1105
1106        assert_incoming_kind(&decoded, expected);
1107        assert_eq!(decoded.failure(), &failure);
1108    }
1109
1110    #[test]
1111    fn activity_decode_hint_preserves_timeout_reason() {
1112        let failure = Failure {
1113            message: "activity failed".to_owned(),
1114            cause: Some(Box::new(Failure {
1115                message: "timed out".to_owned(),
1116                failure_info: Some(FailureInfo::TimeoutFailureInfo(
1117                    TimeoutFailureInfo::default(),
1118                )),
1119                ..Default::default()
1120            })),
1121            failure_info: Some(FailureInfo::ActivityFailureInfo(ActivityFailureInfo {
1122                activity_id: "act-1".to_owned(),
1123                activity_type: Some(crate::protos::temporal::api::common::v1::ActivityType {
1124                    name: "test-activity".to_owned(),
1125                }),
1126                scheduled_event_id: 5,
1127                started_event_id: 6,
1128                identity: "worker-1".to_owned(),
1129                retry_state: crate::protos::temporal::api::enums::v1::RetryState::Timeout.into(),
1130            })),
1131            ..Default::default()
1132        };
1133        let data_converter = crate::data_converters::DataConverter::new(
1134            PayloadConverter::default(),
1135            DefaultFailureConverter,
1136            crate::data_converters::DefaultPayloadCodec,
1137        );
1138
1139        let decoded = data_converter
1140            .to_error(
1141                &SerializationContextData::Workflow,
1142                failure.clone(),
1143                ActivityExecutionDecodeHint { cancelled: false },
1144            )
1145            .unwrap();
1146
1147        let ActivityExecutionError::Failed(decoded_failure) = decoded else {
1148            panic!("expected failed activity execution error");
1149        };
1150        assert_eq!(decoded_failure.failure(), &failure);
1151        assert_eq!(decoded_failure.activity_id(), "act-1");
1152        assert_eq!(
1153            decoded_failure.activity_type().map(|ty| ty.name.as_str()),
1154            Some("test-activity")
1155        );
1156        assert_eq!(decoded_failure.scheduled_event_id(), 5);
1157        assert_eq!(decoded_failure.started_event_id(), 6);
1158        assert_eq!(decoded_failure.identity(), "worker-1");
1159        assert_eq!(
1160            decoded_failure.retry_state(),
1161            crate::protos::temporal::api::enums::v1::RetryState::Timeout
1162        );
1163        assert!(matches!(
1164            decoded_failure.cause(),
1165            Some(IncomingError::Timeout(_))
1166        ));
1167    }
1168
1169    #[rstest]
1170    #[case(
1171        cancelled_failure("activity cancelled"),
1172        ActivityExecutionKind::Cancelled,
1173        None
1174    )]
1175    #[case(timeout_failure("timed out"), ActivityExecutionKind::Failed, None)]
1176    #[case(
1177        Failure {
1178            message: "activity task cancelled".to_owned(),
1179            cause: Some(Box::new(cancelled_failure("activity cancelled"))),
1180            failure_info: Some(FailureInfo::ActivityFailureInfo(
1181                ActivityFailureInfo::default(),
1182            )),
1183            ..Default::default()
1184        },
1185        ActivityExecutionKind::Cancelled,
1186        Some(cancelled_failure("activity cancelled"))
1187    )]
1188    fn activity_cancelled_decode_hint_adapts_expected_shape(
1189        #[case] failure: Failure,
1190        #[case] expected_kind: ActivityExecutionKind,
1191        #[case] expected_failure: Option<Failure>,
1192    ) {
1193        let decoded = data_converter()
1194            .to_error(
1195                &SerializationContextData::Workflow,
1196                failure.clone(),
1197                ActivityExecutionDecodeHint { cancelled: true },
1198            )
1199            .unwrap();
1200
1201        match expected_kind {
1202            ActivityExecutionKind::Failed => {
1203                let ActivityExecutionError::Failed(decoded_failure) = decoded else {
1204                    panic!("expected failed activity execution error");
1205                };
1206                assert_eq!(decoded_failure.failure(), &failure);
1207                assert!(decoded_failure.cause().is_none());
1208            }
1209            ActivityExecutionKind::Cancelled => {
1210                let ActivityExecutionError::Cancelled(decoded_failure) = decoded else {
1211                    panic!("expected cancelled activity execution error");
1212                };
1213                assert_eq!(
1214                    decoded_failure.failure(),
1215                    expected_failure.as_ref().unwrap_or(&failure)
1216                );
1217                assert!(decoded_failure.cause().is_none());
1218            }
1219        }
1220    }
1221
1222    #[test]
1223    fn timeout_error_exposes_timeout_info_fields() {
1224        let heartbeat_details = crate::protos::temporal::api::common::v1::Payloads {
1225            payloads: vec![Payload {
1226                data: b"hb".to_vec(),
1227                ..Default::default()
1228            }],
1229        };
1230        let failure = Failure {
1231            message: "timed out".to_owned(),
1232            failure_info: Some(FailureInfo::TimeoutFailureInfo(TimeoutFailureInfo {
1233                timeout_type: crate::protos::temporal::api::enums::v1::TimeoutType::Heartbeat
1234                    .into(),
1235                last_heartbeat_details: Some(heartbeat_details.clone()),
1236            })),
1237            ..Default::default()
1238        };
1239
1240        let decoded = DefaultFailureConverter
1241            .to_error(
1242                failure.clone(),
1243                &PayloadConverter::default(),
1244                &SerializationContextData::Workflow,
1245            )
1246            .unwrap();
1247
1248        let IncomingError::Timeout(timeout) = decoded else {
1249            panic!("expected timeout error");
1250        };
1251        assert_eq!(
1252            timeout.timeout_type(),
1253            crate::protos::temporal::api::enums::v1::TimeoutType::Heartbeat
1254        );
1255        assert_eq!(
1256            timeout.raw_last_heartbeat_details(),
1257            Some(heartbeat_details.payloads.as_slice())
1258        );
1259        assert_eq!(timeout.failure(), &failure);
1260    }
1261
1262    #[test]
1263    fn cancelled_error_exposes_details() {
1264        let details = crate::protos::temporal::api::common::v1::Payloads {
1265            payloads: vec![Payload {
1266                data: b"cancel".to_vec(),
1267                ..Default::default()
1268            }],
1269        };
1270        let failure = Failure {
1271            message: "cancelled".to_owned(),
1272            failure_info: Some(FailureInfo::CanceledFailureInfo(CanceledFailureInfo {
1273                details: Some(details.clone()),
1274                identity: Default::default(),
1275            })),
1276            ..Default::default()
1277        };
1278
1279        let decoded = DefaultFailureConverter
1280            .to_error(
1281                failure.clone(),
1282                &PayloadConverter::default(),
1283                &SerializationContextData::Workflow,
1284            )
1285            .unwrap();
1286
1287        let IncomingError::Cancelled(cancelled) = decoded else {
1288            panic!("expected cancelled error");
1289        };
1290        assert_eq!(cancelled.raw_details(), Some(details.payloads.as_slice()));
1291        assert_eq!(cancelled.failure(), &failure);
1292    }
1293
1294    #[test]
1295    fn child_workflow_decode_hint_preserves_child_failure_proto() {
1296        let failure = Failure {
1297            message: "child workflow failed".to_owned(),
1298            failure_info: Some(FailureInfo::ChildWorkflowExecutionFailureInfo(
1299                ChildWorkflowExecutionFailureInfo {
1300                    namespace: "default".to_owned(),
1301                    workflow_execution: Some(
1302                        crate::protos::temporal::api::common::v1::WorkflowExecution {
1303                            workflow_id: "child-id".to_owned(),
1304                            run_id: "run-id".to_owned(),
1305                        },
1306                    ),
1307                    workflow_type: Some(crate::protos::temporal::api::common::v1::WorkflowType {
1308                        name: "child-type".to_owned(),
1309                    }),
1310                    initiated_event_id: 11,
1311                    started_event_id: 22,
1312                    retry_state: crate::protos::temporal::api::enums::v1::RetryState::Timeout
1313                        .into(),
1314                },
1315            )),
1316            ..Default::default()
1317        };
1318        let decoded = data_converter()
1319            .to_error(
1320                &SerializationContextData::Workflow,
1321                failure.clone(),
1322                ChildWorkflowExecutionDecodeHint,
1323            )
1324            .unwrap();
1325
1326        let ChildWorkflowExecutionError::Failed(decoded_failure) = decoded else {
1327            panic!("expected failed child-workflow execution error");
1328        };
1329        assert_eq!(decoded_failure.failure(), &failure);
1330        assert_eq!(decoded_failure.namespace(), "default");
1331        assert_eq!(
1332            decoded_failure
1333                .workflow_execution()
1334                .map(|wf| wf.workflow_id.as_str()),
1335            Some("child-id")
1336        );
1337        assert_eq!(
1338            decoded_failure
1339                .workflow_execution()
1340                .map(|wf| wf.run_id.as_str()),
1341            Some("run-id")
1342        );
1343        assert_eq!(
1344            decoded_failure.workflow_type().map(|wf| wf.name.as_str()),
1345            Some("child-type")
1346        );
1347        assert_eq!(decoded_failure.initiated_event_id(), 11);
1348        assert_eq!(decoded_failure.started_event_id(), 22);
1349        assert_eq!(
1350            decoded_failure.retry_state(),
1351            crate::protos::temporal::api::enums::v1::RetryState::Timeout
1352        );
1353    }
1354
1355    #[rstest]
1356    #[case(
1357        Failure {
1358            message: "child workflow cancelled".to_owned(),
1359            cause: Some(Box::new(cancelled_failure("child workflow cancelled"))),
1360            failure_info: Some(FailureInfo::ChildWorkflowExecutionFailureInfo(
1361                ChildWorkflowExecutionFailureInfo::default(),
1362            )),
1363            ..Default::default()
1364        },
1365        Some(IncomingKind::Cancelled)
1366    )]
1367    #[case(timeout_failure("timed out"), None)]
1368    fn child_workflow_execution_decode_hint_adapts_expected_cause(
1369        #[case] failure: Failure,
1370        #[case] expected_cause: Option<IncomingKind>,
1371    ) {
1372        let decoded = data_converter()
1373            .to_error(
1374                &SerializationContextData::Workflow,
1375                failure.clone(),
1376                ChildWorkflowExecutionDecodeHint,
1377            )
1378            .unwrap();
1379
1380        let ChildWorkflowExecutionError::Failed(decoded_failure) = decoded else {
1381            panic!("expected failed child-workflow execution error");
1382        };
1383        assert_eq!(decoded_failure.failure(), &failure);
1384        match expected_cause {
1385            Some(expected) => {
1386                let cause = decoded_failure
1387                    .cause()
1388                    .expect("expected child failure cause");
1389                assert_incoming_kind(cause, expected);
1390            }
1391            None => assert!(decoded_failure.cause().is_none()),
1392        }
1393    }
1394
1395    #[test]
1396    fn child_workflow_start_decode_hint_preserves_top_level_cancellation() {
1397        let failure = Failure {
1398            message: "child start cancelled".to_owned(),
1399            failure_info: Some(FailureInfo::CanceledFailureInfo(
1400                CanceledFailureInfo::default(),
1401            )),
1402            ..Default::default()
1403        };
1404        let decoded = data_converter()
1405            .to_error(
1406                &SerializationContextData::Workflow,
1407                failure.clone(),
1408                ChildWorkflowStartDecodeHint,
1409            )
1410            .unwrap();
1411
1412        let ChildWorkflowStartError::Cancelled(decoded_failure) = decoded else {
1413            panic!("expected cancelled child-workflow start error");
1414        };
1415        assert_eq!(decoded_failure.failure(), &failure);
1416        assert!(decoded_failure.cause().is_none());
1417    }
1418
1419    #[test]
1420    fn child_workflow_signal_decode_hint_preserves_failure_proto() {
1421        let failure = Failure {
1422            message: "child workflow signal failed".to_owned(),
1423            cause: Some(Box::new(Failure {
1424                message: "timed out".to_owned(),
1425                failure_info: Some(FailureInfo::TimeoutFailureInfo(
1426                    TimeoutFailureInfo::default(),
1427                )),
1428                ..Default::default()
1429            })),
1430            ..Default::default()
1431        };
1432        let decoded = data_converter()
1433            .to_error(
1434                &SerializationContextData::Workflow,
1435                failure.clone(),
1436                WorkflowSignalDecodeHint,
1437            )
1438            .unwrap();
1439
1440        let WorkflowSignalError::Failed(decoded_failure) = decoded else {
1441            panic!("expected failed child-workflow signal error");
1442        };
1443        assert_eq!(decoded_failure.failure(), &failure);
1444        assert!(matches!(
1445            decoded_failure.error(),
1446            IncomingError::Application(_)
1447        ));
1448        assert!(matches!(
1449            decoded_failure.cause(),
1450            Some(IncomingError::Timeout(_))
1451        ));
1452        assert!(std::error::Error::source(&decoded_failure).is_some());
1453    }
1454
1455    #[test]
1456    fn outgoing_cancelled_activity_errors_encode_to_cancelled_failures() {
1457        let failure = DefaultFailureConverter.to_failure(
1458            OutgoingError::Activity(OutgoingActivityError::Cancelled { details: None }),
1459            &PayloadConverter::default(),
1460            &SerializationContextData::Activity,
1461        );
1462
1463        assert_eq!(failure.message, "Activity cancelled");
1464        assert!(matches!(
1465            failure.failure_info,
1466            Some(FailureInfo::CanceledFailureInfo(_))
1467        ));
1468    }
1469
1470    #[test]
1471    fn outgoing_cancelled_activity_errors_encode_serializable_details_with_payload_converter() {
1472        let failure = DefaultFailureConverter.to_failure(
1473            OutgoingError::Activity(OutgoingActivityError::Cancelled {
1474                details: Some("detail".to_string().into()),
1475            }),
1476            &PayloadConverter::default(),
1477            &SerializationContextData::Activity,
1478        );
1479
1480        let err = DefaultFailureConverter
1481            .to_error(
1482                failure,
1483                &PayloadConverter::default(),
1484                &SerializationContextData::Activity,
1485            )
1486            .unwrap();
1487        let cancelled = err.as_cancelled().unwrap();
1488        let details: String = cancelled.details().unwrap().unwrap();
1489        assert_eq!(details, "detail");
1490    }
1491}