1use crate::{core::DomainReason, StructError};
2
3use super::{
4 context::OperationResult, report::DiagnosticReport, ErrorCategory, ErrorIdentityProvider,
5 ErrorMetadata, OperationContext, SourceFrame,
6};
7
8pub const STABLE_SNAPSHOT_SCHEMA_VERSION: &str = "orion-error.snapshot.v2";
9#[cfg_attr(feature = "serde", derive(serde::Serialize))]
10#[derive(Debug, Clone, PartialEq)]
11pub struct SnapshotContextFrame {
12 pub target: Option<String>,
14 pub action: Option<String>,
16 pub locator: Option<String>,
18 pub path: Vec<String>,
20 pub metadata: ErrorMetadata,
22 pub fields: Vec<(String, String)>,
24 pub result: OperationResult,
26}
27
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29#[derive(Debug, Clone, PartialEq)]
30pub struct StableSnapshotContextFrame {
31 pub target: Option<String>,
32 pub action: Option<String>,
33 pub locator: Option<String>,
34 pub path: Vec<String>,
35 pub metadata: ErrorMetadata,
36}
37
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39#[derive(Debug, Clone, PartialEq)]
40pub struct SnapshotSourceFrame {
41 pub index: usize,
42 pub message: String,
44 pub display: Option<String>,
46 pub type_name: Option<String>,
48 pub error_code: Option<i32>,
49 pub reason: Option<String>,
50 pub want: Option<String>,
51 pub path: Option<String>,
52 pub detail: Option<String>,
53 pub metadata: ErrorMetadata,
54 pub is_root_cause: bool,
55}
56
57#[cfg_attr(feature = "serde", derive(serde::Serialize))]
58#[derive(Debug, Clone, PartialEq)]
59pub struct StableSnapshotSourceFrame {
60 pub index: usize,
61 pub message: String,
62 pub error_code: Option<i32>,
63 pub reason: Option<String>,
64 pub want: Option<String>,
65 pub path: Option<String>,
66 pub detail: Option<String>,
67 pub metadata: ErrorMetadata,
68 pub is_root_cause: bool,
69}
70
71#[derive(Debug, Clone, PartialEq)]
77pub struct ErrorSnapshot {
78 pub reason: String,
79 pub detail: Option<String>,
80 pub position: Option<String>,
81 pub want: Option<String>,
82 pub path: Option<String>,
83 pub context: Vec<SnapshotContextFrame>,
84 pub root_metadata: ErrorMetadata,
85 pub source_frames: Vec<SnapshotSourceFrame>,
86}
87
88#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89#[derive(Debug, Clone, PartialEq)]
90pub struct StableErrorSnapshot {
91 pub schema_version: &'static str,
92 pub reason: String,
93 pub detail: Option<String>,
94 pub position: Option<String>,
95 pub want: Option<String>,
96 pub path: Option<String>,
97 pub context: Vec<StableSnapshotContextFrame>,
98 pub root_metadata: ErrorMetadata,
99 pub source_frames: Vec<StableSnapshotSourceFrame>,
100}
101
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct ErrorIdentity {
110 pub code: String,
111 pub category: ErrorCategory,
112 pub reason: String,
113 pub detail: Option<String>,
114 pub position: Option<String>,
115 pub want: Option<String>,
116 pub path: Option<String>,
117}
118
119#[cfg(feature = "serde")]
120impl serde::Serialize for ErrorSnapshot {
121 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122 where
123 S: serde::Serializer,
124 {
125 self.stable_export().serialize(serializer)
126 }
127}
128
129impl ErrorSnapshot {
130 pub fn stable_context(&self) -> &[SnapshotContextFrame] {
131 &self.context
132 }
133
134 pub fn stable_source_frames(&self) -> &[SnapshotSourceFrame] {
135 &self.source_frames
136 }
137
138 pub fn root_source_frame(&self) -> Option<&SnapshotSourceFrame> {
139 self.source_frames.iter().find(|frame| frame.is_root_cause)
140 }
141
142 pub fn stable_export(&self) -> StableErrorSnapshot {
143 self.clone().into_stable_export()
144 }
145
146 pub fn into_stable_export(self) -> StableErrorSnapshot {
147 StableErrorSnapshot {
148 schema_version: STABLE_SNAPSHOT_SCHEMA_VERSION,
149 reason: self.reason,
150 detail: self.detail,
151 position: self.position,
152 want: self.want,
153 path: self.path,
154 context: self.context.into_iter().map(Into::into).collect(),
155 root_metadata: self.root_metadata,
156 source_frames: self.source_frames.into_iter().map(Into::into).collect(),
157 }
158 }
159
160 #[cfg(feature = "serde_json")]
161 pub fn to_stable_snapshot_json(&self) -> serde_json::Result<serde_json::Value> {
162 serde_json::to_value(self.stable_export())
163 }
164
165 pub fn report(&self) -> DiagnosticReport {
166 self.clone().into_report()
167 }
168
169 pub fn into_report(self) -> DiagnosticReport {
170 DiagnosticReport {
171 reason: self.reason,
172 detail: self.detail,
173 position: self.position,
174 want: self.want,
175 path: self.path,
176 context: self.context.into_iter().map(Into::into).collect(),
177 root_metadata: self.root_metadata,
178 source_frames: self.source_frames.into_iter().map(Into::into).collect(),
179 }
180 }
181}
182
183impl StableErrorSnapshot {
184 pub fn report(&self) -> DiagnosticReport {
185 DiagnosticReport {
186 reason: self.reason.clone(),
187 detail: self.detail.clone(),
188 position: self.position.clone(),
189 want: self.want.clone(),
190 path: self.path.clone(),
191 context: self.context.iter().cloned().map(Into::into).collect(),
192 root_metadata: self.root_metadata.clone(),
193 source_frames: self.source_frames.iter().cloned().map(Into::into).collect(),
194 }
195 }
196
197 pub fn into_report(self) -> DiagnosticReport {
198 DiagnosticReport {
199 reason: self.reason,
200 detail: self.detail,
201 position: self.position,
202 want: self.want,
203 path: self.path,
204 context: self.context.into_iter().map(Into::into).collect(),
205 root_metadata: self.root_metadata,
206 source_frames: self.source_frames.into_iter().map(Into::into).collect(),
207 }
208 }
209}
210
211impl SnapshotContextFrame {
212 pub fn stable_export(&self) -> StableSnapshotContextFrame {
213 self.clone().into()
214 }
215}
216
217impl SnapshotSourceFrame {
218 pub fn stable_export(&self) -> StableSnapshotSourceFrame {
219 self.clone().into()
220 }
221}
222
223impl From<SnapshotContextFrame> for StableSnapshotContextFrame {
224 fn from(value: SnapshotContextFrame) -> Self {
225 StableSnapshotContextFrame {
226 target: value.target,
227 action: value.action,
228 locator: value.locator,
229 path: value.path,
230 metadata: value.metadata,
231 }
232 }
233}
234
235impl From<&SnapshotContextFrame> for StableSnapshotContextFrame {
236 fn from(value: &SnapshotContextFrame) -> Self {
237 value.clone().into()
238 }
239}
240
241impl From<StableSnapshotContextFrame> for SnapshotContextFrame {
242 fn from(value: StableSnapshotContextFrame) -> Self {
243 SnapshotContextFrame {
244 target: value.target,
245 action: value.action,
246 locator: value.locator,
247 path: value.path,
248 metadata: value.metadata,
249 fields: Vec::new(),
250 result: OperationResult::Fail,
251 }
252 }
253}
254
255impl From<&StableSnapshotContextFrame> for SnapshotContextFrame {
256 fn from(value: &StableSnapshotContextFrame) -> Self {
257 value.clone().into()
258 }
259}
260
261impl From<SnapshotSourceFrame> for StableSnapshotSourceFrame {
262 fn from(value: SnapshotSourceFrame) -> Self {
263 StableSnapshotSourceFrame {
264 index: value.index,
265 message: value.message,
266 error_code: value.error_code,
267 reason: value.reason,
268 want: value.want,
269 path: value.path,
270 detail: value.detail,
271 metadata: value.metadata,
272 is_root_cause: value.is_root_cause,
273 }
274 }
275}
276
277impl From<&SnapshotSourceFrame> for StableSnapshotSourceFrame {
278 fn from(value: &SnapshotSourceFrame) -> Self {
279 value.clone().into()
280 }
281}
282
283impl From<StableSnapshotSourceFrame> for SnapshotSourceFrame {
284 fn from(value: StableSnapshotSourceFrame) -> Self {
285 SnapshotSourceFrame {
286 index: value.index,
287 message: value.message,
288 display: None,
289 type_name: None,
290 error_code: value.error_code,
291 reason: value.reason,
292 want: value.want,
293 path: value.path,
294 detail: value.detail,
295 metadata: value.metadata,
296 is_root_cause: value.is_root_cause,
297 }
298 }
299}
300
301impl From<&StableSnapshotSourceFrame> for SnapshotSourceFrame {
302 fn from(value: &StableSnapshotSourceFrame) -> Self {
303 value.clone().into()
304 }
305}
306
307impl From<OperationContext> for SnapshotContextFrame {
308 fn from(value: OperationContext) -> Self {
309 Self {
310 target: value.target().clone(),
311 action: value.action().clone(),
312 locator: value.locator().clone(),
313 path: value.normalized_path_segments(),
314 metadata: value.metadata().clone(),
315 fields: value.context().items.clone(),
316 result: value.result().clone(),
317 }
318 }
319}
320
321impl From<SnapshotContextFrame> for OperationContext {
322 fn from(value: SnapshotContextFrame) -> Self {
323 let mut ctx = value
324 .target
325 .clone()
326 .map(OperationContext::from_target)
327 .unwrap_or_default();
328 ctx.replace_target_for_report(value.target);
329 ctx.replace_action_for_report(value.action);
330 ctx.replace_locator_for_report(value.locator);
331 ctx.replace_path_for_report(value.path);
332 ctx.context_mut_for_report().items = value.fields;
333 ctx.replace_metadata_for_report(value.metadata);
334 match value.result {
335 OperationResult::Suc => ctx.mark_suc(),
336 OperationResult::Fail => {}
337 OperationResult::Cancel => ctx.mark_cancel(),
338 }
339 ctx
340 }
341}
342
343impl From<StableSnapshotContextFrame> for OperationContext {
344 fn from(value: StableSnapshotContextFrame) -> Self {
345 SnapshotContextFrame::from(value).into()
346 }
347}
348
349impl From<&StableSnapshotContextFrame> for OperationContext {
350 fn from(value: &StableSnapshotContextFrame) -> Self {
351 value.clone().into()
352 }
353}
354
355impl From<SourceFrame> for SnapshotSourceFrame {
356 fn from(value: SourceFrame) -> Self {
357 Self {
358 index: value.index,
359 message: value.message,
360 display: value.display,
361 type_name: value.type_name,
362 error_code: value.error_code,
363 reason: value.reason,
364 want: value.want,
365 path: value.path,
366 detail: value.detail,
367 metadata: value.metadata,
368 is_root_cause: value.is_root_cause,
369 }
370 }
371}
372
373impl From<SnapshotSourceFrame> for SourceFrame {
374 fn from(value: SnapshotSourceFrame) -> Self {
375 Self {
376 index: value.index,
377 message: value.message,
378 display: value.display,
379 debug: String::new(),
380 type_name: value.type_name,
381 error_code: value.error_code,
382 reason: value.reason,
383 want: value.want,
384 path: value.path,
385 detail: value.detail,
386 metadata: value.metadata,
387 is_root_cause: value.is_root_cause,
388 }
389 }
390}
391
392impl From<StableSnapshotSourceFrame> for SourceFrame {
393 fn from(value: StableSnapshotSourceFrame) -> Self {
394 SnapshotSourceFrame::from(value).into()
395 }
396}
397
398impl From<&StableSnapshotSourceFrame> for SourceFrame {
399 fn from(value: &StableSnapshotSourceFrame) -> Self {
400 value.clone().into()
401 }
402}
403
404impl<T: DomainReason> From<&StructError<T>> for ErrorSnapshot {
405 fn from(value: &StructError<T>) -> Self {
406 value.snapshot()
407 }
408}
409
410impl<T: DomainReason> From<StructError<T>> for ErrorSnapshot {
411 fn from(value: StructError<T>) -> Self {
412 value.into_snapshot()
413 }
414}
415
416impl From<&ErrorSnapshot> for StableErrorSnapshot {
417 fn from(value: &ErrorSnapshot) -> Self {
418 value.stable_export()
419 }
420}
421
422impl From<ErrorSnapshot> for StableErrorSnapshot {
423 fn from(value: ErrorSnapshot) -> Self {
424 value.into_stable_export()
425 }
426}
427
428impl<T: DomainReason> From<&StructError<T>> for StableErrorSnapshot {
429 fn from(value: &StructError<T>) -> Self {
430 value.snapshot().into_stable_export()
431 }
432}
433
434impl<T: DomainReason> From<StructError<T>> for StableErrorSnapshot {
435 fn from(value: StructError<T>) -> Self {
436 value.into_snapshot().into_stable_export()
437 }
438}
439
440impl<T: DomainReason> StructError<T> {
441 pub fn snapshot(&self) -> ErrorSnapshot {
442 ErrorSnapshot {
443 reason: self.reason().to_string(),
444 detail: self.detail().clone(),
445 position: self.position().clone(),
446 want: self.target_main(),
447 path: self.target_path(),
448 context: self.contexts().iter().cloned().map(Into::into).collect(),
449 root_metadata: self.context_metadata(),
450 source_frames: self
451 .source_frames()
452 .iter()
453 .cloned()
454 .map(Into::into)
455 .collect(),
456 }
457 }
458
459 pub fn into_snapshot(self) -> ErrorSnapshot {
460 self.snapshot()
461 }
462}
463
464impl<T> StructError<T>
465where
466 T: DomainReason + ErrorIdentityProvider,
467{
468 pub fn identity_snapshot(&self) -> ErrorIdentity {
469 ErrorIdentity {
470 code: self.stable_code().to_string(),
471 category: self.error_category(),
472 reason: self.reason().to_string(),
473 detail: self.detail().clone(),
474 position: self.position().clone(),
475 want: self.target_main(),
476 path: self.target_path(),
477 }
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use crate::{
484 core::{context::ContextRecord, DomainReason, ErrorMetadata, SourceFrame},
485 ErrorCategory, ErrorCode, ErrorIdentityProvider, OperationContext, StructError, UvsReason,
486 };
487
488 use super::{
489 DiagnosticReport, ErrorSnapshot, SnapshotContextFrame, SnapshotSourceFrame,
490 StableErrorSnapshot, StableSnapshotContextFrame, StableSnapshotSourceFrame,
491 STABLE_SNAPSHOT_SCHEMA_VERSION,
492 };
493
494 #[derive(Debug, Clone, PartialEq, thiserror::Error)]
495 enum TestReason {
496 #[error("test error")]
497 TestError,
498 #[error("{0}")]
499 Uvs(UvsReason),
500 }
501
502 impl From<UvsReason> for TestReason {
503 fn from(value: UvsReason) -> Self {
504 Self::Uvs(value)
505 }
506 }
507
508 impl DomainReason for TestReason {}
509
510 impl ErrorCode for TestReason {
511 fn error_code(&self) -> i32 {
512 match self {
513 TestReason::TestError => 1001,
514 TestReason::Uvs(reason) => reason.error_code(),
515 }
516 }
517 }
518
519 impl ErrorIdentityProvider for TestReason {
520 fn stable_code(&self) -> &'static str {
521 match self {
522 TestReason::TestError => "test.test_error",
523 TestReason::Uvs(reason) => reason.stable_code(),
524 }
525 }
526
527 fn error_category(&self) -> ErrorCategory {
528 match self {
529 TestReason::TestError => ErrorCategory::Logic,
530 TestReason::Uvs(reason) => reason.error_category(),
531 }
532 }
533 }
534
535 #[test]
536 fn test_snapshot_captures_runtime_fields_and_source_frames() {
537 let source = StructError::from(TestReason::TestError).with_context(
538 OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
539 );
540 let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
541 .with_detail("engine bootstrap failed")
542 .with_position("src/main.rs:42")
543 .with_context(
544 OperationContext::doing("start engine").with_meta("component.name", "engine"),
545 )
546 .with_struct_source(source);
547
548 let snapshot = err.snapshot();
549
550 assert_eq!(snapshot.reason, "system error");
551 assert_eq!(snapshot.detail.as_deref(), Some("engine bootstrap failed"));
552 assert_eq!(snapshot.position.as_deref(), Some("src/main.rs:42"));
553 assert_eq!(snapshot.want.as_deref(), Some("start engine"));
554 assert_eq!(snapshot.context[0].target.as_deref(), Some("start engine"));
555 assert_eq!(
556 snapshot.root_metadata.get_str("component.name"),
557 Some("engine")
558 );
559 assert_eq!(
560 snapshot.source_frames[0].metadata.get_str("config.kind"),
561 Some("sink_defaults")
562 );
563 }
564
565 #[test]
566 fn test_identity_snapshot_captures_stable_identity_fields() {
567 let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
568 .with_detail("engine bootstrap failed")
569 .with_position("src/main.rs:42")
570 .with_context(OperationContext::doing("start engine"));
571
572 let identity = err.identity_snapshot();
573
574 assert_eq!(identity.code, "sys.io_error");
575 assert_eq!(identity.category, ErrorCategory::Sys);
576 assert_eq!(identity.reason, "system error");
577 assert_eq!(identity.detail.as_deref(), Some("engine bootstrap failed"));
578 assert_eq!(identity.position.as_deref(), Some("src/main.rs:42"));
579 assert_eq!(identity.want.as_deref(), Some("start engine"));
580 assert_eq!(identity.path.as_deref(), Some("start engine"));
581 }
582
583 #[cfg(feature = "serde")]
584 #[test]
585 fn test_identity_snapshot_serialization_includes_code_and_category() {
586 use super::ErrorIdentity;
587
588 let identity = ErrorIdentity {
589 code: "sys.io_error".to_string(),
590 category: ErrorCategory::Sys,
591 reason: "system error".to_string(),
592 detail: Some("engine bootstrap failed".to_string()),
593 position: Some("src/main.rs:42".to_string()),
594 want: Some("start engine".to_string()),
595 path: Some("start engine".to_string()),
596 };
597
598 let value = serde_json::to_value(identity).unwrap();
599
600 assert_eq!(value["code"], serde_json::json!("sys.io_error"));
601 assert_eq!(value["category"], serde_json::json!("Sys"));
602 assert_eq!(value["reason"], serde_json::json!("system error"));
603 }
604
605 #[test]
606 fn test_snapshot_preserves_action_and_locator_context_fields() {
607 let mut ctx = OperationContext::at("config.toml");
608 ctx.with_doing("parse config");
609
610 let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
611 .with_context(
612 OperationContext::doing("load config").with_meta("component.name", "engine"),
613 )
614 .with_context(ctx);
615
616 let snapshot = err.snapshot();
617
618 assert_eq!(snapshot.context[0].action.as_deref(), Some("load config"));
619 assert_eq!(snapshot.context[1].action.as_deref(), Some("parse config"));
620 assert_eq!(snapshot.context[1].locator.as_deref(), Some("config.toml"));
621
622 let report = snapshot.into_report();
623 assert_eq!(report.context[1].action().as_deref(), Some("parse config"));
624 assert_eq!(report.context[1].locator().as_deref(), Some("config.toml"));
625 }
626
627 #[test]
628 fn test_snapshot_report_conversion_preserves_payload() {
629 let snapshot = ErrorSnapshot {
630 reason: "system error".to_string(),
631 detail: Some("engine bootstrap failed".to_string()),
632 position: Some("src/main.rs:42".to_string()),
633 want: Some("start engine".to_string()),
634 path: Some("start engine / load defaults".to_string()),
635 context: vec![SnapshotContextFrame {
636 target: Some("start engine".to_string()),
637 action: None,
638 locator: None,
639 path: vec!["start engine".to_string()],
640 metadata: ErrorMetadata::new(),
641 fields: vec![],
642 result: crate::core::context::OperationResult::Fail,
643 }],
644 root_metadata: {
645 let mut metadata = ErrorMetadata::new();
646 metadata.insert("component.name", "engine");
647 metadata
648 },
649 source_frames: vec![],
650 };
651
652 let report = snapshot.report();
653
654 assert_eq!(report.reason, snapshot.reason);
655 assert_eq!(report.detail, snapshot.detail);
656 assert_eq!(report.position, snapshot.position);
657 assert_eq!(report.want, snapshot.want);
658 assert_eq!(report.path, snapshot.path);
659 assert_eq!(
660 report.context,
661 snapshot
662 .context
663 .clone()
664 .into_iter()
665 .map(Into::into)
666 .collect::<Vec<OperationContext>>()
667 );
668 assert_eq!(report.root_metadata, snapshot.root_metadata);
669 assert_eq!(
670 report.source_frames,
671 snapshot
672 .source_frames
673 .clone()
674 .into_iter()
675 .map(Into::into)
676 .collect::<Vec<SourceFrame>>()
677 );
678 }
679
680 #[test]
681 fn test_snapshot_from_struct_error_matches_snapshot_method() {
682 let err = StructError::from(TestReason::TestError)
683 .with_detail("engine bootstrap failed")
684 .with_context(OperationContext::doing("start engine"));
685
686 let via_method = err.snapshot();
687 let via_from = ErrorSnapshot::from(&err);
688
689 assert_eq!(via_from, via_method);
690 }
691
692 #[test]
693 fn test_snapshot_from_owned_struct_error_matches_snapshot_method() {
694 let err = StructError::from(TestReason::TestError)
695 .with_detail("engine bootstrap failed")
696 .with_context(OperationContext::doing("start engine"));
697
698 let via_method = err.snapshot();
699 let via_from = ErrorSnapshot::from(err);
700
701 assert_eq!(via_from, via_method);
702 }
703
704 #[test]
705 fn test_struct_error_into_snapshot_matches_snapshot_method() {
706 let err = StructError::from(TestReason::TestError)
707 .with_detail("engine bootstrap failed")
708 .with_context(OperationContext::doing("start engine"));
709
710 let via_method = err.snapshot();
711 let via_into = err.into_snapshot();
712
713 assert_eq!(via_into, via_method);
714 }
715
716 #[test]
717 fn test_snapshot_into_report_matches_borrowed_report() {
718 let snapshot = ErrorSnapshot {
719 reason: "system error".to_string(),
720 detail: Some("engine bootstrap failed".to_string()),
721 position: Some("src/main.rs:42".to_string()),
722 want: Some("start engine".to_string()),
723 path: Some("start engine".to_string()),
724 context: vec![SnapshotContextFrame {
725 target: Some("start engine".to_string()),
726 action: None,
727 locator: None,
728 path: vec!["start engine".to_string()],
729 metadata: ErrorMetadata::new(),
730 fields: vec![("tenant".to_string(), "alpha".to_string())],
731 result: crate::core::context::OperationResult::Fail,
732 }],
733 root_metadata: ErrorMetadata::new(),
734 source_frames: vec![SnapshotSourceFrame {
735 index: 0,
736 message: "db unavailable".to_string(),
737 display: Some("db unavailable".to_string()),
738 type_name: Some("std::io::Error".to_string()),
739 error_code: None,
740 reason: None,
741 want: Some("load config".to_string()),
742 path: Some("load config / read".to_string()),
743 detail: Some("inner detail".to_string()),
744 metadata: ErrorMetadata::new(),
745 is_root_cause: true,
746 }],
747 };
748
749 let via_borrowed = snapshot.report();
750 let via_owned = snapshot.clone().into_report();
751 let via_from = DiagnosticReport::from(snapshot);
752
753 assert_eq!(via_owned, via_borrowed);
754 assert_eq!(via_from, via_borrowed);
755 }
756
757 #[test]
758 fn test_snapshot_stable_helpers_prefer_snapshot_native_frames() {
759 let source = StructError::from(TestReason::TestError)
760 .with_detail("inner detail")
761 .with_context(
762 OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
763 );
764 let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
765 .with_detail("outer detail")
766 .with_context(OperationContext::doing("start engine"))
767 .with_struct_source(source);
768
769 let snapshot = err.snapshot();
770
771 assert_eq!(snapshot.stable_context(), snapshot.context.as_slice());
772 assert_eq!(
773 snapshot.stable_source_frames(),
774 snapshot.source_frames.as_slice()
775 );
776 assert_eq!(snapshot.root_source_frame().unwrap().message, "test error");
777 assert_eq!(
778 snapshot
779 .root_source_frame()
780 .unwrap()
781 .metadata
782 .get_str("config.kind"),
783 Some("sink_defaults")
784 );
785 }
786
787 #[test]
788 fn test_snapshot_stable_export_strips_compat_projection_fields() {
789 let source = StructError::from(TestReason::TestError)
790 .with_detail("inner detail")
791 .with_context(
792 OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
793 );
794 let mut outer = OperationContext::at("engine.toml");
795 outer.with_doing("start engine");
796 let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
797 .with_detail("outer detail")
798 .with_context(outer)
799 .with_struct_source(source);
800
801 let snapshot = err.snapshot();
802 let stable = snapshot.stable_export();
803
804 assert_eq!(stable.schema_version, STABLE_SNAPSHOT_SCHEMA_VERSION);
805 assert_eq!(stable.reason, snapshot.reason);
806 assert_eq!(stable.context[0].target.as_deref(), Some("start engine"));
807 assert_eq!(stable.context[0].action.as_deref(), Some("start engine"));
808 assert_eq!(stable.context[0].locator.as_deref(), Some("engine.toml"));
809 assert_eq!(
810 stable.context[0].path,
811 vec!["start engine".to_string(), "engine.toml".to_string()]
812 );
813 assert_eq!(
814 stable.source_frames[0].message,
815 snapshot.source_frames[0].message
816 );
817 assert_eq!(
818 stable.source_frames[0].metadata.get_str("config.kind"),
819 Some("sink_defaults")
820 );
821 }
822
823 #[test]
824 fn test_snapshot_into_stable_export_matches_borrowed_stable_export() {
825 let snapshot = ErrorSnapshot {
826 reason: "system error".to_string(),
827 detail: Some("outer detail".to_string()),
828 position: Some("src/main.rs:42".to_string()),
829 want: Some("start engine".to_string()),
830 path: Some("start engine".to_string()),
831 context: vec![SnapshotContextFrame {
832 target: Some("start engine".to_string()),
833 action: None,
834 locator: None,
835 path: vec!["start engine".to_string()],
836 metadata: ErrorMetadata::new(),
837 fields: vec![("tenant".to_string(), "alpha".to_string())],
838 result: crate::core::context::OperationResult::Fail,
839 }],
840 root_metadata: ErrorMetadata::new(),
841 source_frames: vec![SnapshotSourceFrame {
842 index: 0,
843 message: "db unavailable".to_string(),
844 display: Some("db unavailable".to_string()),
845 type_name: Some("std::io::Error".to_string()),
846 error_code: None,
847 reason: None,
848 want: Some("load config".to_string()),
849 path: Some("load config / read".to_string()),
850 detail: Some("inner detail".to_string()),
851 metadata: ErrorMetadata::new(),
852 is_root_cause: true,
853 }],
854 };
855
856 let via_borrowed = snapshot.stable_export();
857 let via_owned = snapshot.clone().into_stable_export();
858 let via_from_borrowed = StableErrorSnapshot::from(&snapshot);
859 let via_from_owned = StableErrorSnapshot::from(snapshot);
860
861 assert_eq!(via_owned, via_borrowed);
862 assert_eq!(via_from_borrowed, via_borrowed);
863 assert_eq!(via_from_owned, via_borrowed);
864 assert_eq!(via_borrowed.schema_version, STABLE_SNAPSHOT_SCHEMA_VERSION);
865 }
866
867 #[test]
868 fn test_stable_snapshot_from_struct_error_matches_snapshot_stable_export() {
869 let source = StructError::from(TestReason::TestError)
870 .with_detail("inner detail")
871 .with_context(
872 OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
873 );
874 let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
875 .with_detail("outer detail")
876 .with_context(OperationContext::doing("start engine"))
877 .with_struct_source(source);
878
879 let via_method = err.snapshot().stable_export();
880 let via_borrowed = StableErrorSnapshot::from(&err);
881 let via_owned = StableErrorSnapshot::from(err);
882
883 assert_eq!(via_borrowed, via_method);
884 assert_eq!(via_owned, via_method);
885 }
886
887 #[test]
888 fn test_snapshot_frame_stable_from_matches_stable_export() {
889 let context = SnapshotContextFrame {
890 target: Some("start engine".to_string()),
891 action: None,
892 locator: None,
893 path: vec!["start engine".to_string()],
894 metadata: ErrorMetadata::new(),
895 fields: vec![("tenant".to_string(), "alpha".to_string())],
896 result: crate::core::context::OperationResult::Fail,
897 };
898 let source = SnapshotSourceFrame {
899 index: 0,
900 message: "db unavailable".to_string(),
901 display: Some("db unavailable".to_string()),
902 type_name: Some("std::io::Error".to_string()),
903 error_code: None,
904 reason: None,
905 want: Some("load config".to_string()),
906 path: Some("load config / read".to_string()),
907 detail: Some("inner detail".to_string()),
908 metadata: ErrorMetadata::new(),
909 is_root_cause: true,
910 };
911
912 assert_eq!(
913 StableSnapshotContextFrame::from(&context),
914 context.stable_export()
915 );
916 assert_eq!(
917 StableSnapshotContextFrame::from(context.clone()),
918 context.stable_export()
919 );
920 assert_eq!(
921 StableSnapshotSourceFrame::from(&source),
922 source.stable_export()
923 );
924 assert_eq!(
925 StableSnapshotSourceFrame::from(source.clone()),
926 source.stable_export()
927 );
928 }
929
930 #[test]
931 fn test_stable_snapshot_into_report_matches_report() {
932 let stable = StableErrorSnapshot {
933 schema_version: STABLE_SNAPSHOT_SCHEMA_VERSION,
934 reason: "system error".to_string(),
935 detail: Some("outer detail".to_string()),
936 position: None,
937 want: Some("start engine".to_string()),
938 path: Some("start engine".to_string()),
939 context: vec![StableSnapshotContextFrame {
940 target: Some("start engine".to_string()),
941 action: None,
942 locator: None,
943 path: vec!["start engine".to_string()],
944 metadata: ErrorMetadata::new(),
945 }],
946 root_metadata: ErrorMetadata::new(),
947 source_frames: vec![StableSnapshotSourceFrame {
948 index: 0,
949 message: "db unavailable".to_string(),
950 error_code: None,
951 reason: None,
952 want: Some("load config".to_string()),
953 path: Some("load config / read".to_string()),
954 detail: Some("inner detail".to_string()),
955 metadata: ErrorMetadata::new(),
956 is_root_cause: true,
957 }],
958 };
959
960 let via_method = stable.report();
961 let via_owned = stable.clone().into_report();
962
963 assert_eq!(via_owned, via_method);
964 }
965
966 #[test]
967 fn test_stable_frame_to_compat_frame_defaults_compat_fields() {
968 let context = StableSnapshotContextFrame {
969 target: Some("start engine".to_string()),
970 action: None,
971 locator: None,
972 path: vec!["start engine".to_string()],
973 metadata: ErrorMetadata::new(),
974 };
975 let source = StableSnapshotSourceFrame {
976 index: 0,
977 message: "db unavailable".to_string(),
978 error_code: None,
979 reason: None,
980 want: Some("load config".to_string()),
981 path: Some("load config / read".to_string()),
982 detail: Some("inner detail".to_string()),
983 metadata: ErrorMetadata::new(),
984 is_root_cause: true,
985 };
986
987 let compat_context = SnapshotContextFrame::from(&context);
988 let compat_source = SnapshotSourceFrame::from(&source);
989
990 assert_eq!(compat_context.target, context.target);
991 assert_eq!(compat_context.path, context.path);
992 assert_eq!(compat_context.fields, Vec::<(String, String)>::new());
993 assert_eq!(
994 compat_context.result,
995 crate::core::context::OperationResult::Fail
996 );
997 assert_eq!(compat_source.message, source.message);
998 assert_eq!(compat_source.display, None);
999 assert_eq!(compat_source.type_name, None);
1000 }
1001
1002 #[test]
1003 fn test_snapshot_context_frame_roundtrip_to_operation_context() {
1004 let mut ctx = OperationContext::doing("start engine");
1005 ctx.with_doing("load defaults");
1006 ctx.record("tenant", "alpha");
1007 ctx.record_meta("component.name", "engine");
1008
1009 let snapshot_frame = SnapshotContextFrame::from(ctx.clone());
1010 let roundtrip: OperationContext = snapshot_frame.clone().into();
1011
1012 assert_eq!(snapshot_frame.target.as_deref(), Some("start engine"));
1013 assert_eq!(
1014 snapshot_frame.path,
1015 vec!["start engine".to_string(), "load defaults".to_string()]
1016 );
1017 assert_eq!(roundtrip.target().as_deref(), Some("start engine"));
1018 assert_eq!(
1019 roundtrip.path(),
1020 vec!["start engine".to_string(), "load defaults".to_string()]
1021 );
1022 assert_eq!(
1023 roundtrip.metadata().get_str("component.name"),
1024 Some("engine")
1025 );
1026 assert_eq!(
1027 roundtrip.context().items,
1028 vec![("tenant".to_string(), "alpha".to_string())]
1029 );
1030 }
1031
1032 #[test]
1033 fn test_snapshot_context_frame_roundtrip_normalizes_action_locator_path() {
1034 let mut ctx = OperationContext::at("engine.toml");
1035 ctx.with_doing("start engine");
1036
1037 let snapshot_frame = SnapshotContextFrame::from(ctx);
1038 let roundtrip: OperationContext = snapshot_frame.clone().into();
1039
1040 assert_eq!(snapshot_frame.target.as_deref(), Some("start engine"));
1041 assert_eq!(snapshot_frame.action.as_deref(), Some("start engine"));
1042 assert_eq!(snapshot_frame.locator.as_deref(), Some("engine.toml"));
1043 assert_eq!(
1044 snapshot_frame.path,
1045 vec!["start engine".to_string(), "engine.toml".to_string()]
1046 );
1047 assert_eq!(
1048 roundtrip.path(),
1049 vec!["start engine".to_string(), "engine.toml".to_string()]
1050 );
1051 assert_eq!(
1052 roundtrip.path_string().as_deref(),
1053 Some("start engine / engine.toml")
1054 );
1055 }
1056
1057 #[test]
1058 fn test_snapshot_source_frame_roundtrip_to_report_frame() {
1059 let frame = SnapshotSourceFrame {
1060 index: 0,
1061 message: "db unavailable".to_string(),
1062 display: Some("db unavailable".to_string()),
1063 type_name: Some("std::io::Error".to_string()),
1064 error_code: None,
1065 reason: None,
1066 want: Some("load config".to_string()),
1067 path: Some("load config / read".to_string()),
1068 detail: Some("inner detail".to_string()),
1069 metadata: {
1070 let mut metadata = ErrorMetadata::new();
1071 metadata.insert("config.kind", "sink_defaults");
1072 metadata
1073 },
1074 is_root_cause: true,
1075 };
1076
1077 let report_frame: SourceFrame = frame.clone().into();
1078 let roundtrip = SnapshotSourceFrame::from(report_frame);
1079
1080 assert_eq!(roundtrip, frame);
1081 }
1082
1083 #[cfg(feature = "serde")]
1084 #[test]
1085 fn test_snapshot_default_serialization_uses_stable_export_shape() {
1086 let source = StructError::from(TestReason::TestError)
1087 .with_detail("inner detail")
1088 .with_context(
1089 OperationContext::doing("load defaults").with_meta("config.kind", "sink_defaults"),
1090 );
1091 let err = StructError::from(TestReason::Uvs(UvsReason::system_error()))
1092 .with_detail("engine bootstrap failed")
1093 .with_position("src/main.rs:42")
1094 .with_context(
1095 OperationContext::doing("start engine").with_meta("component.name", "engine"),
1096 )
1097 .with_struct_source(source);
1098
1099 let json_value = serde_json::to_value(err.snapshot()).unwrap();
1100
1101 assert_eq!(
1102 json_value["schema_version"],
1103 serde_json::json!(STABLE_SNAPSHOT_SCHEMA_VERSION)
1104 );
1105 assert_eq!(json_value["reason"], serde_json::json!("system error"));
1106 assert_eq!(
1107 json_value["detail"],
1108 serde_json::json!("engine bootstrap failed")
1109 );
1110 assert_eq!(json_value["position"], serde_json::json!("src/main.rs:42"));
1111 assert_eq!(json_value["want"], serde_json::json!("start engine"));
1112 assert_eq!(json_value["path"], serde_json::json!("start engine"));
1113 assert_eq!(
1114 json_value["root_metadata"]["component.name"],
1115 serde_json::json!("engine")
1116 );
1117 assert_eq!(
1118 json_value["context"][0]["target"],
1119 serde_json::json!("start engine")
1120 );
1121 assert_eq!(
1122 json_value["context"][0]["action"],
1123 serde_json::json!("start engine")
1124 );
1125 assert_eq!(json_value["context"][0]["locator"], serde_json::Value::Null);
1126 assert_eq!(
1127 json_value["context"][0]["path"],
1128 serde_json::json!(["start engine"])
1129 );
1130 assert_eq!(
1131 json_value["context"][0]["metadata"]["component.name"],
1132 serde_json::json!("engine")
1133 );
1134 assert!(json_value["context"][0].get("fields").is_none());
1135 assert!(json_value["context"][0].get("result").is_none());
1136 assert_eq!(
1137 json_value["source_frames"][0]["message"],
1138 serde_json::json!("test error")
1139 );
1140 assert_eq!(
1141 json_value["source_frames"][0]["reason"],
1142 serde_json::json!("test error")
1143 );
1144 assert_eq!(
1145 json_value["source_frames"][0]["want"],
1146 serde_json::json!("load defaults")
1147 );
1148 assert_eq!(
1149 json_value["source_frames"][0]["path"],
1150 serde_json::json!("load defaults")
1151 );
1152 assert_eq!(
1153 json_value["source_frames"][0]["detail"],
1154 serde_json::json!("inner detail")
1155 );
1156 assert_eq!(
1157 json_value["source_frames"][0]["metadata"]["config.kind"],
1158 serde_json::json!("sink_defaults")
1159 );
1160 assert_eq!(
1161 json_value["source_frames"][0]["is_root_cause"],
1162 serde_json::json!(true)
1163 );
1164 assert!(json_value["source_frames"][0].get("debug").is_none());
1165 assert!(json_value["source_frames"][0].get("display").is_none());
1166 assert!(json_value["source_frames"][0].get("type_name").is_none());
1167 assert!(json_value.get("source_message").is_none());
1168 assert!(json_value.get("source_chain").is_none());
1169 }
1170
1171 #[cfg(feature = "serde")]
1172 #[test]
1173 fn test_snapshot_stable_export_serialization_omits_compat_projection_fields() {
1174 let snapshot = ErrorSnapshot {
1175 reason: "system error".to_string(),
1176 detail: Some("outer detail".to_string()),
1177 position: Some("src/main.rs:42".to_string()),
1178 want: Some("start engine".to_string()),
1179 path: Some("start engine".to_string()),
1180 context: vec![SnapshotContextFrame {
1181 target: Some("start engine".to_string()),
1182 action: Some("start engine".to_string()),
1183 locator: Some("engine.toml".to_string()),
1184 path: vec!["start engine".to_string()],
1185 metadata: {
1186 let mut metadata = ErrorMetadata::new();
1187 metadata.insert("component.name", "engine");
1188 metadata
1189 },
1190 fields: vec![("tenant".to_string(), "alpha".to_string())],
1191 result: crate::core::context::OperationResult::Fail,
1192 }],
1193 root_metadata: {
1194 let mut metadata = ErrorMetadata::new();
1195 metadata.insert("component.name", "engine");
1196 metadata
1197 },
1198 source_frames: vec![SnapshotSourceFrame {
1199 index: 0,
1200 message: "db unavailable".to_string(),
1201 display: Some("db unavailable".to_string()),
1202 type_name: Some("std::io::Error".to_string()),
1203 error_code: None,
1204 reason: None,
1205 want: Some("load config".to_string()),
1206 path: Some("load config / read".to_string()),
1207 detail: Some("inner detail".to_string()),
1208 metadata: {
1209 let mut metadata = ErrorMetadata::new();
1210 metadata.insert("config.kind", "sink_defaults");
1211 metadata
1212 },
1213 is_root_cause: true,
1214 }],
1215 };
1216
1217 let stable = snapshot.stable_export();
1218 let json_value = serde_json::to_value(&stable).unwrap();
1219
1220 assert_eq!(StableErrorSnapshot::clone(&stable), stable);
1221 assert_eq!(
1222 json_value["schema_version"],
1223 serde_json::json!(STABLE_SNAPSHOT_SCHEMA_VERSION)
1224 );
1225 assert_eq!(json_value["reason"], serde_json::json!("system error"));
1226 assert_eq!(
1227 json_value["context"][0]["target"],
1228 serde_json::json!("start engine")
1229 );
1230 assert_eq!(
1231 json_value["context"][0]["action"],
1232 serde_json::json!("start engine")
1233 );
1234 assert_eq!(
1235 json_value["context"][0]["locator"],
1236 serde_json::json!("engine.toml")
1237 );
1238 assert_eq!(
1239 json_value["context"][0]["path"],
1240 serde_json::json!(["start engine"])
1241 );
1242 assert_eq!(
1243 json_value["context"][0]["metadata"]["component.name"],
1244 serde_json::json!("engine")
1245 );
1246 assert!(json_value["context"][0].get("fields").is_none());
1247 assert!(json_value["context"][0].get("result").is_none());
1248 assert_eq!(
1249 json_value["source_frames"][0]["message"],
1250 serde_json::json!("db unavailable")
1251 );
1252 assert_eq!(
1253 json_value["source_frames"][0]["path"],
1254 serde_json::json!("load config / read")
1255 );
1256 assert_eq!(
1257 json_value["source_frames"][0]["detail"],
1258 serde_json::json!("inner detail")
1259 );
1260 assert!(json_value["source_frames"][0].get("display").is_none());
1261 assert!(json_value["source_frames"][0].get("type_name").is_none());
1262 }
1263
1264 #[cfg(feature = "serde_json")]
1265 #[test]
1266 fn test_to_stable_snapshot_json_uses_stable_export_shape() {
1267 let snapshot = ErrorSnapshot {
1268 reason: "system error".to_string(),
1269 detail: Some("outer detail".to_string()),
1270 position: None,
1271 want: Some("start engine".to_string()),
1272 path: Some("start engine".to_string()),
1273 context: vec![SnapshotContextFrame {
1274 target: Some("start engine".to_string()),
1275 action: Some("start engine".to_string()),
1276 locator: Some("engine.toml".to_string()),
1277 path: vec!["start engine".to_string()],
1278 metadata: ErrorMetadata::new(),
1279 fields: vec![("tenant".to_string(), "alpha".to_string())],
1280 result: crate::core::context::OperationResult::Fail,
1281 }],
1282 root_metadata: ErrorMetadata::new(),
1283 source_frames: vec![SnapshotSourceFrame {
1284 index: 0,
1285 message: "db unavailable".to_string(),
1286 display: Some("db unavailable".to_string()),
1287 type_name: Some("std::io::Error".to_string()),
1288 error_code: None,
1289 reason: None,
1290 want: Some("load config".to_string()),
1291 path: Some("load config / read".to_string()),
1292 detail: None,
1293 metadata: ErrorMetadata::new(),
1294 is_root_cause: true,
1295 }],
1296 };
1297
1298 let json_value = snapshot.to_stable_snapshot_json().unwrap();
1299
1300 assert_eq!(
1301 json_value,
1302 serde_json::to_value(snapshot.stable_export()).unwrap()
1303 );
1304 assert_eq!(
1305 json_value["schema_version"],
1306 serde_json::json!(STABLE_SNAPSHOT_SCHEMA_VERSION)
1307 );
1308 assert_eq!(
1309 json_value["context"][0]["action"],
1310 serde_json::json!("start engine")
1311 );
1312 assert_eq!(
1313 json_value["context"][0]["locator"],
1314 serde_json::json!("engine.toml")
1315 );
1316 assert!(json_value["context"][0].get("fields").is_none());
1317 assert!(json_value["source_frames"][0].get("display").is_none());
1318 }
1319
1320 #[cfg(feature = "serde_json")]
1321 #[test]
1322 fn test_stable_snapshot_json_fields_match_schema_constants() {
1323 let snapshot = ErrorSnapshot {
1324 reason: "system error".to_string(),
1325 detail: Some("outer detail".to_string()),
1326 position: Some("src/main.rs:42".to_string()),
1327 want: Some("start engine".to_string()),
1328 path: Some("start engine".to_string()),
1329 context: vec![SnapshotContextFrame {
1330 target: Some("start engine".to_string()),
1331 action: None,
1332 locator: None,
1333 path: vec!["start engine".to_string()],
1334 metadata: ErrorMetadata::new(),
1335 fields: vec![("tenant".to_string(), "alpha".to_string())],
1336 result: crate::core::context::OperationResult::Fail,
1337 }],
1338 root_metadata: ErrorMetadata::new(),
1339 source_frames: vec![SnapshotSourceFrame {
1340 index: 0,
1341 message: "db unavailable".to_string(),
1342 display: Some("db unavailable".to_string()),
1343 type_name: Some("std::io::Error".to_string()),
1344 error_code: None,
1345 reason: None,
1346 want: Some("load config".to_string()),
1347 path: Some("load config / read".to_string()),
1348 detail: Some("inner detail".to_string()),
1349 metadata: ErrorMetadata::new(),
1350 is_root_cause: true,
1351 }],
1352 };
1353
1354 let json_value = snapshot.to_stable_snapshot_json().unwrap();
1355 let top_level = json_value.as_object().unwrap();
1356 let context = json_value["context"][0].as_object().unwrap();
1357 let source_frame = json_value["source_frames"][0].as_object().unwrap();
1358
1359 assert_eq!(
1360 sorted_keys(top_level),
1361 sorted_strings(&[
1362 "schema_version",
1363 "reason",
1364 "detail",
1365 "position",
1366 "want",
1367 "path",
1368 "context",
1369 "root_metadata",
1370 "source_frames",
1371 ])
1372 );
1373 assert_eq!(
1374 sorted_keys(context),
1375 sorted_strings(&["target", "action", "locator", "path", "metadata"])
1376 );
1377 assert_eq!(
1378 sorted_keys(source_frame),
1379 sorted_strings(&[
1380 "index",
1381 "message",
1382 "error_code",
1383 "reason",
1384 "want",
1385 "path",
1386 "detail",
1387 "metadata",
1388 "is_root_cause",
1389 ])
1390 );
1391 }
1392
1393 #[cfg(feature = "serde_json")]
1394 fn sorted_keys(map: &serde_json::Map<String, serde_json::Value>) -> Vec<String> {
1395 let mut keys = map.keys().cloned().collect::<Vec<_>>();
1396 keys.sort();
1397 keys
1398 }
1399
1400 #[cfg(feature = "serde_json")]
1401 fn sorted_strings(values: &[&str]) -> Vec<String> {
1402 let mut values = values
1403 .iter()
1404 .map(|value| value.to_string())
1405 .collect::<Vec<_>>();
1406 values.sort();
1407 values
1408 }
1409}
1410
1411#[cfg(doc)]
1412mod stable_snapshot_compile_fail_docs {
1413 }