Skip to main content

asupersync/trace/
format.rs

1//! Formatting utilities for trace output.
2//!
3//! Provides human-readable and machine-readable formatting for traces.
4
5use super::buffer::TraceBuffer;
6use super::canonicalize::{TraceEventKey, canonicalize, trace_event_key, trace_fingerprint};
7use super::event::TraceEvent;
8use serde::{Deserialize, Serialize};
9use std::io::{self, Write};
10
11/// Schema version for golden trace fixtures.
12pub const GOLDEN_TRACE_SCHEMA_VERSION: u32 = 1;
13
14/// Minimal configuration snapshot for golden trace fixtures.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct GoldenTraceConfig {
17    /// Deterministic seed used to run the workload.
18    pub seed: u64,
19    /// Entropy seed used for capability randomness.
20    pub entropy_seed: u64,
21    /// Virtual worker count.
22    pub worker_count: usize,
23    /// Trace buffer capacity.
24    pub trace_capacity: usize,
25    /// Maximum steps before termination (if set).
26    pub max_steps: Option<u64>,
27    /// Maximum number of Foata layers to keep in the canonical prefix.
28    pub canonical_prefix_layers: usize,
29    /// Maximum number of events to keep in the canonical prefix.
30    pub canonical_prefix_events: usize,
31}
32
33/// Summary of oracle results for a golden trace run.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct GoldenTraceOracleSummary {
36    /// Sorted list of oracle violation tags (empty if all invariants held).
37    pub violations: Vec<String>,
38}
39
40/// Golden trace fixture for deterministic verification.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct GoldenTraceFixture {
43    /// Fixture schema version.
44    pub schema_version: u32,
45    /// Configuration snapshot.
46    pub config: GoldenTraceConfig,
47    /// Canonical trace fingerprint.
48    pub fingerprint: u64,
49    /// Number of events in the trace.
50    pub event_count: u64,
51    /// Canonicalized prefix (Foata layers of stable event keys).
52    pub canonical_prefix: Vec<Vec<TraceEventKey>>,
53    /// Oracle summary captured at end of the run.
54    pub oracle_summary: GoldenTraceOracleSummary,
55}
56
57impl GoldenTraceFixture {
58    /// Build a golden trace fixture from a trace event slice.
59    #[must_use]
60    pub fn from_events(
61        config: GoldenTraceConfig,
62        events: &[TraceEvent],
63        oracle_violations: impl IntoIterator<Item = impl Into<String>>,
64    ) -> Self {
65        let canonical_prefix = canonical_prefix(
66            events,
67            config.canonical_prefix_layers,
68            config.canonical_prefix_events,
69        );
70        let mut violations: Vec<String> = oracle_violations.into_iter().map(Into::into).collect();
71        violations.sort();
72        violations.dedup();
73
74        Self {
75            schema_version: GOLDEN_TRACE_SCHEMA_VERSION,
76            fingerprint: trace_fingerprint(events),
77            event_count: u64::try_from(events.len()).unwrap_or(u64::MAX),
78            canonical_prefix,
79            oracle_summary: GoldenTraceOracleSummary { violations },
80            config,
81        }
82    }
83
84    /// Compare two fixtures and return a diff if any field changed.
85    pub fn verify(&self, actual: &Self) -> Result<(), GoldenTraceDiff> {
86        GoldenTraceDiff::from_fixtures(self, actual).into_result()
87    }
88
89    /// Build a structured replay-delta report against another fixture.
90    #[must_use]
91    pub fn delta_report(&self, actual: &Self) -> GoldenTraceDeltaReport {
92        GoldenTraceDiff::from_fixtures(self, actual).to_delta_report(self, actual)
93    }
94}
95
96/// Category of replay-delta mismatch.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum GoldenTraceDeltaClass {
100    /// Runtime configuration or schema changed.
101    Config,
102    /// Timing envelope changed while replay semantics may still match.
103    Timing,
104    /// Core semantic behavior changed (event stream/fingerprint/prefix).
105    Semantic,
106    /// Diagnostic or oracle-surface behavior changed.
107    Observability,
108}
109
110/// Severity for replay-delta mismatch.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum GoldenTraceDeltaSeverity {
114    /// Informational drift that does not typically fail a gate.
115    Info,
116    /// Drift that should trigger review and potential gate escalation.
117    Warning,
118    /// Drift that should fail verification unless explicitly approved.
119    Error,
120}
121
122/// Single replay-delta mismatch.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct GoldenTraceDelta {
125    /// Logical drift class for this mismatch.
126    pub class: GoldenTraceDeltaClass,
127    /// Severity assigned to this mismatch.
128    pub severity: GoldenTraceDeltaSeverity,
129    /// Stable field identifier for machine parsing.
130    pub field: String,
131    /// Human-readable mismatch summary.
132    pub message: String,
133}
134
135/// Structured replay-delta report between two fixtures.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[allow(clippy::struct_excessive_bools)]
138pub struct GoldenTraceDeltaReport {
139    /// Fingerprint recorded in the baseline fixture.
140    pub expected_fingerprint: u64,
141    /// Fingerprint recorded in the candidate fixture.
142    pub actual_fingerprint: u64,
143    /// Event count from the baseline fixture.
144    pub expected_event_count: u64,
145    /// Event count from the candidate fixture.
146    pub actual_event_count: u64,
147    /// True when schema or configuration changed.
148    pub config_drift: bool,
149    /// True when core semantic behavior changed.
150    pub semantic_drift: bool,
151    /// True when replay timing envelope changed.
152    pub timing_drift: bool,
153    /// True when oracle/diagnostic surface changed.
154    pub observability_drift: bool,
155    /// Detailed mismatch entries.
156    pub deltas: Vec<GoldenTraceDelta>,
157}
158
159impl GoldenTraceDeltaReport {
160    /// Returns true when no drift is present.
161    #[must_use]
162    pub fn is_clean(&self) -> bool {
163        self.deltas.is_empty()
164    }
165
166    /// Serialize the report to pretty JSON for CI artifacts.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if serialization fails.
171    pub fn to_json(&self) -> Result<String, serde_json::Error> {
172        serde_json::to_string_pretty(self)
173    }
174}
175
176/// Diff between two golden trace fixtures.
177#[derive(Debug, Default)]
178pub struct GoldenTraceDiff {
179    mismatches: Vec<GoldenTraceMismatch>,
180}
181
182impl GoldenTraceDiff {
183    /// Returns true if no mismatches were recorded.
184    #[must_use]
185    pub fn is_empty(&self) -> bool {
186        self.mismatches.is_empty()
187    }
188
189    fn push(&mut self, mismatch: GoldenTraceMismatch) {
190        self.mismatches.push(mismatch);
191    }
192
193    fn from_fixtures(expected: &GoldenTraceFixture, actual: &GoldenTraceFixture) -> Self {
194        let mut diff = Self::default();
195        if expected.schema_version != actual.schema_version {
196            diff.push(GoldenTraceMismatch::SchemaVersion {
197                expected: expected.schema_version,
198                actual: actual.schema_version,
199            });
200        }
201        if expected.config != actual.config {
202            diff.push(GoldenTraceMismatch::Config {
203                expected: expected.config.clone(),
204                actual: actual.config.clone(),
205            });
206        }
207        if expected.fingerprint != actual.fingerprint {
208            diff.push(GoldenTraceMismatch::Fingerprint {
209                expected: expected.fingerprint,
210                actual: actual.fingerprint,
211            });
212        }
213        if expected.event_count != actual.event_count {
214            diff.push(GoldenTraceMismatch::EventCount {
215                expected: expected.event_count,
216                actual: actual.event_count,
217            });
218        }
219        if expected.canonical_prefix != actual.canonical_prefix {
220            diff.push(GoldenTraceMismatch::CanonicalPrefix {
221                expected_layers: expected.canonical_prefix.len(),
222                actual_layers: actual.canonical_prefix.len(),
223                first_mismatch: first_prefix_mismatch(
224                    &expected.canonical_prefix,
225                    &actual.canonical_prefix,
226                ),
227            });
228        }
229        if expected.oracle_summary != actual.oracle_summary {
230            diff.push(GoldenTraceMismatch::OracleViolations {
231                expected: expected.oracle_summary.violations.clone(),
232                actual: actual.oracle_summary.violations.clone(),
233            });
234        }
235        diff
236    }
237
238    fn into_result(self) -> Result<(), Self> {
239        if self.is_empty() { Ok(()) } else { Err(self) }
240    }
241
242    /// Convert mismatch data to a structured replay-delta report.
243    #[must_use]
244    pub fn to_delta_report(
245        &self,
246        expected: &GoldenTraceFixture,
247        actual: &GoldenTraceFixture,
248    ) -> GoldenTraceDeltaReport {
249        let mut config_drift = false;
250        let mut semantic_drift = false;
251        let mut timing_drift = false;
252        let mut observability_drift = false;
253        let mut deltas = Vec::with_capacity(self.mismatches.len());
254
255        for mismatch in &self.mismatches {
256            let (class, severity, field) = classify_delta(mismatch);
257            match class {
258                GoldenTraceDeltaClass::Config => config_drift = true,
259                GoldenTraceDeltaClass::Timing => timing_drift = true,
260                GoldenTraceDeltaClass::Semantic => semantic_drift = true,
261                GoldenTraceDeltaClass::Observability => observability_drift = true,
262            }
263            deltas.push(GoldenTraceDelta {
264                class,
265                severity,
266                field: field.to_string(),
267                message: mismatch.to_string(),
268            });
269        }
270
271        GoldenTraceDeltaReport {
272            expected_fingerprint: expected.fingerprint,
273            actual_fingerprint: actual.fingerprint,
274            expected_event_count: expected.event_count,
275            actual_event_count: actual.event_count,
276            config_drift,
277            semantic_drift,
278            timing_drift,
279            observability_drift,
280            deltas,
281        }
282    }
283}
284
285fn classify_delta(
286    mismatch: &GoldenTraceMismatch,
287) -> (
288    GoldenTraceDeltaClass,
289    GoldenTraceDeltaSeverity,
290    &'static str,
291) {
292    match mismatch {
293        GoldenTraceMismatch::SchemaVersion { .. } => (
294            GoldenTraceDeltaClass::Config,
295            GoldenTraceDeltaSeverity::Error,
296            "schema_version",
297        ),
298        GoldenTraceMismatch::Config { .. } => (
299            GoldenTraceDeltaClass::Config,
300            GoldenTraceDeltaSeverity::Error,
301            "config",
302        ),
303        GoldenTraceMismatch::Fingerprint { .. } => (
304            GoldenTraceDeltaClass::Semantic,
305            GoldenTraceDeltaSeverity::Error,
306            "fingerprint",
307        ),
308        GoldenTraceMismatch::EventCount { .. } => (
309            GoldenTraceDeltaClass::Timing,
310            GoldenTraceDeltaSeverity::Warning,
311            "event_count",
312        ),
313        GoldenTraceMismatch::CanonicalPrefix { .. } => (
314            GoldenTraceDeltaClass::Semantic,
315            GoldenTraceDeltaSeverity::Error,
316            "canonical_prefix",
317        ),
318        GoldenTraceMismatch::OracleViolations { .. } => (
319            GoldenTraceDeltaClass::Observability,
320            GoldenTraceDeltaSeverity::Warning,
321            "oracle_violations",
322        ),
323    }
324}
325
326impl std::fmt::Display for GoldenTraceDiff {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        for mismatch in &self.mismatches {
329            writeln!(f, "{mismatch}")?;
330        }
331        Ok(())
332    }
333}
334
335impl std::error::Error for GoldenTraceDiff {}
336
337#[derive(Debug)]
338enum GoldenTraceMismatch {
339    SchemaVersion {
340        expected: u32,
341        actual: u32,
342    },
343    Config {
344        expected: GoldenTraceConfig,
345        actual: GoldenTraceConfig,
346    },
347    Fingerprint {
348        expected: u64,
349        actual: u64,
350    },
351    EventCount {
352        expected: u64,
353        actual: u64,
354    },
355    CanonicalPrefix {
356        expected_layers: usize,
357        actual_layers: usize,
358        first_mismatch: Option<(usize, usize)>,
359    },
360    OracleViolations {
361        expected: Vec<String>,
362        actual: Vec<String>,
363    },
364}
365
366impl std::fmt::Display for GoldenTraceMismatch {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        match self {
369            Self::SchemaVersion { expected, actual } => {
370                write!(
371                    f,
372                    "schema_version changed (expected {expected}, actual {actual})"
373                )
374            }
375            Self::Config { expected, actual } => {
376                write!(
377                    f,
378                    "config changed (expected {expected:?}, actual {actual:?})"
379                )
380            }
381            Self::Fingerprint { expected, actual } => {
382                write!(
383                    f,
384                    "fingerprint changed (expected 0x{expected:016X}, actual 0x{actual:016X})"
385                )
386            }
387            Self::EventCount { expected, actual } => write!(
388                f,
389                "event_count changed (expected {expected}, actual {actual})"
390            ),
391            Self::CanonicalPrefix {
392                expected_layers,
393                actual_layers,
394                first_mismatch,
395            } => {
396                if let Some((layer, index)) = first_mismatch {
397                    write!(
398                        f,
399                        "canonical_prefix mismatch (layer {layer}, index {index}; expected_layers={expected_layers}, actual_layers={actual_layers})"
400                    )
401                } else {
402                    write!(
403                        f,
404                        "canonical_prefix mismatch (expected_layers={expected_layers}, actual_layers={actual_layers})"
405                    )
406                }
407            }
408            Self::OracleViolations { expected, actual } => {
409                write!(
410                    f,
411                    "oracle violations changed (expected {expected:?}, actual {actual:?})"
412                )
413            }
414        }
415    }
416}
417
418fn canonical_prefix(
419    events: &[TraceEvent],
420    max_layers: usize,
421    max_events: usize,
422) -> Vec<Vec<TraceEventKey>> {
423    let foata = canonicalize(events);
424    let mut remaining = max_events;
425    let mut prefix = Vec::new();
426
427    for layer in foata.layers().iter().take(max_layers) {
428        if remaining == 0 {
429            break;
430        }
431        let mut keys = Vec::new();
432        for event in layer {
433            if remaining == 0 {
434                break;
435            }
436            keys.push(trace_event_key(event));
437            remaining = remaining.saturating_sub(1);
438        }
439        if !keys.is_empty() {
440            prefix.push(keys);
441        }
442    }
443
444    prefix
445}
446
447fn first_prefix_mismatch(
448    expected: &[Vec<TraceEventKey>],
449    actual: &[Vec<TraceEventKey>],
450) -> Option<(usize, usize)> {
451    let layers = expected.len().min(actual.len());
452    for layer_idx in 0..layers {
453        let expected_layer = &expected[layer_idx];
454        let actual_layer = &actual[layer_idx];
455        let events = expected_layer.len().min(actual_layer.len());
456        for event_idx in 0..events {
457            if expected_layer[event_idx] != actual_layer[event_idx] {
458                return Some((layer_idx, event_idx));
459            }
460        }
461        if expected_layer.len() != actual_layer.len() {
462            return Some((layer_idx, events));
463        }
464    }
465    if expected.len() != actual.len() {
466        return Some((layers, 0));
467    }
468    None
469}
470
471/// Formats a trace buffer as human-readable text.
472pub fn format_trace(buffer: &TraceBuffer, w: &mut impl Write) -> io::Result<()> {
473    writeln!(w, "=== Trace ({} events) ===", buffer.len())?;
474    for event in buffer.iter() {
475        writeln!(w, "{event}")?;
476    }
477    writeln!(w, "=== End Trace ===")?;
478    Ok(())
479}
480
481/// Formats a trace buffer as a string.
482#[must_use]
483pub fn trace_to_string(buffer: &TraceBuffer) -> String {
484    let mut s = Vec::new();
485    format_trace(buffer, &mut s).expect("writing to Vec should not fail");
486    String::from_utf8(s).expect("trace should be valid UTF-8")
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use crate::trace::event::{TraceData, TraceEvent, TraceEventKind};
493    use crate::types::Time;
494
495    #[test]
496    fn format_empty_trace() {
497        let buffer = TraceBuffer::new(10);
498        let output = trace_to_string(&buffer);
499        assert!(output.contains("0 events"));
500    }
501
502    #[test]
503    fn format_with_events() {
504        let mut buffer = TraceBuffer::new(10);
505        buffer.push(TraceEvent::new(
506            1,
507            Time::from_millis(100),
508            TraceEventKind::UserTrace,
509            TraceData::Message("test".to_string()),
510        ));
511        let output = trace_to_string(&buffer);
512        assert!(output.contains("1 events"));
513        assert!(output.contains("test"));
514    }
515
516    // Pure data-type tests (wave 14 – CyanBarn)
517
518    #[test]
519    fn golden_trace_config_debug_clone_eq() {
520        let cfg = GoldenTraceConfig {
521            seed: 42,
522            entropy_seed: 7,
523            worker_count: 4,
524            trace_capacity: 1000,
525            max_steps: Some(500),
526            canonical_prefix_layers: 10,
527            canonical_prefix_events: 100,
528        };
529        let dbg = format!("{cfg:?}");
530        assert!(dbg.contains("GoldenTraceConfig"));
531
532        let cloned = cfg.clone();
533        assert_eq!(cfg, cloned);
534    }
535
536    #[test]
537    fn golden_trace_config_ne() {
538        let a = GoldenTraceConfig {
539            seed: 1,
540            entropy_seed: 0,
541            worker_count: 1,
542            trace_capacity: 10,
543            max_steps: None,
544            canonical_prefix_layers: 1,
545            canonical_prefix_events: 1,
546        };
547        let mut b = a.clone();
548        b.seed = 2;
549        assert_ne!(a, b);
550    }
551
552    #[test]
553    fn golden_trace_oracle_summary_debug_clone_eq() {
554        let summary = GoldenTraceOracleSummary {
555            violations: vec!["leak".to_string()],
556        };
557        let dbg = format!("{summary:?}");
558        assert!(dbg.contains("GoldenTraceOracleSummary"));
559
560        let cloned = summary.clone();
561        assert_eq!(summary, cloned);
562    }
563
564    #[test]
565    fn golden_trace_oracle_summary_empty() {
566        let summary = GoldenTraceOracleSummary { violations: vec![] };
567        assert!(summary.violations.is_empty());
568    }
569
570    #[test]
571    fn golden_trace_diff_default_is_empty() {
572        let diff = GoldenTraceDiff::default();
573        assert!(diff.is_empty());
574    }
575
576    #[test]
577    fn golden_trace_diff_debug() {
578        let diff = GoldenTraceDiff::default();
579        let dbg = format!("{diff:?}");
580        assert!(dbg.contains("GoldenTraceDiff"));
581    }
582
583    #[test]
584    fn golden_trace_diff_display_empty() {
585        let diff = GoldenTraceDiff::default();
586        let display = diff.to_string();
587        assert!(display.is_empty());
588    }
589
590    #[test]
591    fn golden_trace_diff_error_trait() {
592        let diff = GoldenTraceDiff::default();
593        let err: &dyn std::error::Error = &diff;
594        assert!(err.source().is_none());
595    }
596
597    #[test]
598    fn golden_trace_mismatch_display_all_variants() {
599        let m = GoldenTraceMismatch::SchemaVersion {
600            expected: 1,
601            actual: 2,
602        };
603        assert!(m.to_string().contains("schema_version"));
604
605        let m = GoldenTraceMismatch::Fingerprint {
606            expected: 0xAB,
607            actual: 0xCD,
608        };
609        assert!(m.to_string().contains("fingerprint"));
610
611        let m = GoldenTraceMismatch::EventCount {
612            expected: 10,
613            actual: 20,
614        };
615        assert!(m.to_string().contains("event_count"));
616
617        let m = GoldenTraceMismatch::CanonicalPrefix {
618            expected_layers: 3,
619            actual_layers: 5,
620            first_mismatch: Some((1, 2)),
621        };
622        let s = m.to_string();
623        assert!(s.contains("canonical_prefix"));
624        assert!(s.contains("layer 1"));
625
626        let m = GoldenTraceMismatch::CanonicalPrefix {
627            expected_layers: 3,
628            actual_layers: 5,
629            first_mismatch: None,
630        };
631        assert!(m.to_string().contains("expected_layers=3"));
632
633        let m = GoldenTraceMismatch::OracleViolations {
634            expected: vec!["a".into()],
635            actual: vec!["b".into()],
636        };
637        assert!(m.to_string().contains("oracle violations"));
638    }
639
640    #[test]
641    fn golden_trace_mismatch_config_variant() {
642        let cfg1 = GoldenTraceConfig {
643            seed: 1,
644            entropy_seed: 0,
645            worker_count: 1,
646            trace_capacity: 10,
647            max_steps: None,
648            canonical_prefix_layers: 1,
649            canonical_prefix_events: 1,
650        };
651        let cfg2 = GoldenTraceConfig { seed: 2, ..cfg1 };
652        let m = GoldenTraceMismatch::Config {
653            expected: cfg1,
654            actual: cfg2,
655        };
656        assert!(m.to_string().contains("config changed"));
657    }
658
659    #[test]
660    fn golden_trace_mismatch_debug() {
661        let m = GoldenTraceMismatch::SchemaVersion {
662            expected: 1,
663            actual: 2,
664        };
665        let dbg = format!("{m:?}");
666        assert!(dbg.contains("SchemaVersion"));
667    }
668
669    #[test]
670    fn schema_version_constant() {
671        assert_eq!(GOLDEN_TRACE_SCHEMA_VERSION, 1);
672    }
673
674    #[test]
675    fn first_prefix_mismatch_identical() {
676        let a: Vec<Vec<TraceEventKey>> = vec![];
677        assert!(first_prefix_mismatch(&a, &a).is_none());
678    }
679
680    #[test]
681    fn first_prefix_mismatch_different_lengths() {
682        let a: Vec<Vec<TraceEventKey>> = vec![vec![]];
683        let b: Vec<Vec<TraceEventKey>> = vec![];
684        let m = first_prefix_mismatch(&a, &b);
685        assert!(m.is_some());
686    }
687
688    #[test]
689    fn golden_trace_delta_report_clean_when_equal() {
690        let config = GoldenTraceConfig {
691            seed: 1,
692            entropy_seed: 1,
693            worker_count: 1,
694            trace_capacity: 32,
695            max_steps: Some(128),
696            canonical_prefix_layers: 2,
697            canonical_prefix_events: 8,
698        };
699        let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
700        let report = expected.delta_report(&expected);
701        assert!(report.is_clean());
702        assert!(!report.config_drift);
703        assert!(!report.semantic_drift);
704        assert!(!report.timing_drift);
705        assert!(!report.observability_drift);
706        assert!(report.to_json().expect("json").contains("\"deltas\""));
707    }
708
709    #[test]
710    fn golden_trace_delta_report_detects_drift_classes() {
711        let config = GoldenTraceConfig {
712            seed: 1,
713            entropy_seed: 1,
714            worker_count: 1,
715            trace_capacity: 32,
716            max_steps: Some(128),
717            canonical_prefix_layers: 2,
718            canonical_prefix_events: 8,
719        };
720        let expected = GoldenTraceFixture::from_events(config, &[], std::iter::empty::<String>());
721        let mut actual = expected.clone();
722        actual.config.seed = 2;
723        actual.fingerprint ^= 0xA5A5;
724        actual.event_count = actual.event_count.saturating_add(1);
725        actual.oracle_summary.violations = vec!["TaskLeak".to_string()];
726
727        let report = expected.delta_report(&actual);
728        assert!(!report.is_clean());
729        assert!(report.config_drift);
730        assert!(report.semantic_drift);
731        assert!(report.timing_drift);
732        assert!(report.observability_drift);
733        assert!(report.deltas.iter().any(|d| d.field == "config"));
734        assert!(report.deltas.iter().any(|d| d.field == "fingerprint"));
735        assert!(report.deltas.iter().any(|d| d.field == "event_count"));
736        assert!(report.deltas.iter().any(|d| d.field == "oracle_violations"));
737    }
738}