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