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