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