1use 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
31pub trait FailureConverter {
33 fn to_failure(
35 &self,
36 error: OutgoingError,
37 payload_converter: &PayloadConverter,
38 context: &SerializationContextData,
39 ) -> Failure;
40
41 fn to_error(
43 &self,
44 failure: Failure,
45 payload_converter: &PayloadConverter,
46 context: &SerializationContextData,
47 ) -> Result<IncomingError, PayloadConversionError>;
48}
49
50pub struct DefaultFailureConverter;
52
53pub trait FailureDecodeHint {
55 type Output;
57
58 fn adapt(self, normalized: IncomingError) -> Self::Output;
60}
61
62#[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#[derive(Debug, Clone, Copy)]
76pub struct ActivityExecutionDecodeHint {
77 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 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#[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#[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#[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
227trait 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}