Skip to main content

asupersync/trace/
crashpack.rs

1//! Deterministic crash pack format for Spork failures.
2//!
3//! Crash packs are **repro artifacts**, not logs. They capture the minimal
4//! information needed to reproduce a concurrency bug under `LabRuntime`:
5//!
6//! - Deterministic seed + configuration snapshot
7//! - Canonical trace fingerprint
8//! - Minimal divergent prefix (if available)
9//! - Evidence ledger snapshot for key supervision/registry decisions
10//!
11//! # Format Goals
12//!
13//! - **Self-contained**: a crash pack plus the code at the pinned commit is
14//!   sufficient to reproduce the failure.
15//! - **Deterministic**: two crash packs from the same failure are byte-equal
16//!   (modulo wall-clock `created_at`).
17//! - **Versioned**: schema version for forward compatibility.
18//! - **Compact**: trace prefix is bounded; full trace is referenced, not inlined.
19//!
20//! # Example
21//!
22//! ```ignore
23//! use asupersync::trace::crashpack::{CrashPack, CrashPackConfig, FailureInfo, FailureOutcome};
24//! use asupersync::types::{TaskId, RegionId, Time};
25//!
26//! let pack = CrashPack::builder(CrashPackConfig {
27//!     seed: 42,
28//!     config_hash: 0xDEAD,
29//!     ..Default::default()
30//! })
31//! .failure(FailureInfo {
32//!     task: TaskId::testing_default(),
33//!     region: RegionId::testing_default(),
34//!     outcome: FailureOutcome::Panicked { message: "oops".to_string() },
35//!     virtual_time: Time::from_secs(5),
36//! })
37//! .fingerprint(0xCAFE_BABE)
38//! .build()
39//! .expect("crash pack builder should have failure metadata");
40//!
41//! assert_eq!(pack.manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
42//! ```
43//!
44//! # Bead
45//!
46//! bd-2md12 | Parent: bd-qbcnu
47
48use crate::trace::canonicalize::{TraceEventKey, canonicalize, trace_event_key, trace_fingerprint};
49use crate::trace::event::TraceEvent;
50use crate::trace::replay::ReplayEvent;
51use crate::trace::scoring::EvidenceEntry;
52use crate::types::{CancelKind, RegionId, TaskId, Time};
53use serde::{Deserialize, Serialize};
54use std::fmt;
55
56// =============================================================================
57// Schema Version
58// =============================================================================
59
60/// Current schema version for crash packs.
61///
62/// Increment when making breaking changes to the format.
63pub const CRASHPACK_SCHEMA_VERSION: u32 = 1;
64
65// =============================================================================
66// Configuration Snapshot
67// =============================================================================
68
69/// Minimal configuration snapshot embedded in a crash pack.
70///
71/// Captures the deterministic parameters needed to reproduce the execution.
72/// Together with the code at `commit_hash`, this is sufficient to set up
73/// a `LabRuntime` that replays the same schedule.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct CrashPackConfig {
76    /// Deterministic seed for the `LabRuntime` scheduler.
77    pub seed: u64,
78
79    /// Hash of the runtime configuration (for compatibility checking).
80    ///
81    /// If this differs when replaying, the reproduction may not match.
82    pub config_hash: u64,
83
84    /// Number of virtual workers in the lab runtime.
85    pub worker_count: usize,
86
87    /// Maximum scheduler steps before forced termination (if any).
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub max_steps: Option<u64>,
90
91    /// Git commit hash (hex) of the code that produced this crash pack.
92    ///
93    /// Optional; when present, allows exact code checkout for reproduction.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub commit_hash: Option<String>,
96}
97
98impl Default for CrashPackConfig {
99    fn default() -> Self {
100        Self {
101            seed: 0,
102            config_hash: 0,
103            worker_count: 1,
104            max_steps: None,
105            commit_hash: None,
106        }
107    }
108}
109
110// =============================================================================
111// Failure Info
112// =============================================================================
113
114/// Description of the triggering failure.
115///
116/// Captures which task failed, where, and what the outcome was.
117#[derive(Debug, Clone, Serialize)]
118pub struct FailureInfo {
119    /// The task that failed.
120    pub task: TaskId,
121
122    /// The region containing the failed task.
123    pub region: RegionId,
124
125    /// The failure outcome.
126    pub outcome: FailureOutcome,
127
128    /// Virtual time at which the failure was observed.
129    pub virtual_time: Time,
130}
131
132impl PartialEq for FailureInfo {
133    fn eq(&self, other: &Self) -> bool {
134        self.task == other.task
135            && self.region == other.region
136            && self.outcome == other.outcome
137            && self.virtual_time == other.virtual_time
138    }
139}
140
141impl Eq for FailureInfo {}
142
143/// Minimal failure outcome for crash packs.
144///
145/// This is intentionally smaller than [`crate::types::Outcome`]. Crash packs are repro
146/// artifacts, so we only record the deterministic summary needed for debugging.
147#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
148pub enum FailureOutcome {
149    /// Application error.
150    Err,
151    /// Cancelled, recording only the cancellation kind.
152    Cancelled {
153        /// The kind of cancellation.
154        cancel_kind: CancelKind,
155    },
156    /// Panicked, recording only the panic message.
157    Panicked {
158        /// The panic message.
159        message: String,
160    },
161}
162
163/// Serializable snapshot of an [`EvidenceEntry`] for crash packs.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct EvidenceEntrySnapshot {
166    /// Birth column index in the boundary matrix.
167    pub birth: usize,
168    /// Death column index (or `usize::MAX` for unpaired/infinite classes).
169    pub death: usize,
170    /// Whether this class is novel (not seen before).
171    pub is_novel: bool,
172    /// Persistence interval length (None = infinite).
173    pub persistence: Option<u64>,
174}
175
176impl From<EvidenceEntry> for EvidenceEntrySnapshot {
177    fn from(e: EvidenceEntry) -> Self {
178        Self {
179            birth: e.class.birth,
180            death: e.class.death,
181            is_novel: e.is_novel,
182            persistence: e.persistence,
183        }
184    }
185}
186
187// =============================================================================
188// Supervision Decision Snapshot
189// =============================================================================
190
191/// Snapshot of a supervision decision captured in the crash pack.
192///
193/// Records what the supervisor decided and why, providing the "evidence
194/// ledger" for debugging supervision chain behavior.
195#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
196pub struct SupervisionSnapshot {
197    /// Virtual time when the decision was made.
198    pub virtual_time: Time,
199
200    /// The task involved in the decision.
201    pub task: TaskId,
202
203    /// The region containing the task.
204    pub region: RegionId,
205
206    /// Human-readable decision tag (e.g., "restart", "stop", "escalate").
207    pub decision: String,
208
209    /// Additional context (e.g., "attempt 3 of 5", "budget exhausted").
210    pub context: Option<String>,
211}
212
213// =============================================================================
214// Crash Pack Manifest (bd-35u33)
215// =============================================================================
216
217/// Minimum schema version this code can read.
218///
219/// Crash packs with `schema_version < MINIMUM_SUPPORTED_SCHEMA_VERSION` are
220/// rejected during validation.
221pub const MINIMUM_SUPPORTED_SCHEMA_VERSION: u32 = 1;
222
223/// The kind of content described by a [`ManifestAttachment`].
224///
225/// Known kinds get first-class enum variants for type-safe matching.
226/// Unknown or user-defined content uses `Custom`.
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(tag = "kind")]
229pub enum AttachmentKind {
230    /// Canonical trace prefix (Foata layers).
231    CanonicalPrefix,
232    /// Minimal divergent replay prefix.
233    DivergentPrefix,
234    /// Evidence ledger entries.
235    EvidenceLedger,
236    /// Supervision decision log.
237    SupervisionLog,
238    /// Oracle violation list.
239    OracleViolations,
240    /// User-defined or future attachment type.
241    Custom {
242        /// Free-form type tag.
243        tag: String,
244    },
245}
246
247/// Describes one attachment in the crash pack.
248///
249/// The manifest carries an attachment list so that tooling can inspect
250/// what a crash pack contains without deserializing the full payload.
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct ManifestAttachment {
253    /// What kind of content this attachment holds.
254    #[serde(flatten)]
255    pub kind: AttachmentKind,
256
257    /// Number of top-level items (events, entries, layers, etc.).
258    pub item_count: u64,
259
260    /// Approximate serialized size in bytes (0 if unknown).
261    #[serde(default, skip_serializing_if = "is_zero")]
262    pub size_hint_bytes: u64,
263}
264
265// serde expects `skip_serializing_if` predicates to take `&T`.
266#[allow(clippy::trivially_copy_pass_by_ref)] // serde skip_serializing_if requires &T
267fn is_zero(v: &u64) -> bool {
268    *v == 0
269}
270
271/// The crash pack manifest: top-level metadata and structural summary.
272///
273/// The manifest is the first thing read when opening a crash pack. It
274/// provides enough information to:
275/// 1. Check version compatibility
276/// 2. Identify the failure at a glance
277/// 3. Locate the detailed trace data
278/// 4. Enumerate attachments without full deserialization
279///
280/// # Schema Versioning
281///
282/// The `schema_version` field enables forward compatibility. Use
283/// [`validate()`](CrashPackManifest::validate) before processing a crash pack
284/// to ensure the current code can interpret it correctly.
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub struct CrashPackManifest {
287    /// Schema version for forward compatibility.
288    pub schema_version: u32,
289
290    /// Configuration snapshot for reproduction.
291    pub config: CrashPackConfig,
292
293    /// Canonical trace fingerprint (deterministic hash of the full trace).
294    ///
295    /// Two crash packs with the same fingerprint represent the same failure
296    /// modulo configuration.
297    pub fingerprint: u64,
298
299    /// Total number of trace events in the execution.
300    pub event_count: u64,
301
302    /// Wall-clock timestamp when the crash pack was created (Unix epoch nanos).
303    pub created_at: u64,
304
305    /// Attachment table of contents.
306    ///
307    /// Lists the sections present in this crash pack so tooling can
308    /// discover content without deserializing the full payload.
309    #[serde(default, skip_serializing_if = "Vec::is_empty")]
310    pub attachments: Vec<ManifestAttachment>,
311}
312
313/// Errors from manifest schema validation.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub enum ManifestValidationError {
316    /// Schema version is newer than what this code supports.
317    VersionTooNew {
318        /// The manifest's schema version.
319        manifest_version: u32,
320        /// The maximum version this code supports.
321        supported_version: u32,
322    },
323    /// Schema version is older than the minimum this code can read.
324    VersionTooOld {
325        /// The manifest's schema version.
326        manifest_version: u32,
327        /// The minimum version this code requires.
328        minimum_version: u32,
329    },
330}
331
332impl std::fmt::Display for ManifestValidationError {
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        match self {
335            Self::VersionTooNew {
336                manifest_version,
337                supported_version,
338            } => write!(
339                f,
340                "crash pack schema v{manifest_version} is newer than supported v{supported_version}"
341            ),
342            Self::VersionTooOld {
343                manifest_version,
344                minimum_version,
345            } => write!(
346                f,
347                "crash pack schema v{manifest_version} is older than minimum v{minimum_version}"
348            ),
349        }
350    }
351}
352
353impl std::error::Error for ManifestValidationError {}
354
355impl CrashPackManifest {
356    /// Create a new manifest with the given config and fingerprint.
357    #[must_use]
358    pub fn new(config: CrashPackConfig, fingerprint: u64, event_count: u64) -> Self {
359        Self {
360            schema_version: CRASHPACK_SCHEMA_VERSION,
361            config,
362            fingerprint,
363            event_count,
364            created_at: wall_clock_nanos(),
365            attachments: Vec::new(),
366        }
367    }
368
369    /// Validate that this manifest's schema version is compatible with the
370    /// current code.
371    ///
372    /// Returns `Ok(())` if `MINIMUM_SUPPORTED_SCHEMA_VERSION <= schema_version <= CRASHPACK_SCHEMA_VERSION`.
373    pub fn validate(&self) -> Result<(), ManifestValidationError> {
374        if self.schema_version > CRASHPACK_SCHEMA_VERSION {
375            return Err(ManifestValidationError::VersionTooNew {
376                manifest_version: self.schema_version,
377                supported_version: CRASHPACK_SCHEMA_VERSION,
378            });
379        }
380        if self.schema_version < MINIMUM_SUPPORTED_SCHEMA_VERSION {
381            return Err(ManifestValidationError::VersionTooOld {
382                manifest_version: self.schema_version,
383                minimum_version: MINIMUM_SUPPORTED_SCHEMA_VERSION,
384            });
385        }
386        Ok(())
387    }
388
389    /// Returns `true` if this manifest's schema version is compatible.
390    #[must_use]
391    pub fn is_compatible(&self) -> bool {
392        self.validate().is_ok()
393    }
394
395    /// Look up an attachment by kind.
396    #[must_use]
397    pub fn attachment(&self, kind: &AttachmentKind) -> Option<&ManifestAttachment> {
398        self.attachments.iter().find(|a| &a.kind == kind)
399    }
400
401    /// Returns `true` if the manifest lists an attachment of the given kind.
402    #[must_use]
403    pub fn has_attachment(&self, kind: &AttachmentKind) -> bool {
404        self.attachment(kind).is_some()
405    }
406}
407
408// =============================================================================
409// Crash Pack
410// =============================================================================
411
412/// A complete crash pack: a self-contained repro artifact for a Spork failure.
413///
414/// # Structure
415///
416/// ```text
417/// CrashPack
418/// ├── manifest          — version, config, fingerprint, event count
419/// ├── failure           — triggering failure (task, region, outcome, vt)
420/// ├── canonical_prefix  — Foata layers of the trace prefix (deterministic)
421/// ├── divergent_prefix  — minimal replay prefix to reach the divergence point
422/// ├── evidence          — evidence ledger entries (supervision/registry decisions)
423/// ├── supervision_log   — supervision decision snapshots
424/// └── oracle_violations — invariant violations detected by oracles
425/// ```
426///
427/// # Determinism
428///
429/// All fields except `manifest.created_at` are deterministic: given the same
430/// seed, config, and code, the same crash pack is produced.
431#[derive(Debug, Clone, Serialize)]
432pub struct CrashPack {
433    /// Top-level manifest with version, config, and fingerprint.
434    pub manifest: CrashPackManifest,
435
436    /// The triggering failure.
437    pub failure: FailureInfo,
438
439    /// Canonicalized trace prefix (Foata normal form layers of event keys).
440    ///
441    /// Bounded to avoid unbounded growth; the number of layers and events
442    /// per layer are configurable at creation time.
443    pub canonical_prefix: Vec<Vec<TraceEventKey>>,
444
445    /// Minimal divergent prefix: the shortest replay event sequence that
446    /// reaches the failure point.
447    ///
448    /// This is the primary repro artifact. Feed it to `TraceReplayer` to
449    /// step through the execution up to the failure.
450    pub divergent_prefix: Vec<ReplayEvent>,
451
452    /// Evidence ledger entries capturing key runtime decisions.
453    ///
454    /// These are the "proof" entries from the scoring/evidence system
455    /// that document why the runtime made particular choices.
456    pub evidence: Vec<EvidenceEntrySnapshot>,
457
458    /// Supervision decision log leading up to the failure.
459    ///
460    /// Ordered by virtual time; captures the chain of restart/stop/escalate
461    /// decisions that preceded (or caused) the failure.
462    pub supervision_log: Vec<SupervisionSnapshot>,
463
464    /// Oracle invariant violations detected during the execution.
465    ///
466    /// Sorted and deduplicated. Empty if all invariants held.
467    pub oracle_violations: Vec<String>,
468
469    /// Verbatim replay command for reproducing this failure.
470    ///
471    /// When present, this can be copy-pasted into a shell to replay the
472    /// exact execution that produced this crash pack.
473    #[serde(default, skip_serializing_if = "Option::is_none")]
474    pub replay: Option<ReplayCommand>,
475}
476
477impl PartialEq for CrashPack {
478    fn eq(&self, other: &Self) -> bool {
479        // Equality ignores created_at (wall clock) per determinism contract
480        self.manifest.schema_version == other.manifest.schema_version
481            && self.manifest.config == other.manifest.config
482            && self.manifest.fingerprint == other.manifest.fingerprint
483            && self.manifest.event_count == other.manifest.event_count
484            && self.manifest.attachments == other.manifest.attachments
485            && self.failure == other.failure
486            && self.canonical_prefix == other.canonical_prefix
487            && self.divergent_prefix == other.divergent_prefix
488            && self.evidence == other.evidence
489            && self.supervision_log == other.supervision_log
490            && self.oracle_violations == other.oracle_violations
491            && self.replay == other.replay
492    }
493}
494
495impl Eq for CrashPack {}
496
497impl CrashPack {
498    /// Start building a crash pack with the given configuration.
499    #[must_use]
500    pub fn builder(config: CrashPackConfig) -> CrashPackBuilder {
501        CrashPackBuilder {
502            config,
503            failure: None,
504            fingerprint: 0,
505            event_count: 0,
506            canonical_prefix: Vec::new(),
507            divergent_prefix: Vec::new(),
508            evidence: Vec::new(),
509            supervision_log: Vec::new(),
510            oracle_violations: Vec::new(),
511            replay: None,
512        }
513    }
514
515    /// Generate a replay command from this crash pack's configuration.
516    ///
517    /// This is a convenience method equivalent to
518    /// `ReplayCommand::from_config(&pack.manifest.config, artifact_path)`.
519    #[must_use]
520    pub fn replay_command(&self, artifact_path: Option<&str>) -> ReplayCommand {
521        ReplayCommand::from_config(&self.manifest.config, artifact_path)
522    }
523
524    /// Returns `true` if any oracle violations were detected.
525    #[must_use]
526    pub fn has_violations(&self) -> bool {
527        !self.oracle_violations.is_empty()
528    }
529
530    /// Returns `true` if a divergent prefix is available for replay.
531    #[must_use]
532    pub fn has_divergent_prefix(&self) -> bool {
533        !self.divergent_prefix.is_empty()
534    }
535
536    /// Returns the seed from the configuration.
537    #[must_use]
538    pub fn seed(&self) -> u64 {
539        self.manifest.config.seed
540    }
541
542    /// Returns the canonical trace fingerprint.
543    #[must_use]
544    pub fn fingerprint(&self) -> u64 {
545        self.manifest.fingerprint
546    }
547}
548
549// =============================================================================
550// Builder
551// =============================================================================
552
553/// Builder for constructing a [`CrashPack`] incrementally.
554///
555/// Required: `config` (provided at construction) and `failure` (via `.failure()`).
556/// All other fields have sensible defaults (empty).
557#[derive(Debug)]
558pub struct CrashPackBuilder {
559    config: CrashPackConfig,
560    failure: Option<FailureInfo>,
561    fingerprint: u64,
562    event_count: u64,
563    canonical_prefix: Vec<Vec<TraceEventKey>>,
564    divergent_prefix: Vec<ReplayEvent>,
565    evidence: Vec<EvidenceEntrySnapshot>,
566    supervision_log: Vec<SupervisionSnapshot>,
567    oracle_violations: Vec<String>,
568    replay: Option<ReplayCommand>,
569}
570
571/// Error returned when a [`CrashPackBuilder`] is incomplete.
572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
573pub enum CrashPackBuildError {
574    /// The builder did not receive the required [`FailureInfo`].
575    MissingFailure,
576}
577
578impl fmt::Display for CrashPackBuildError {
579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580        match self {
581            Self::MissingFailure => f.write_str("crash pack builder requires failure metadata"),
582        }
583    }
584}
585
586impl std::error::Error for CrashPackBuildError {}
587
588impl CrashPackBuilder {
589    /// Set the triggering failure.
590    #[must_use]
591    pub fn failure(mut self, failure: FailureInfo) -> Self {
592        self.failure = Some(failure);
593        self
594    }
595
596    /// Set the canonical trace fingerprint.
597    #[must_use]
598    pub fn fingerprint(mut self, fingerprint: u64) -> Self {
599        self.fingerprint = fingerprint;
600        self
601    }
602
603    /// Set the total event count.
604    #[must_use]
605    pub fn event_count(mut self, count: u64) -> Self {
606        self.event_count = count;
607        self
608    }
609
610    /// Populate canonical prefix, fingerprint, and event count from raw trace events.
611    ///
612    /// This is the primary integration point for the canonicalization pipeline.
613    /// It calls [`canonicalize()`] to compute the Foata normal form, extracts
614    /// [`TraceEventKey`] layers for the canonical prefix, and computes a
615    /// deterministic fingerprint via [`trace_fingerprint()`].
616    ///
617    /// Two different schedules that are equivalent modulo commutations of
618    /// independent events will produce the same fingerprint and the same
619    /// canonical prefix.
620    #[must_use]
621    pub fn from_trace(mut self, events: &[TraceEvent]) -> Self {
622        let foata = canonicalize(events);
623        self.canonical_prefix = foata
624            .layers()
625            .iter()
626            .map(|layer| layer.iter().map(trace_event_key).collect())
627            .collect();
628        self.fingerprint = trace_fingerprint(events);
629        self.event_count = events.len() as u64;
630        self
631    }
632
633    /// Set the canonical Foata prefix.
634    #[must_use]
635    pub fn canonical_prefix(mut self, prefix: Vec<Vec<TraceEventKey>>) -> Self {
636        self.canonical_prefix = prefix;
637        self
638    }
639
640    /// Set the minimal divergent prefix for replay.
641    #[must_use]
642    pub fn divergent_prefix(mut self, prefix: Vec<ReplayEvent>) -> Self {
643        self.divergent_prefix = prefix;
644        self
645    }
646
647    /// Add evidence ledger entries.
648    #[must_use]
649    pub fn evidence(mut self, entries: Vec<EvidenceEntry>) -> Self {
650        self.evidence = entries
651            .into_iter()
652            .map(EvidenceEntrySnapshot::from)
653            .collect();
654        self
655    }
656
657    /// Add a supervision decision snapshot.
658    #[must_use]
659    pub fn supervision_snapshot(mut self, snapshot: SupervisionSnapshot) -> Self {
660        self.supervision_log.push(snapshot);
661        self
662    }
663
664    /// Set oracle violations.
665    #[must_use]
666    pub fn oracle_violations(mut self, violations: Vec<String>) -> Self {
667        let mut v = violations;
668        v.sort();
669        v.dedup();
670        self.oracle_violations = v;
671        self
672    }
673
674    /// Set the replay command for reproducing this failure.
675    #[must_use]
676    pub fn replay(mut self, command: ReplayCommand) -> Self {
677        self.replay = Some(command);
678        self
679    }
680
681    /// Build the crash pack.
682    ///
683    /// The manifest's attachment list is auto-populated from the crash pack
684    /// content: non-empty sections are listed as attachments so that tooling
685    /// can inspect the table of contents without full deserialization.
686    ///
687    pub fn build(self) -> Result<CrashPack, CrashPackBuildError> {
688        let failure = self.failure.ok_or(CrashPackBuildError::MissingFailure)?;
689
690        // Sort supervision log with a total order for determinism.
691        // Equal virtual times are expected in practice; include stable
692        // secondary keys so serialization does not depend on insertion order.
693        let mut supervision_log = self.supervision_log;
694        supervision_log.sort_by(|a, b| {
695            a.virtual_time
696                .cmp(&b.virtual_time)
697                .then_with(|| a.task.cmp(&b.task))
698                .then_with(|| a.region.cmp(&b.region))
699                .then_with(|| a.decision.cmp(&b.decision))
700                .then_with(|| a.context.cmp(&b.context))
701        });
702
703        // Build attachment table of contents from non-empty sections
704        let mut attachments = Vec::new();
705        if !self.canonical_prefix.is_empty() {
706            let item_count: u64 = self
707                .canonical_prefix
708                .iter()
709                .map(|layer| layer.len() as u64)
710                .sum();
711            attachments.push(ManifestAttachment {
712                kind: AttachmentKind::CanonicalPrefix,
713                item_count,
714                size_hint_bytes: 0,
715            });
716        }
717        if !self.divergent_prefix.is_empty() {
718            attachments.push(ManifestAttachment {
719                kind: AttachmentKind::DivergentPrefix,
720                item_count: self.divergent_prefix.len() as u64,
721                size_hint_bytes: 0,
722            });
723        }
724        if !self.evidence.is_empty() {
725            attachments.push(ManifestAttachment {
726                kind: AttachmentKind::EvidenceLedger,
727                item_count: self.evidence.len() as u64,
728                size_hint_bytes: 0,
729            });
730        }
731        if !supervision_log.is_empty() {
732            attachments.push(ManifestAttachment {
733                kind: AttachmentKind::SupervisionLog,
734                item_count: supervision_log.len() as u64,
735                size_hint_bytes: 0,
736            });
737        }
738        if !self.oracle_violations.is_empty() {
739            attachments.push(ManifestAttachment {
740                kind: AttachmentKind::OracleViolations,
741                item_count: self.oracle_violations.len() as u64,
742                size_hint_bytes: 0,
743            });
744        }
745
746        let mut manifest = CrashPackManifest::new(self.config, self.fingerprint, self.event_count);
747        manifest.attachments = attachments;
748
749        Ok(CrashPack {
750            manifest,
751            failure,
752            canonical_prefix: self.canonical_prefix,
753            divergent_prefix: self.divergent_prefix,
754            evidence: self.evidence,
755            supervision_log,
756            oracle_violations: self.oracle_violations,
757            replay: self.replay,
758        })
759    }
760}
761
762// =============================================================================
763// Replay Command Contract (bd-1teda)
764// =============================================================================
765
766/// An environment variable required for deterministic replay.
767#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
768pub struct ReplayEnvVar {
769    /// Variable name (e.g., `ASUPERSYNC_SEED`).
770    pub key: String,
771    /// Variable value.
772    pub value: String,
773}
774
775/// A verbatim replay command that can reproduce the crash pack's failure.
776///
777/// The command is a fully-specified invocation that, given the same code
778/// at the recorded commit, will reproduce the exact failure.
779///
780/// # Example JSON
781///
782/// ```json
783/// {
784///   "program": "cargo",
785///   "args": ["test", "--lib", "--", "--seed", "42"],
786///   "env": [{"key": "ASUPERSYNC_WORKERS", "value": "4"}],
787///   "command_line": "ASUPERSYNC_WORKERS=4 cargo test --lib -- --seed 42"
788/// }
789/// ```
790#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
791pub struct ReplayCommand {
792    /// The binary or program to invoke.
793    pub program: String,
794
795    /// Command-line arguments, each as a separate string.
796    pub args: Vec<String>,
797
798    /// Environment variables required for replay.
799    #[serde(default, skip_serializing_if = "Vec::is_empty")]
800    pub env: Vec<ReplayEnvVar>,
801
802    /// Human-readable one-liner that can be copy-pasted into a shell.
803    ///
804    /// Includes env var prefixes, the program, and all arguments.
805    pub command_line: String,
806}
807
808impl ReplayCommand {
809    /// Build a replay command from a crash pack's configuration.
810    ///
811    /// Generates a `cargo test` invocation with the crash pack's seed
812    /// and configuration parameters.
813    #[must_use]
814    pub fn from_config(config: &CrashPackConfig, artifact_path: Option<&str>) -> Self {
815        let mut args = vec![
816            "test".to_string(),
817            "--lib".to_string(),
818            "--".to_string(),
819            "--seed".to_string(),
820            config.seed.to_string(),
821        ];
822
823        let mut env = Vec::new();
824
825        env.push(ReplayEnvVar {
826            key: "ASUPERSYNC_WORKERS".to_string(),
827            value: config.worker_count.to_string(),
828        });
829
830        if let Some(max_steps) = config.max_steps {
831            env.push(ReplayEnvVar {
832                key: "ASUPERSYNC_MAX_STEPS".to_string(),
833                value: max_steps.to_string(),
834            });
835        }
836
837        if let Some(path) = artifact_path {
838            args.push("--crashpack".to_string());
839            args.push(path.to_string());
840        }
841
842        let command_line = build_command_line("cargo", &args, &env);
843
844        Self {
845            program: "cargo".to_string(),
846            args,
847            env,
848            command_line,
849        }
850    }
851
852    /// Build a replay command for the `asupersync trace replay` CLI subcommand.
853    #[must_use]
854    pub fn from_config_cli(config: &CrashPackConfig, artifact_path: &str) -> Self {
855        let mut args = vec![
856            "trace".to_string(),
857            "replay".to_string(),
858            "--seed".to_string(),
859            config.seed.to_string(),
860            "--workers".to_string(),
861            config.worker_count.to_string(),
862        ];
863
864        if let Some(max_steps) = config.max_steps {
865            args.push("--max-steps".to_string());
866            args.push(max_steps.to_string());
867        }
868
869        args.push(artifact_path.to_string());
870
871        let command_line = build_command_line("asupersync", &args, &[]);
872
873        Self {
874            program: "asupersync".to_string(),
875            args,
876            env: Vec::new(),
877            command_line,
878        }
879    }
880}
881
882impl std::fmt::Display for ReplayCommand {
883    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
884        write!(f, "{}", self.command_line)
885    }
886}
887
888/// Build a shell-friendly command line string.
889fn build_command_line(program: &str, args: &[String], env: &[ReplayEnvVar]) -> String {
890    let mut parts = Vec::new();
891    for var in env {
892        parts.push(format!(
893            "{}={}",
894            shell_escape(&var.key),
895            shell_escape(&var.value)
896        ));
897    }
898    parts.push(program.to_string());
899    for arg in args {
900        parts.push(shell_escape(arg));
901    }
902    parts.join(" ")
903}
904
905/// Minimally escape a string for shell embedding.
906///
907/// If the string contains shell-unsafe characters, wrap it in single quotes.
908/// Otherwise, return it as-is.
909fn shell_escape(s: &str) -> String {
910    if s.is_empty() {
911        return "''".to_string();
912    }
913    if s.chars()
914        .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':' | '=' | ','))
915    {
916        s.to_string()
917    } else {
918        format!("'{}'", s.replace('\'', "'\\''"))
919    }
920}
921
922// =============================================================================
923// Artifact Writer Capability (bd-1skcu)
924// =============================================================================
925
926/// Identifier for a written crash pack artifact.
927///
928/// Returned by [`CrashPackWriter::write`] to identify where the artifact was
929/// stored. The path is deterministic: given the same seed and fingerprint, the
930/// same artifact path is produced.
931#[derive(Debug, Clone, PartialEq, Eq)]
932pub struct ArtifactId {
933    /// The full path or identifier of the written artifact.
934    path: String,
935}
936
937impl ArtifactId {
938    /// Returns the artifact path/identifier as a string.
939    #[must_use]
940    pub fn path(&self) -> &str {
941        &self.path
942    }
943}
944
945impl std::fmt::Display for ArtifactId {
946    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
947        write!(f, "{}", self.path)
948    }
949}
950
951/// Error returned when writing a crash pack fails.
952#[derive(Debug)]
953pub enum CrashPackWriteError {
954    /// Serialization failed.
955    Serialize(String),
956    /// I/O error while writing.
957    Io(std::io::Error),
958}
959
960impl std::fmt::Display for CrashPackWriteError {
961    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
962        match self {
963            Self::Serialize(msg) => write!(f, "crash pack serialization failed: {msg}"),
964            Self::Io(e) => write!(f, "crash pack I/O error: {e}"),
965        }
966    }
967}
968
969impl std::error::Error for CrashPackWriteError {
970    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
971        match self {
972            Self::Io(e) => Some(e),
973            Self::Serialize(_) => None,
974        }
975    }
976}
977
978/// Capability for writing crash packs to persistent storage.
979///
980/// This is the **only** way to persist a crash pack. There are no ambient
981/// filesystem writes — callers must hold an explicit `&dyn CrashPackWriter`
982/// to write artifacts. This follows asupersync's capability-security model.
983///
984/// # Deterministic Paths
985///
986/// Artifact paths are deterministic:
987/// `crashpack-{seed:016x}-{config_hash:016x}-{fingerprint:016x}-v{version}.json`.
988/// Two writes of the same crash pack produce the same path.
989pub trait CrashPackWriter: Send + Sync + std::fmt::Debug {
990    /// Write a crash pack, returning an [`ArtifactId`] identifying the artifact.
991    fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError>;
992
993    /// Whether this writer persists to durable storage.
994    fn is_persistent(&self) -> bool;
995
996    /// Implementation name (e.g., `"file"`, `"memory"`).
997    fn name(&self) -> &'static str;
998}
999
1000/// Compute the deterministic artifact filename for a crash pack.
1001///
1002/// Format: `crashpack-{seed:016x}-{config_hash:016x}-{fingerprint:016x}-v{version}.json`
1003#[must_use]
1004pub fn artifact_filename(pack: &CrashPack) -> String {
1005    format!(
1006        "crashpack-{:016x}-{:016x}-{:016x}-v{}.json",
1007        pack.seed(),
1008        pack.manifest.config.config_hash,
1009        pack.fingerprint(),
1010        pack.manifest.schema_version,
1011    )
1012}
1013
1014/// File-based crash pack writer.
1015///
1016/// Writes JSON crash packs to a specified directory with deterministic
1017/// filenames. The directory must exist; this writer does not create it
1018/// (explicit opt-in means the caller sets up the output directory).
1019#[derive(Debug)]
1020pub struct FileCrashPackWriter {
1021    base_dir: std::path::PathBuf,
1022}
1023
1024impl FileCrashPackWriter {
1025    /// Create a writer targeting the given directory.
1026    ///
1027    /// The directory must already exist.
1028    #[must_use]
1029    pub fn new(base_dir: std::path::PathBuf) -> Self {
1030        Self { base_dir }
1031    }
1032
1033    /// Returns the base directory for artifact output.
1034    #[must_use]
1035    pub fn base_dir(&self) -> &std::path::Path {
1036        &self.base_dir
1037    }
1038}
1039
1040impl CrashPackWriter for FileCrashPackWriter {
1041    fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError> {
1042        let filename = artifact_filename(pack);
1043        let path = self.base_dir.join(&filename);
1044
1045        let json = serde_json::to_string_pretty(pack)
1046            .map_err(|e| CrashPackWriteError::Serialize(e.to_string()))?;
1047
1048        std::fs::write(&path, json.as_bytes()).map_err(CrashPackWriteError::Io)?;
1049
1050        Ok(ArtifactId {
1051            path: path.to_string_lossy().into_owned(),
1052        })
1053    }
1054
1055    fn is_persistent(&self) -> bool {
1056        true
1057    }
1058
1059    fn name(&self) -> &'static str {
1060        "file"
1061    }
1062}
1063
1064/// In-memory crash pack writer for testing.
1065///
1066/// Collects written packs in a `Vec` behind a mutex. Not persistent.
1067#[derive(Debug, Default)]
1068pub struct MemoryCrashPackWriter {
1069    packs: parking_lot::Mutex<Vec<(ArtifactId, String)>>,
1070}
1071
1072impl MemoryCrashPackWriter {
1073    /// Create an empty in-memory writer.
1074    #[must_use]
1075    pub fn new() -> Self {
1076        Self::default()
1077    }
1078
1079    /// Returns all written packs as `(artifact_id, json)` pairs.
1080    pub fn written(&self) -> Vec<(ArtifactId, String)> {
1081        self.packs.lock().clone()
1082    }
1083
1084    /// Returns the number of packs written.
1085    #[must_use]
1086    pub fn count(&self) -> usize {
1087        self.packs.lock().len()
1088    }
1089}
1090
1091impl CrashPackWriter for MemoryCrashPackWriter {
1092    fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError> {
1093        let filename = artifact_filename(pack);
1094        let json = serde_json::to_string_pretty(pack)
1095            .map_err(|e| CrashPackWriteError::Serialize(e.to_string()))?;
1096
1097        let artifact_id = ArtifactId { path: filename };
1098        self.packs.lock().push((artifact_id.clone(), json));
1099
1100        Ok(artifact_id)
1101    }
1102
1103    fn is_persistent(&self) -> bool {
1104        false
1105    }
1106
1107    fn name(&self) -> &'static str {
1108        "memory"
1109    }
1110}
1111
1112// =============================================================================
1113// Helpers
1114// =============================================================================
1115
1116/// Get wall-clock time as nanoseconds since Unix epoch.
1117fn wall_clock_nanos() -> u64 {
1118    std::time::SystemTime::now()
1119        .duration_since(std::time::UNIX_EPOCH)
1120        .map_or(0, |d| d.as_nanos().min(u128::from(u64::MAX)) as u64)
1121}
1122
1123// =============================================================================
1124// Tests
1125// =============================================================================
1126
1127#[cfg(test)]
1128mod tests {
1129    use super::*;
1130    use crate::util::ArenaIndex;
1131
1132    fn init_test(name: &str) {
1133        crate::test_utils::init_test_logging();
1134        crate::test_phase!(name);
1135    }
1136
1137    fn tid(n: u32) -> TaskId {
1138        TaskId::from_arena(ArenaIndex::new(n, 0))
1139    }
1140
1141    fn rid(n: u32) -> RegionId {
1142        RegionId::from_arena(ArenaIndex::new(n, 0))
1143    }
1144
1145    fn sample_failure() -> FailureInfo {
1146        FailureInfo {
1147            task: tid(1),
1148            region: rid(0),
1149            outcome: FailureOutcome::Panicked {
1150                message: "test panic".to_string(),
1151            },
1152            virtual_time: Time::from_secs(5),
1153        }
1154    }
1155
1156    fn sample_config() -> CrashPackConfig {
1157        CrashPackConfig {
1158            seed: 42,
1159            config_hash: 0xDEAD,
1160            worker_count: 4,
1161            max_steps: Some(1000),
1162            commit_hash: Some("abc123".to_string()),
1163        }
1164    }
1165
1166    #[test]
1167    fn builder_missing_failure_returns_error() {
1168        init_test("builder_missing_failure_returns_error");
1169
1170        let err = CrashPack::builder(sample_config())
1171            .build()
1172            .expect_err("builder should fail closed without failure metadata");
1173
1174        assert_eq!(err, CrashPackBuildError::MissingFailure);
1175        assert_eq!(
1176            err.to_string(),
1177            "crash pack builder requires failure metadata"
1178        );
1179
1180        crate::test_complete!("builder_missing_failure_returns_error");
1181    }
1182
1183    #[test]
1184    fn schema_version_is_set() {
1185        init_test("schema_version_is_set");
1186
1187        let pack = CrashPack::builder(sample_config())
1188            .failure(sample_failure())
1189            .build()
1190            .expect("crash pack builder should have failure metadata");
1191
1192        assert_eq!(pack.manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
1193        assert_eq!(pack.manifest.schema_version, 1);
1194
1195        crate::test_complete!("schema_version_is_set");
1196    }
1197
1198    #[test]
1199    fn builder_sets_all_fields() {
1200        init_test("builder_sets_all_fields");
1201
1202        let pack = CrashPack::builder(sample_config())
1203            .failure(sample_failure())
1204            .fingerprint(0xCAFE_BABE)
1205            .event_count(500)
1206            .oracle_violations(vec!["inv-1".into(), "inv-2".into()])
1207            .build()
1208            .expect("crash pack builder should have failure metadata");
1209
1210        assert_eq!(pack.manifest.config.seed, 42);
1211        assert_eq!(pack.manifest.config.config_hash, 0xDEAD);
1212        assert_eq!(pack.manifest.config.worker_count, 4);
1213        assert_eq!(pack.manifest.config.max_steps, Some(1000));
1214        assert_eq!(pack.manifest.config.commit_hash.as_deref(), Some("abc123"));
1215        assert_eq!(pack.manifest.fingerprint, 0xCAFE_BABE);
1216        assert_eq!(pack.manifest.event_count, 500);
1217        assert_eq!(pack.failure.task, tid(1));
1218        assert_eq!(pack.failure.region, rid(0));
1219        assert_eq!(pack.failure.virtual_time, Time::from_secs(5));
1220        assert!(pack.has_violations());
1221        assert_eq!(pack.oracle_violations, vec!["inv-1", "inv-2"]);
1222        assert!(!pack.has_divergent_prefix());
1223
1224        crate::test_complete!("builder_sets_all_fields");
1225    }
1226
1227    #[test]
1228    fn default_config() {
1229        init_test("default_config");
1230
1231        let config = CrashPackConfig::default();
1232        assert_eq!(config.seed, 0);
1233        assert_eq!(config.config_hash, 0);
1234        assert_eq!(config.worker_count, 1);
1235        assert_eq!(config.max_steps, None);
1236        assert_eq!(config.commit_hash, None);
1237
1238        crate::test_complete!("default_config");
1239    }
1240
1241    #[test]
1242    fn seed_and_fingerprint_accessors() {
1243        init_test("seed_and_fingerprint_accessors");
1244
1245        let pack = CrashPack::builder(CrashPackConfig {
1246            seed: 999,
1247            ..Default::default()
1248        })
1249        .failure(sample_failure())
1250        .fingerprint(0x1234)
1251        .build()
1252        .expect("crash pack builder should have failure metadata");
1253
1254        assert_eq!(pack.seed(), 999);
1255        assert_eq!(pack.fingerprint(), 0x1234);
1256
1257        crate::test_complete!("seed_and_fingerprint_accessors");
1258    }
1259
1260    #[test]
1261    fn oracle_violations_sorted_and_deduped() {
1262        init_test("oracle_violations_sorted_and_deduped");
1263
1264        let pack = CrashPack::builder(CrashPackConfig::default())
1265            .failure(sample_failure())
1266            .oracle_violations(vec![
1267                "z-violation".into(),
1268                "a-violation".into(),
1269                "z-violation".into(), // duplicate
1270                "m-violation".into(),
1271            ])
1272            .build()
1273            .expect("crash pack builder should have failure metadata");
1274
1275        assert_eq!(
1276            pack.oracle_violations,
1277            vec!["a-violation", "m-violation", "z-violation"]
1278        );
1279
1280        crate::test_complete!("oracle_violations_sorted_and_deduped");
1281    }
1282
1283    #[test]
1284    fn supervision_log_sorted_by_vt() {
1285        init_test("supervision_log_sorted_by_vt");
1286
1287        let pack = CrashPack::builder(CrashPackConfig::default())
1288            .failure(sample_failure())
1289            .supervision_snapshot(SupervisionSnapshot {
1290                virtual_time: Time::from_secs(10),
1291                task: tid(1),
1292                region: rid(0),
1293                decision: "restart".into(),
1294                context: Some("attempt 2 of 3".into()),
1295            })
1296            .supervision_snapshot(SupervisionSnapshot {
1297                virtual_time: Time::from_secs(5),
1298                task: tid(1),
1299                region: rid(0),
1300                decision: "restart".into(),
1301                context: Some("attempt 1 of 3".into()),
1302            })
1303            .supervision_snapshot(SupervisionSnapshot {
1304                virtual_time: Time::from_secs(15),
1305                task: tid(1),
1306                region: rid(0),
1307                decision: "stop".into(),
1308                context: Some("budget exhausted".into()),
1309            })
1310            .build()
1311            .expect("crash pack builder should have failure metadata");
1312
1313        assert_eq!(pack.supervision_log.len(), 3);
1314        // Should be sorted by virtual_time
1315        assert_eq!(pack.supervision_log[0].virtual_time, Time::from_secs(5));
1316        assert_eq!(pack.supervision_log[1].virtual_time, Time::from_secs(10));
1317        assert_eq!(pack.supervision_log[2].virtual_time, Time::from_secs(15));
1318
1319        crate::test_complete!("supervision_log_sorted_by_vt");
1320    }
1321
1322    #[test]
1323    fn supervision_log_equal_vt_has_deterministic_total_order() {
1324        init_test("supervision_log_equal_vt_has_deterministic_total_order");
1325
1326        let s1 = SupervisionSnapshot {
1327            virtual_time: Time::from_secs(5),
1328            task: tid(2),
1329            region: rid(0),
1330            decision: "restart".into(),
1331            context: Some("ctx-b".into()),
1332        };
1333        let s2 = SupervisionSnapshot {
1334            virtual_time: Time::from_secs(5),
1335            task: tid(1),
1336            region: rid(0),
1337            decision: "restart".into(),
1338            context: Some("ctx-a".into()),
1339        };
1340        let s3 = SupervisionSnapshot {
1341            virtual_time: Time::from_secs(5),
1342            task: tid(1),
1343            region: rid(0),
1344            decision: "escalate".into(),
1345            context: Some("ctx-a".into()),
1346        };
1347
1348        let pack_a = CrashPack::builder(CrashPackConfig::default())
1349            .failure(sample_failure())
1350            .supervision_snapshot(s1.clone())
1351            .supervision_snapshot(s2.clone())
1352            .supervision_snapshot(s3.clone())
1353            .build()
1354            .expect("crash pack builder should have failure metadata");
1355
1356        let pack_b = CrashPack::builder(CrashPackConfig::default())
1357            .failure(sample_failure())
1358            .supervision_snapshot(s3.clone())
1359            .supervision_snapshot(s1.clone())
1360            .supervision_snapshot(s2.clone())
1361            .build()
1362            .expect("crash pack builder should have failure metadata");
1363
1364        // Same logical entries must yield identical ordering regardless of insertion order.
1365        assert_eq!(pack_a.supervision_log, pack_b.supervision_log);
1366        assert_eq!(pack_a.supervision_log, vec![s3, s2, s1]);
1367
1368        crate::test_complete!("supervision_log_equal_vt_has_deterministic_total_order");
1369    }
1370
1371    #[test]
1372    fn crash_pack_equality_ignores_created_at() {
1373        init_test("crash_pack_equality_ignores_created_at");
1374
1375        let pack1 = CrashPack::builder(sample_config())
1376            .failure(sample_failure())
1377            .fingerprint(0xABCD)
1378            .build()
1379            .expect("crash pack builder should have failure metadata");
1380
1381        // Build a second pack at a different wall-clock time
1382        let pack2 = CrashPack::builder(sample_config())
1383            .failure(sample_failure())
1384            .fingerprint(0xABCD)
1385            .build()
1386            .expect("crash pack builder should have failure metadata");
1387
1388        // created_at will differ, but equality should still hold
1389        assert_eq!(pack1, pack2);
1390
1391        crate::test_complete!("crash_pack_equality_ignores_created_at");
1392    }
1393
1394    #[test]
1395    fn crash_pack_inequality_on_different_fingerprint() {
1396        init_test("crash_pack_inequality_on_different_fingerprint");
1397
1398        let pack1 = CrashPack::builder(sample_config())
1399            .failure(sample_failure())
1400            .fingerprint(0x1111)
1401            .build()
1402            .expect("crash pack builder should have failure metadata");
1403
1404        let pack2 = CrashPack::builder(sample_config())
1405            .failure(sample_failure())
1406            .fingerprint(0x2222)
1407            .build()
1408            .expect("crash pack builder should have failure metadata");
1409
1410        assert_ne!(pack1, pack2);
1411
1412        crate::test_complete!("crash_pack_inequality_on_different_fingerprint");
1413    }
1414
1415    #[test]
1416    fn crash_pack_inequality_on_different_divergent_prefix() {
1417        init_test("crash_pack_inequality_on_different_divergent_prefix");
1418
1419        let pack1 = CrashPack::builder(sample_config())
1420            .failure(sample_failure())
1421            .fingerprint(0xABCD)
1422            .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 1 }])
1423            .build()
1424            .expect("crash pack builder should have failure metadata");
1425
1426        let pack2 = CrashPack::builder(sample_config())
1427            .failure(sample_failure())
1428            .fingerprint(0xABCD)
1429            .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 2 }])
1430            .build()
1431            .expect("crash pack builder should have failure metadata");
1432
1433        assert_ne!(pack1, pack2);
1434
1435        crate::test_complete!("crash_pack_inequality_on_different_divergent_prefix");
1436    }
1437
1438    #[test]
1439    fn empty_pack_defaults() {
1440        init_test("empty_pack_defaults");
1441
1442        let pack = CrashPack::builder(CrashPackConfig::default())
1443            .failure(sample_failure())
1444            .build()
1445            .expect("crash pack builder should have failure metadata");
1446
1447        assert!(pack.canonical_prefix.is_empty());
1448        assert!(pack.divergent_prefix.is_empty());
1449        assert!(pack.evidence.is_empty());
1450        assert!(pack.supervision_log.is_empty());
1451        assert!(pack.oracle_violations.is_empty());
1452        assert!(!pack.has_violations());
1453        assert!(!pack.has_divergent_prefix());
1454
1455        crate::test_complete!("empty_pack_defaults");
1456    }
1457
1458    #[test]
1459    fn failure_info_equality() {
1460        init_test("failure_info_equality");
1461
1462        let f1 = FailureInfo {
1463            task: tid(1),
1464            region: rid(0),
1465            outcome: FailureOutcome::Panicked {
1466                message: "a".to_string(),
1467            },
1468            virtual_time: Time::from_secs(5),
1469        };
1470        let f2 = FailureInfo {
1471            task: tid(1),
1472            region: rid(0),
1473            outcome: FailureOutcome::Err, // different outcome
1474            virtual_time: Time::from_secs(5),
1475        };
1476        // outcome participates in equality
1477        assert_ne!(f1, f2);
1478
1479        let f3 = FailureInfo {
1480            task: tid(2), // different task
1481            region: rid(0),
1482            outcome: FailureOutcome::Panicked {
1483                message: "a".to_string(),
1484            },
1485            virtual_time: Time::from_secs(5),
1486        };
1487        assert_ne!(f1, f3);
1488
1489        crate::test_complete!("failure_info_equality");
1490    }
1491
1492    #[test]
1493    fn manifest_new_sets_version() {
1494        init_test("manifest_new_sets_version");
1495
1496        let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0xBEEF, 100);
1497
1498        assert_eq!(manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
1499        assert_eq!(manifest.fingerprint, 0xBEEF);
1500        assert_eq!(manifest.event_count, 100);
1501        assert!(manifest.created_at > 0);
1502
1503        crate::test_complete!("manifest_new_sets_version");
1504    }
1505
1506    #[test]
1507    fn with_divergent_prefix() {
1508        init_test("with_divergent_prefix");
1509
1510        let prefix = vec![
1511            ReplayEvent::RngSeed { seed: 42 },
1512            ReplayEvent::TaskScheduled {
1513                task: crate::trace::replay::CompactTaskId(1),
1514                at_tick: 0,
1515            },
1516        ];
1517
1518        let pack = CrashPack::builder(CrashPackConfig::default())
1519            .failure(sample_failure())
1520            .divergent_prefix(prefix)
1521            .build()
1522            .expect("crash pack builder should have failure metadata");
1523
1524        assert!(pack.has_divergent_prefix());
1525        assert_eq!(pack.divergent_prefix.len(), 2);
1526
1527        crate::test_complete!("with_divergent_prefix");
1528    }
1529
1530    #[test]
1531    fn with_canonical_prefix() {
1532        init_test("with_canonical_prefix");
1533
1534        let layer = vec![TraceEventKey {
1535            kind: 1,
1536            primary: 0,
1537            secondary: 0,
1538            tertiary: 0,
1539        }];
1540
1541        let pack = CrashPack::builder(CrashPackConfig::default())
1542            .failure(sample_failure())
1543            .canonical_prefix(vec![layer])
1544            .build()
1545            .expect("crash pack builder should have failure metadata");
1546
1547        assert_eq!(pack.canonical_prefix.len(), 1);
1548
1549        crate::test_complete!("with_canonical_prefix");
1550    }
1551
1552    #[test]
1553    fn supervision_snapshot_with_context() {
1554        init_test("supervision_snapshot_with_context");
1555
1556        let snap = SupervisionSnapshot {
1557            virtual_time: Time::from_secs(10),
1558            task: tid(3),
1559            region: rid(1),
1560            decision: "escalate".into(),
1561            context: Some("parent region R0".into()),
1562        };
1563
1564        assert_eq!(snap.decision, "escalate");
1565        assert_eq!(snap.context.as_deref(), Some("parent region R0"));
1566
1567        crate::test_complete!("supervision_snapshot_with_context");
1568    }
1569
1570    // =================================================================
1571    // Canonicalization pipeline integration (bd-zfxio)
1572    // =================================================================
1573
1574    #[test]
1575    fn from_trace_populates_fields() {
1576        init_test("from_trace_populates_fields");
1577
1578        let events = [
1579            TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1580            TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1581            TraceEvent::complete(3, Time::ZERO, tid(1), rid(1)),
1582        ];
1583
1584        let pack = CrashPack::builder(sample_config())
1585            .failure(sample_failure())
1586            .from_trace(&events)
1587            .build()
1588            .expect("crash pack builder should have failure metadata");
1589
1590        assert_eq!(pack.manifest.event_count, 3);
1591        assert_ne!(pack.manifest.fingerprint, 0);
1592        assert!(!pack.canonical_prefix.is_empty());
1593
1594        crate::test_complete!("from_trace_populates_fields");
1595    }
1596
1597    #[test]
1598    fn from_trace_equivalent_traces_same_fingerprint() {
1599        init_test("from_trace_equivalent_traces_same_fingerprint");
1600
1601        // Two schedules that differ only in the order of independent events.
1602        // spawn(T1,R1) and spawn(T2,R2) are independent — swapping them
1603        // produces the same equivalence class.
1604        let trace_a = [
1605            TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1606            TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1607        ];
1608        let trace_b = [
1609            TraceEvent::spawn(1, Time::ZERO, tid(2), rid(2)),
1610            TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1611        ];
1612
1613        let pack_a = CrashPack::builder(sample_config())
1614            .failure(sample_failure())
1615            .from_trace(&trace_a)
1616            .build()
1617            .expect("crash pack builder should have failure metadata");
1618        let pack_b = CrashPack::builder(sample_config())
1619            .failure(sample_failure())
1620            .from_trace(&trace_b)
1621            .build()
1622            .expect("crash pack builder should have failure metadata");
1623
1624        assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
1625        assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
1626        assert_eq!(pack_a, pack_b);
1627
1628        crate::test_complete!("from_trace_equivalent_traces_same_fingerprint");
1629    }
1630
1631    #[test]
1632    fn from_trace_different_dependent_traces_different_fingerprint() {
1633        init_test("from_trace_different_dependent_traces_different_fingerprint");
1634
1635        // Same-task events in different orders produce genuinely different
1636        // causal structures (spawn→complete vs complete→spawn).
1637        let trace_a = [
1638            TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1639            TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
1640        ];
1641        let trace_b = [
1642            TraceEvent::complete(1, Time::ZERO, tid(1), rid(1)),
1643            TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1644        ];
1645
1646        let pack_a = CrashPack::builder(sample_config())
1647            .failure(sample_failure())
1648            .from_trace(&trace_a)
1649            .build()
1650            .expect("crash pack builder should have failure metadata");
1651        let pack_b = CrashPack::builder(sample_config())
1652            .failure(sample_failure())
1653            .from_trace(&trace_b)
1654            .build()
1655            .expect("crash pack builder should have failure metadata");
1656
1657        assert_ne!(pack_a.fingerprint(), pack_b.fingerprint());
1658        assert_ne!(pack_a, pack_b);
1659
1660        crate::test_complete!("from_trace_different_dependent_traces_different_fingerprint");
1661    }
1662
1663    #[test]
1664    fn from_trace_canonical_prefix_matches_foata_layers() {
1665        init_test("from_trace_canonical_prefix_matches_foata_layers");
1666
1667        let events = [
1668            TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1669            TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1670            TraceEvent::complete(3, Time::ZERO, tid(1), rid(1)),
1671            TraceEvent::complete(4, Time::ZERO, tid(2), rid(2)),
1672        ];
1673
1674        let pack = CrashPack::builder(CrashPackConfig::default())
1675            .failure(sample_failure())
1676            .from_trace(&events)
1677            .build()
1678            .expect("crash pack builder should have failure metadata");
1679
1680        // Independently compute Foata layers and compare.
1681        let foata = canonicalize(&events);
1682        let expected_prefix: Vec<Vec<TraceEventKey>> = foata
1683            .layers()
1684            .iter()
1685            .map(|layer| layer.iter().map(trace_event_key).collect())
1686            .collect();
1687
1688        assert_eq!(pack.canonical_prefix, expected_prefix);
1689
1690        crate::test_complete!("from_trace_canonical_prefix_matches_foata_layers");
1691    }
1692
1693    #[test]
1694    fn from_trace_empty_trace() {
1695        init_test("from_trace_empty_trace");
1696
1697        let pack = CrashPack::builder(CrashPackConfig::default())
1698            .failure(sample_failure())
1699            .from_trace(&[])
1700            .build()
1701            .expect("crash pack builder should have failure metadata");
1702
1703        assert!(pack.canonical_prefix.is_empty());
1704        assert_eq!(pack.manifest.event_count, 0);
1705
1706        crate::test_complete!("from_trace_empty_trace");
1707    }
1708
1709    #[test]
1710    fn from_trace_three_independent_all_permutations() {
1711        init_test("from_trace_three_independent_all_permutations");
1712
1713        // Three independent events in all 6 permutations must produce
1714        // identical crash packs (same fingerprint, same canonical prefix).
1715        let e1 = TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1));
1716        let e2 = TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2));
1717        let e3 = TraceEvent::spawn(3, Time::ZERO, tid(3), rid(3));
1718
1719        let perms: Vec<Vec<TraceEvent>> = vec![
1720            vec![e1.clone(), e2.clone(), e3.clone()],
1721            vec![e1.clone(), e3.clone(), e2.clone()],
1722            vec![e2.clone(), e1.clone(), e3.clone()],
1723            vec![e2.clone(), e3.clone(), e1.clone()],
1724            vec![e3.clone(), e1.clone(), e2.clone()],
1725            vec![e3, e2, e1],
1726        ];
1727
1728        let reference = CrashPack::builder(CrashPackConfig::default())
1729            .failure(sample_failure())
1730            .from_trace(&perms[0])
1731            .build()
1732            .expect("crash pack builder should have failure metadata");
1733
1734        for (i, perm) in perms.iter().enumerate().skip(1) {
1735            let pack = CrashPack::builder(CrashPackConfig::default())
1736                .failure(sample_failure())
1737                .from_trace(perm)
1738                .build()
1739                .expect("crash pack builder should have failure metadata");
1740            assert_eq!(
1741                pack.fingerprint(),
1742                reference.fingerprint(),
1743                "permutation {i} has different fingerprint"
1744            );
1745            assert_eq!(
1746                pack.canonical_prefix, reference.canonical_prefix,
1747                "permutation {i} has different canonical prefix"
1748            );
1749        }
1750
1751        crate::test_complete!("from_trace_three_independent_all_permutations");
1752    }
1753
1754    #[test]
1755    fn from_trace_diamond_dependency() {
1756        init_test("from_trace_diamond_dependency");
1757
1758        // Region create → two independent spawns → two independent completes.
1759        // Swapping the independent pairs must produce the same crash pack.
1760        let trace_a = [
1761            TraceEvent::region_created(1, Time::ZERO, rid(1), None),
1762            TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1763            TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
1764            TraceEvent::complete(4, Time::ZERO, tid(1), rid(1)),
1765            TraceEvent::complete(5, Time::ZERO, tid(2), rid(1)),
1766        ];
1767        let trace_b = [
1768            TraceEvent::region_created(1, Time::ZERO, rid(1), None),
1769            TraceEvent::spawn(2, Time::ZERO, tid(2), rid(1)),
1770            TraceEvent::spawn(3, Time::ZERO, tid(1), rid(1)),
1771            TraceEvent::complete(4, Time::ZERO, tid(2), rid(1)),
1772            TraceEvent::complete(5, Time::ZERO, tid(1), rid(1)),
1773        ];
1774
1775        let pack_a = CrashPack::builder(sample_config())
1776            .failure(sample_failure())
1777            .from_trace(&trace_a)
1778            .build()
1779            .expect("crash pack builder should have failure metadata");
1780        let pack_b = CrashPack::builder(sample_config())
1781            .failure(sample_failure())
1782            .from_trace(&trace_b)
1783            .build()
1784            .expect("crash pack builder should have failure metadata");
1785
1786        assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
1787        assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
1788        // 3 layers: region_create | spawn×2 | complete×2
1789        assert_eq!(pack_a.canonical_prefix.len(), 3);
1790
1791        crate::test_complete!("from_trace_diamond_dependency");
1792    }
1793
1794    // =================================================================
1795    // Artifact Writer Capability (bd-1skcu)
1796    // =================================================================
1797
1798    #[test]
1799    fn artifact_filename_is_deterministic() {
1800        init_test("artifact_filename_is_deterministic");
1801
1802        let pack = CrashPack::builder(CrashPackConfig {
1803            seed: 42,
1804            ..Default::default()
1805        })
1806        .failure(sample_failure())
1807        .fingerprint(0xCAFE_BABE)
1808        .build()
1809        .expect("crash pack builder should have failure metadata");
1810
1811        let name1 = artifact_filename(&pack);
1812        let name2 = artifact_filename(&pack);
1813        assert_eq!(name1, name2);
1814        assert_eq!(
1815            name1,
1816            "crashpack-000000000000002a-0000000000000000-00000000cafebabe-v1.json"
1817        );
1818
1819        crate::test_complete!("artifact_filename_is_deterministic");
1820    }
1821
1822    #[test]
1823    fn artifact_filename_varies_by_seed_and_fingerprint() {
1824        init_test("artifact_filename_varies_by_seed_and_fingerprint");
1825
1826        let pack_a = CrashPack::builder(CrashPackConfig {
1827            seed: 1,
1828            ..Default::default()
1829        })
1830        .failure(sample_failure())
1831        .fingerprint(0xAAAA)
1832        .build()
1833        .expect("crash pack builder should have failure metadata");
1834
1835        let pack_b = CrashPack::builder(CrashPackConfig {
1836            seed: 2,
1837            ..Default::default()
1838        })
1839        .failure(sample_failure())
1840        .fingerprint(0xBBBB)
1841        .build()
1842        .expect("crash pack builder should have failure metadata");
1843
1844        assert_ne!(artifact_filename(&pack_a), artifact_filename(&pack_b));
1845
1846        crate::test_complete!("artifact_filename_varies_by_seed_and_fingerprint");
1847    }
1848
1849    #[test]
1850    fn artifact_filename_varies_by_config_hash() {
1851        init_test("artifact_filename_varies_by_config_hash");
1852
1853        let pack_a = CrashPack::builder(CrashPackConfig {
1854            seed: 42,
1855            config_hash: 0xAAAA,
1856            ..Default::default()
1857        })
1858        .failure(sample_failure())
1859        .fingerprint(0x1234)
1860        .build()
1861        .expect("crash pack builder should have failure metadata");
1862
1863        let pack_b = CrashPack::builder(CrashPackConfig {
1864            seed: 42,
1865            config_hash: 0xBBBB,
1866            ..Default::default()
1867        })
1868        .failure(sample_failure())
1869        .fingerprint(0x1234)
1870        .build()
1871        .expect("crash pack builder should have failure metadata");
1872
1873        assert_ne!(artifact_filename(&pack_a), artifact_filename(&pack_b));
1874
1875        crate::test_complete!("artifact_filename_varies_by_config_hash");
1876    }
1877
1878    #[test]
1879    fn memory_writer_collects_packs() {
1880        init_test("memory_writer_collects_packs");
1881
1882        let writer = MemoryCrashPackWriter::new();
1883        assert_eq!(writer.count(), 0);
1884        assert!(!writer.is_persistent());
1885        assert_eq!(writer.name(), "memory");
1886
1887        let pack = CrashPack::builder(sample_config())
1888            .failure(sample_failure())
1889            .fingerprint(0x1234)
1890            .build()
1891            .expect("crash pack builder should have failure metadata");
1892
1893        let artifact = writer.write(&pack).unwrap();
1894        assert_eq!(writer.count(), 1);
1895        assert!(artifact.path().contains("crashpack-"));
1896        assert!(artifact.path().contains("1234"));
1897
1898        // Write a second pack
1899        let pack2 = CrashPack::builder(CrashPackConfig {
1900            seed: 99,
1901            ..Default::default()
1902        })
1903        .failure(sample_failure())
1904        .fingerprint(0x5678)
1905        .build()
1906        .expect("crash pack builder should have failure metadata");
1907
1908        let artifact2 = writer.write(&pack2).unwrap();
1909        assert_eq!(writer.count(), 2);
1910        assert_ne!(artifact.path(), artifact2.path());
1911
1912        crate::test_complete!("memory_writer_collects_packs");
1913    }
1914
1915    #[test]
1916    fn memory_writer_produces_valid_json() {
1917        init_test("memory_writer_produces_valid_json");
1918
1919        let writer = MemoryCrashPackWriter::new();
1920        let pack = CrashPack::builder(sample_config())
1921            .failure(sample_failure())
1922            .fingerprint(0xDEAD)
1923            .event_count(42)
1924            .oracle_violations(vec!["inv-1".into()])
1925            .build()
1926            .expect("crash pack builder should have failure metadata");
1927
1928        writer.write(&pack).unwrap();
1929        let written = writer.written();
1930        assert_eq!(written.len(), 1);
1931
1932        let json = &written[0].1;
1933        // Must be valid JSON
1934        let parsed: serde_json::Value = serde_json::from_str(json).unwrap();
1935        assert_eq!(parsed["manifest"]["config"]["seed"], 42);
1936        assert_eq!(parsed["manifest"]["fingerprint"], 0xDEAD_u64);
1937        assert_eq!(parsed["manifest"]["event_count"], 42);
1938        assert_eq!(parsed["oracle_violations"][0], "inv-1");
1939
1940        crate::test_complete!("memory_writer_produces_valid_json");
1941    }
1942
1943    #[test]
1944    fn file_writer_writes_to_disk() {
1945        init_test("file_writer_writes_to_disk");
1946
1947        let dir = std::env::temp_dir().join("asupersync_test_crashpack");
1948        let _ = std::fs::create_dir_all(&dir);
1949
1950        let writer = FileCrashPackWriter::new(dir.clone());
1951        assert!(writer.is_persistent());
1952        assert_eq!(writer.name(), "file");
1953        assert_eq!(writer.base_dir(), dir.as_path());
1954
1955        let pack = CrashPack::builder(CrashPackConfig {
1956            seed: 7,
1957            ..Default::default()
1958        })
1959        .failure(sample_failure())
1960        .fingerprint(0xBEEF)
1961        .build()
1962        .expect("crash pack builder should have failure metadata");
1963
1964        let artifact = writer.write(&pack).unwrap();
1965        let expected_name = artifact_filename(&pack);
1966
1967        // Artifact path should contain the deterministic filename
1968        assert!(artifact.path().contains(&expected_name));
1969
1970        // File should exist and contain valid JSON
1971        let contents = std::fs::read_to_string(artifact.path()).unwrap();
1972        let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap();
1973        assert_eq!(parsed["manifest"]["config"]["seed"], 7);
1974
1975        // Cleanup
1976        let _ = std::fs::remove_file(artifact.path());
1977        let _ = std::fs::remove_dir(&dir);
1978
1979        crate::test_complete!("file_writer_writes_to_disk");
1980    }
1981
1982    #[test]
1983    fn file_writer_fails_on_missing_dir() {
1984        init_test("file_writer_fails_on_missing_dir");
1985
1986        let writer =
1987            FileCrashPackWriter::new(std::path::PathBuf::from("/nonexistent/crashpack/dir"));
1988
1989        let pack = CrashPack::builder(CrashPackConfig::default())
1990            .failure(sample_failure())
1991            .build()
1992            .expect("crash pack builder should have failure metadata");
1993
1994        let result = writer.write(&pack);
1995        assert!(result.is_err());
1996
1997        crate::test_complete!("file_writer_fails_on_missing_dir");
1998    }
1999
2000    #[test]
2001    fn artifact_id_display() {
2002        init_test("artifact_id_display");
2003
2004        let id = ArtifactId {
2005            path: "some/path.json".to_string(),
2006        };
2007        assert_eq!(format!("{id}"), "some/path.json");
2008        assert_eq!(id.path(), "some/path.json");
2009
2010        crate::test_complete!("artifact_id_display");
2011    }
2012
2013    #[test]
2014    fn conformance_no_ambient_writes() {
2015        init_test("conformance_no_ambient_writes");
2016
2017        // The CrashPack::builder().build() path never touches the filesystem.
2018        // Writing requires an explicit CrashPackWriter.
2019        let pack = CrashPack::builder(sample_config())
2020            .failure(sample_failure())
2021            .build()
2022            .expect("crash pack builder should have failure metadata");
2023
2024        // pack exists in memory - no writer means no writes
2025        assert_eq!(pack.seed(), 42);
2026
2027        // Only a writer can persist
2028        let writer = MemoryCrashPackWriter::new();
2029        assert_eq!(writer.count(), 0);
2030        writer.write(&pack).unwrap();
2031        assert_eq!(writer.count(), 1);
2032
2033        crate::test_complete!("conformance_no_ambient_writes");
2034    }
2035
2036    #[test]
2037    fn conformance_same_pack_same_artifact_path() {
2038        init_test("conformance_same_pack_same_artifact_path");
2039
2040        let writer = MemoryCrashPackWriter::new();
2041
2042        let pack = CrashPack::builder(CrashPackConfig {
2043            seed: 100,
2044            ..Default::default()
2045        })
2046        .failure(sample_failure())
2047        .fingerprint(0xFACE)
2048        .build()
2049        .expect("crash pack builder should have failure metadata");
2050
2051        let id1 = writer.write(&pack).unwrap();
2052        let id2 = writer.write(&pack).unwrap();
2053
2054        // Same pack produces same artifact path (deterministic naming)
2055        assert_eq!(id1.path(), id2.path());
2056
2057        crate::test_complete!("conformance_same_pack_same_artifact_path");
2058    }
2059
2060    // =================================================================
2061    // Manifest Schema Tests (bd-35u33)
2062    // =================================================================
2063
2064    #[test]
2065    fn manifest_validate_current_version() {
2066        init_test("manifest_validate_current_version");
2067
2068        let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2069        assert!(manifest.validate().is_ok());
2070        assert!(manifest.is_compatible());
2071        assert_eq!(manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
2072
2073        crate::test_complete!("manifest_validate_current_version");
2074    }
2075
2076    #[test]
2077    fn manifest_validate_rejects_future_version() {
2078        init_test("manifest_validate_rejects_future_version");
2079
2080        let mut manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2081        manifest.schema_version = CRASHPACK_SCHEMA_VERSION + 1;
2082
2083        let err = manifest.validate().unwrap_err();
2084        assert!(!manifest.is_compatible());
2085        assert!(matches!(err, ManifestValidationError::VersionTooNew { .. }));
2086        // Display impl
2087        assert!(err.to_string().contains("newer than supported"));
2088
2089        crate::test_complete!("manifest_validate_rejects_future_version");
2090    }
2091
2092    #[test]
2093    fn manifest_validate_rejects_old_version() {
2094        init_test("manifest_validate_rejects_old_version");
2095
2096        let mut manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2097        manifest.schema_version = 0; // below minimum
2098
2099        let err = manifest.validate().unwrap_err();
2100        assert!(!manifest.is_compatible());
2101        assert!(matches!(err, ManifestValidationError::VersionTooOld { .. }));
2102        assert!(err.to_string().contains("older than minimum"));
2103
2104        crate::test_complete!("manifest_validate_rejects_old_version");
2105    }
2106
2107    #[test]
2108    fn manifest_attachments_auto_populated() {
2109        init_test("manifest_attachments_auto_populated");
2110
2111        // A pack with canonical prefix, divergent prefix, and oracle violations
2112        // should have those listed as attachments.
2113        let events = [
2114            TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
2115            TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
2116        ];
2117
2118        let pack = CrashPack::builder(sample_config())
2119            .failure(sample_failure())
2120            .from_trace(&events)
2121            .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 42 }])
2122            .oracle_violations(vec!["inv-1".into()])
2123            .build()
2124            .expect("crash pack builder should have failure metadata");
2125
2126        assert_eq!(pack.manifest.attachments.len(), 3);
2127        assert!(
2128            pack.manifest
2129                .has_attachment(&AttachmentKind::CanonicalPrefix)
2130        );
2131        assert!(
2132            pack.manifest
2133                .has_attachment(&AttachmentKind::DivergentPrefix)
2134        );
2135        assert!(
2136            pack.manifest
2137                .has_attachment(&AttachmentKind::OracleViolations)
2138        );
2139        assert!(
2140            !pack
2141                .manifest
2142                .has_attachment(&AttachmentKind::EvidenceLedger)
2143        );
2144        assert!(
2145            !pack
2146                .manifest
2147                .has_attachment(&AttachmentKind::SupervisionLog)
2148        );
2149
2150        crate::test_complete!("manifest_attachments_auto_populated");
2151    }
2152
2153    #[test]
2154    fn manifest_empty_pack_no_attachments() {
2155        init_test("manifest_empty_pack_no_attachments");
2156
2157        let pack = CrashPack::builder(CrashPackConfig::default())
2158            .failure(sample_failure())
2159            .build()
2160            .expect("crash pack builder should have failure metadata");
2161
2162        assert!(pack.manifest.attachments.is_empty());
2163
2164        crate::test_complete!("manifest_empty_pack_no_attachments");
2165    }
2166
2167    #[test]
2168    fn manifest_attachment_item_counts() {
2169        init_test("manifest_attachment_item_counts");
2170
2171        let pack = CrashPack::builder(sample_config())
2172            .failure(sample_failure())
2173            .canonical_prefix(vec![
2174                vec![TraceEventKey {
2175                    kind: 1,
2176                    primary: 0,
2177                    secondary: 0,
2178                    tertiary: 0,
2179                }],
2180                vec![
2181                    TraceEventKey {
2182                        kind: 2,
2183                        primary: 1,
2184                        secondary: 0,
2185                        tertiary: 0,
2186                    },
2187                    TraceEventKey {
2188                        kind: 2,
2189                        primary: 2,
2190                        secondary: 0,
2191                        tertiary: 0,
2192                    },
2193                ],
2194            ])
2195            .supervision_snapshot(SupervisionSnapshot {
2196                virtual_time: Time::from_secs(1),
2197                task: tid(1),
2198                region: rid(0),
2199                decision: "restart".into(),
2200                context: None,
2201            })
2202            .build()
2203            .expect("crash pack builder should have failure metadata");
2204
2205        // Canonical prefix: 2 layers with 3 total events
2206        let cp = pack
2207            .manifest
2208            .attachment(&AttachmentKind::CanonicalPrefix)
2209            .unwrap();
2210        assert_eq!(cp.item_count, 3);
2211
2212        // Supervision log: 1 entry
2213        let sl = pack
2214            .manifest
2215            .attachment(&AttachmentKind::SupervisionLog)
2216            .unwrap();
2217        assert_eq!(sl.item_count, 1);
2218
2219        crate::test_complete!("manifest_attachment_item_counts");
2220    }
2221
2222    #[test]
2223    fn manifest_attachment_kind_serde_round_trip() {
2224        init_test("manifest_attachment_kind_serde_round_trip");
2225
2226        let kinds = vec![
2227            AttachmentKind::CanonicalPrefix,
2228            AttachmentKind::DivergentPrefix,
2229            AttachmentKind::EvidenceLedger,
2230            AttachmentKind::SupervisionLog,
2231            AttachmentKind::OracleViolations,
2232            AttachmentKind::Custom {
2233                tag: "heap-dump".into(),
2234            },
2235        ];
2236
2237        for kind in &kinds {
2238            let json = serde_json::to_string(kind).unwrap();
2239            let parsed: AttachmentKind = serde_json::from_str(&json).unwrap();
2240            assert_eq!(&parsed, kind, "round trip failed for {json}");
2241        }
2242
2243        crate::test_complete!("manifest_attachment_kind_serde_round_trip");
2244    }
2245
2246    #[test]
2247    fn manifest_serde_round_trip_with_attachments() {
2248        init_test("manifest_serde_round_trip_with_attachments");
2249
2250        let mut manifest = CrashPackManifest::new(sample_config(), 0xBEEF, 100);
2251        manifest.attachments = vec![
2252            ManifestAttachment {
2253                kind: AttachmentKind::CanonicalPrefix,
2254                item_count: 10,
2255                size_hint_bytes: 2048,
2256            },
2257            ManifestAttachment {
2258                kind: AttachmentKind::Custom {
2259                    tag: "user-data".into(),
2260                },
2261                item_count: 1,
2262                size_hint_bytes: 0,
2263            },
2264        ];
2265
2266        let json = serde_json::to_string_pretty(&manifest).unwrap();
2267        let parsed: CrashPackManifest = serde_json::from_str(&json).unwrap();
2268
2269        assert_eq!(parsed.schema_version, CRASHPACK_SCHEMA_VERSION);
2270        assert_eq!(parsed.config.seed, 42);
2271        assert_eq!(parsed.fingerprint, 0xBEEF);
2272        assert_eq!(parsed.attachments.len(), 2);
2273        assert_eq!(parsed.attachments[0].kind, AttachmentKind::CanonicalPrefix);
2274        assert_eq!(parsed.attachments[0].item_count, 10);
2275        assert_eq!(parsed.attachments[0].size_hint_bytes, 2048);
2276        assert_eq!(
2277            parsed.attachments[1].kind,
2278            AttachmentKind::Custom {
2279                tag: "user-data".into()
2280            }
2281        );
2282
2283        crate::test_complete!("manifest_serde_round_trip_with_attachments");
2284    }
2285
2286    #[test]
2287    fn manifest_deserialize_without_attachments() {
2288        init_test("manifest_deserialize_without_attachments");
2289
2290        // Simulate a v1 manifest JSON that was written before the attachments
2291        // field existed. The #[serde(default)] should handle this gracefully.
2292        let json = r#"{
2293            "schema_version": 1,
2294            "config": { "seed": 1, "config_hash": 0, "worker_count": 1 },
2295            "fingerprint": 999,
2296            "event_count": 50,
2297            "created_at": 0
2298        }"#;
2299
2300        let manifest: CrashPackManifest = serde_json::from_str(json).unwrap();
2301        assert_eq!(manifest.schema_version, 1);
2302        assert_eq!(manifest.fingerprint, 999);
2303        assert!(manifest.attachments.is_empty());
2304        assert!(manifest.is_compatible());
2305
2306        crate::test_complete!("manifest_deserialize_without_attachments");
2307    }
2308
2309    #[test]
2310    fn manifest_json_skips_empty_attachments() {
2311        init_test("manifest_json_skips_empty_attachments");
2312
2313        let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2314        let json = serde_json::to_string(&manifest).unwrap();
2315
2316        // Empty attachments should be skipped by skip_serializing_if
2317        assert!(!json.contains("attachments"));
2318
2319        crate::test_complete!("manifest_json_skips_empty_attachments");
2320    }
2321
2322    #[test]
2323    fn manifest_json_skips_zero_size_hint() {
2324        init_test("manifest_json_skips_zero_size_hint");
2325
2326        let attachment = ManifestAttachment {
2327            kind: AttachmentKind::CanonicalPrefix,
2328            item_count: 5,
2329            size_hint_bytes: 0,
2330        };
2331        let json = serde_json::to_string(&attachment).unwrap();
2332        assert!(!json.contains("size_hint_bytes"));
2333
2334        let non_zero = ManifestAttachment {
2335            kind: AttachmentKind::CanonicalPrefix,
2336            item_count: 5,
2337            size_hint_bytes: 1024,
2338        };
2339        let json2 = serde_json::to_string(&non_zero).unwrap();
2340        assert!(json2.contains("size_hint_bytes"));
2341
2342        crate::test_complete!("manifest_json_skips_zero_size_hint");
2343    }
2344
2345    #[test]
2346    fn conformance_attachments_in_crash_pack_json() {
2347        init_test("conformance_attachments_in_crash_pack_json");
2348
2349        // Full crash pack with all sections → attachments appear in JSON
2350        let events = [
2351            TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
2352            TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
2353        ];
2354
2355        let pack = CrashPack::builder(sample_config())
2356            .failure(sample_failure())
2357            .from_trace(&events)
2358            .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 42 }])
2359            .oracle_violations(vec!["v1".into()])
2360            .supervision_snapshot(SupervisionSnapshot {
2361                virtual_time: Time::from_secs(1),
2362                task: tid(1),
2363                region: rid(0),
2364                decision: "restart".into(),
2365                context: None,
2366            })
2367            .build()
2368            .expect("crash pack builder should have failure metadata");
2369
2370        let writer = MemoryCrashPackWriter::new();
2371        writer.write(&pack).unwrap();
2372        let json_str = &writer.written()[0].1;
2373        let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
2374
2375        let atts = parsed["manifest"]["attachments"].as_array().unwrap();
2376        assert_eq!(atts.len(), 4);
2377
2378        // Verify kinds are tagged correctly
2379        let kinds: Vec<&str> = atts.iter().map(|a| a["kind"].as_str().unwrap()).collect();
2380        assert!(kinds.contains(&"CanonicalPrefix"));
2381        assert!(kinds.contains(&"DivergentPrefix"));
2382        assert!(kinds.contains(&"SupervisionLog"));
2383        assert!(kinds.contains(&"OracleViolations"));
2384
2385        crate::test_complete!("conformance_attachments_in_crash_pack_json");
2386    }
2387
2388    #[test]
2389    fn conformance_validation_error_is_std_error() {
2390        init_test("conformance_validation_error_is_std_error");
2391
2392        let err = ManifestValidationError::VersionTooNew {
2393            manifest_version: 99,
2394            supported_version: 1,
2395        };
2396
2397        // Must implement std::error::Error
2398        let _: &dyn std::error::Error = &err;
2399        assert!(err.to_string().contains("99"));
2400
2401        crate::test_complete!("conformance_validation_error_is_std_error");
2402    }
2403
2404    // =================================================================
2405    // Replay Command Contract Tests (bd-1teda)
2406    // =================================================================
2407
2408    #[test]
2409    fn replay_command_from_config_basic() {
2410        init_test("replay_command_from_config_basic");
2411
2412        let config = CrashPackConfig {
2413            seed: 42,
2414            config_hash: 0xDEAD,
2415            worker_count: 4,
2416            max_steps: Some(1000),
2417            commit_hash: Some("abc123".to_string()),
2418        };
2419
2420        let cmd = ReplayCommand::from_config(&config, None);
2421        assert_eq!(cmd.program, "cargo");
2422        assert!(cmd.args.contains(&"--seed".to_string()));
2423        assert!(cmd.args.contains(&"42".to_string()));
2424        assert!(!cmd.env.is_empty());
2425        assert!(cmd.command_line.contains("cargo"));
2426        assert!(cmd.command_line.contains("--seed"));
2427        assert!(cmd.command_line.contains("42"));
2428        assert!(cmd.command_line.contains("ASUPERSYNC_WORKERS=4"));
2429
2430        crate::test_complete!("replay_command_from_config_basic");
2431    }
2432
2433    #[test]
2434    fn replay_command_from_config_with_artifact() {
2435        init_test("replay_command_from_config_with_artifact");
2436
2437        let config = CrashPackConfig {
2438            seed: 99,
2439            worker_count: 2,
2440            ..Default::default()
2441        };
2442
2443        let cmd = ReplayCommand::from_config(&config, Some("crashes/pack.json"));
2444        assert!(cmd.args.contains(&"--crashpack".to_string()));
2445        assert!(cmd.args.contains(&"crashes/pack.json".to_string()));
2446        assert!(cmd.command_line.contains("--crashpack"));
2447        assert!(cmd.command_line.contains("crashes/pack.json"));
2448
2449        crate::test_complete!("replay_command_from_config_with_artifact");
2450    }
2451
2452    #[test]
2453    fn replay_command_cli_mode() {
2454        init_test("replay_command_cli_mode");
2455
2456        let config = CrashPackConfig {
2457            seed: 7,
2458            worker_count: 8,
2459            max_steps: Some(500),
2460            ..Default::default()
2461        };
2462
2463        let cmd = ReplayCommand::from_config_cli(&config, "crashpack.json");
2464        assert_eq!(cmd.program, "asupersync");
2465        assert!(cmd.args.contains(&"trace".to_string()));
2466        assert!(cmd.args.contains(&"replay".to_string()));
2467        assert!(cmd.args.contains(&"--seed".to_string()));
2468        assert!(cmd.args.contains(&"7".to_string()));
2469        assert!(cmd.args.contains(&"--workers".to_string()));
2470        assert!(cmd.args.contains(&"8".to_string()));
2471        assert!(cmd.args.contains(&"--max-steps".to_string()));
2472        assert!(cmd.args.contains(&"500".to_string()));
2473        assert!(cmd.args.contains(&"crashpack.json".to_string()));
2474        assert!(cmd.env.is_empty());
2475        assert_eq!(
2476            cmd.command_line,
2477            "asupersync trace replay --seed 7 --workers 8 --max-steps 500 crashpack.json"
2478        );
2479
2480        crate::test_complete!("replay_command_cli_mode");
2481    }
2482
2483    #[test]
2484    fn replay_command_display() {
2485        init_test("replay_command_display");
2486
2487        let cmd = ReplayCommand::from_config_cli(
2488            &CrashPackConfig {
2489                seed: 1,
2490                worker_count: 1,
2491                ..Default::default()
2492            },
2493            "test.json",
2494        );
2495
2496        let displayed = format!("{cmd}");
2497        assert_eq!(displayed, cmd.command_line);
2498
2499        crate::test_complete!("replay_command_display");
2500    }
2501
2502    #[test]
2503    fn replay_command_serde_round_trip() {
2504        init_test("replay_command_serde_round_trip");
2505
2506        let cmd = ReplayCommand::from_config(
2507            &CrashPackConfig {
2508                seed: 42,
2509                worker_count: 4,
2510                max_steps: Some(1000),
2511                ..Default::default()
2512            },
2513            Some("pack.json"),
2514        );
2515
2516        let json = serde_json::to_string_pretty(&cmd).unwrap();
2517        let parsed: ReplayCommand = serde_json::from_str(&json).unwrap();
2518        assert_eq!(parsed, cmd);
2519
2520        crate::test_complete!("replay_command_serde_round_trip");
2521    }
2522
2523    #[test]
2524    fn replay_command_in_crash_pack() {
2525        init_test("replay_command_in_crash_pack");
2526
2527        let config = sample_config();
2528        let replay_cmd = ReplayCommand::from_config(&config, Some("crashes/test.json"));
2529
2530        let pack = CrashPack::builder(config)
2531            .failure(sample_failure())
2532            .fingerprint(0xCAFE)
2533            .replay(replay_cmd.clone())
2534            .build()
2535            .expect("crash pack builder should have failure metadata");
2536
2537        assert_eq!(pack.replay.as_ref(), Some(&replay_cmd));
2538
2539        // Appears in JSON
2540        let writer = MemoryCrashPackWriter::new();
2541        writer.write(&pack).unwrap();
2542        let json_str = &writer.written()[0].1;
2543        let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
2544        assert!(parsed["replay"]["program"].as_str().is_some());
2545        assert!(
2546            parsed["replay"]["command_line"]
2547                .as_str()
2548                .unwrap()
2549                .contains("--seed")
2550        );
2551
2552        crate::test_complete!("replay_command_in_crash_pack");
2553    }
2554
2555    #[test]
2556    fn replay_command_absent_by_default() {
2557        init_test("replay_command_absent_by_default");
2558
2559        let pack = CrashPack::builder(CrashPackConfig::default())
2560            .failure(sample_failure())
2561            .build()
2562            .expect("crash pack builder should have failure metadata");
2563
2564        assert!(pack.replay.is_none());
2565
2566        // replay field should be absent from JSON
2567        let writer = MemoryCrashPackWriter::new();
2568        writer.write(&pack).unwrap();
2569        let json_str = &writer.written()[0].1;
2570        assert!(!json_str.contains("\"replay\""));
2571
2572        crate::test_complete!("replay_command_absent_by_default");
2573    }
2574
2575    #[test]
2576    fn replay_command_convenience_method() {
2577        init_test("replay_command_convenience_method");
2578
2579        let pack = CrashPack::builder(CrashPackConfig {
2580            seed: 77,
2581            worker_count: 2,
2582            ..Default::default()
2583        })
2584        .failure(sample_failure())
2585        .build()
2586        .expect("crash pack builder should have failure metadata");
2587
2588        let cmd = pack.replay_command(Some("output.json"));
2589        assert!(cmd.command_line.contains("--seed"));
2590        assert!(cmd.command_line.contains("77"));
2591        assert!(cmd.command_line.contains("output.json"));
2592
2593        crate::test_complete!("replay_command_convenience_method");
2594    }
2595
2596    #[test]
2597    fn replay_command_max_steps_included_when_set() {
2598        init_test("replay_command_max_steps_included_when_set");
2599
2600        let with_steps = ReplayCommand::from_config(
2601            &CrashPackConfig {
2602                seed: 1,
2603                max_steps: Some(999),
2604                ..Default::default()
2605            },
2606            None,
2607        );
2608        assert!(with_steps.command_line.contains("ASUPERSYNC_MAX_STEPS=999"));
2609
2610        let without_steps = ReplayCommand::from_config(
2611            &CrashPackConfig {
2612                seed: 1,
2613                max_steps: None,
2614                ..Default::default()
2615            },
2616            None,
2617        );
2618        assert!(!without_steps.command_line.contains("ASUPERSYNC_MAX_STEPS"));
2619
2620        crate::test_complete!("replay_command_max_steps_included_when_set");
2621    }
2622
2623    #[test]
2624    fn shell_escape_handles_special_chars() {
2625        init_test("shell_escape_handles_special_chars");
2626
2627        // Safe strings pass through
2628        assert_eq!(shell_escape("hello"), "hello");
2629        assert_eq!(shell_escape("path/to/file.json"), "path/to/file.json");
2630        assert_eq!(shell_escape("42"), "42");
2631
2632        // Strings with spaces get quoted
2633        assert_eq!(shell_escape("hello world"), "'hello world'");
2634
2635        // Empty string
2636        assert_eq!(shell_escape(""), "''");
2637
2638        crate::test_complete!("shell_escape_handles_special_chars");
2639    }
2640
2641    // =================================================================
2642    // Golden Crashpack + Replay Tests (bd-3mfjw)
2643    // =================================================================
2644
2645    /// A controlled failure scenario: two workers in a region, one panics.
2646    fn golden_failure_events() -> Vec<TraceEvent> {
2647        vec![
2648            TraceEvent::region_created(1, Time::ZERO, rid(1), None),
2649            TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
2650            TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
2651            TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
2652            TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
2653            TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
2654        ]
2655    }
2656
2657    fn golden_config() -> CrashPackConfig {
2658        CrashPackConfig {
2659            seed: 42,
2660            config_hash: 0xDEAD,
2661            worker_count: 4,
2662            max_steps: Some(1000),
2663            commit_hash: Some("abc123def".to_string()),
2664        }
2665    }
2666
2667    fn golden_failure_info() -> FailureInfo {
2668        FailureInfo {
2669            task: tid(2),
2670            region: rid(1),
2671            outcome: FailureOutcome::Panicked {
2672                message: "worker panic in golden scenario".to_string(),
2673            },
2674            virtual_time: Time::from_nanos(200),
2675        }
2676    }
2677
2678    #[test]
2679    fn golden_deterministic_emission() {
2680        init_test("golden_deterministic_emission");
2681
2682        let events = golden_failure_events();
2683
2684        // Build the same crash pack twice.
2685        let pack1 = CrashPack::builder(golden_config())
2686            .failure(golden_failure_info())
2687            .from_trace(&events)
2688            .build()
2689            .expect("crash pack builder should have failure metadata");
2690
2691        let pack2 = CrashPack::builder(golden_config())
2692            .failure(golden_failure_info())
2693            .from_trace(&events)
2694            .build()
2695            .expect("crash pack builder should have failure metadata");
2696
2697        // Determinism: same inputs → same pack (modulo created_at).
2698        assert_eq!(pack1, pack2);
2699        assert_eq!(pack1.fingerprint(), pack2.fingerprint());
2700        assert_eq!(pack1.canonical_prefix, pack2.canonical_prefix);
2701        assert_eq!(pack1.manifest.event_count, pack2.manifest.event_count);
2702
2703        crate::test_complete!("golden_deterministic_emission");
2704    }
2705
2706    #[test]
2707    fn golden_fingerprint_stability() {
2708        init_test("golden_fingerprint_stability");
2709
2710        let events = golden_failure_events();
2711        let pack = CrashPack::builder(golden_config())
2712            .failure(golden_failure_info())
2713            .from_trace(&events)
2714            .build()
2715            .expect("crash pack builder should have failure metadata");
2716
2717        // The fingerprint must be non-zero and consistent.
2718        let fp = pack.fingerprint();
2719        assert_ne!(fp, 0);
2720
2721        // Rebuild from scratch — fingerprint must match exactly.
2722        let fp2 = CrashPack::builder(golden_config())
2723            .failure(golden_failure_info())
2724            .from_trace(&events)
2725            .build()
2726            .expect("crash pack builder should have failure metadata")
2727            .fingerprint();
2728        assert_eq!(fp, fp2);
2729
2730        // Independently compute via trace_fingerprint().
2731        assert_eq!(fp, crate::trace::canonicalize::trace_fingerprint(&events));
2732
2733        crate::test_complete!("golden_fingerprint_stability");
2734    }
2735
2736    #[test]
2737    fn golden_canonical_prefix_structure() {
2738        init_test("golden_canonical_prefix_structure");
2739
2740        let events = golden_failure_events();
2741        let pack = CrashPack::builder(golden_config())
2742            .failure(golden_failure_info())
2743            .from_trace(&events)
2744            .build()
2745            .expect("crash pack builder should have failure metadata");
2746
2747        // Expected Foata structure for the golden scenario:
2748        //   Layer 0: region_created(R1) — no predecessors
2749        //   Layer 1: spawn(T1,R1), spawn(T2,R1) — depend on region_created
2750        //   Layer 2: poll(T1,R1), poll(T2,R1) — depend on respective spawns
2751        //   Layer 3: complete(T1,R1) — depends on poll(T1)
2752        assert_eq!(
2753            pack.canonical_prefix.len(),
2754            4,
2755            "expected 4 Foata layers, got {}",
2756            pack.canonical_prefix.len()
2757        );
2758        assert_eq!(pack.canonical_prefix[0].len(), 1); // region_created
2759        assert_eq!(pack.canonical_prefix[1].len(), 2); // spawn×2
2760        assert_eq!(pack.canonical_prefix[2].len(), 2); // poll×2
2761        assert_eq!(pack.canonical_prefix[3].len(), 1); // complete
2762
2763        // Event count matches input.
2764        assert_eq!(pack.manifest.event_count, 6);
2765
2766        crate::test_complete!("golden_canonical_prefix_structure");
2767    }
2768
2769    #[test]
2770    fn golden_equivalent_schedule_same_pack() {
2771        init_test("golden_equivalent_schedule_same_pack");
2772
2773        // The golden scenario with independent spawns/polls in swapped order.
2774        // This is a different schedule of the same concurrent execution.
2775        let events_a = golden_failure_events();
2776        let events_b = vec![
2777            TraceEvent::region_created(1, Time::ZERO, rid(1), None),
2778            TraceEvent::spawn(2, Time::ZERO, tid(2), rid(1)), // T2 first
2779            TraceEvent::spawn(3, Time::ZERO, tid(1), rid(1)), // T1 second
2780            TraceEvent::poll(4, Time::from_nanos(100), tid(2), rid(1)),
2781            TraceEvent::poll(5, Time::from_nanos(100), tid(1), rid(1)),
2782            TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
2783        ];
2784
2785        let pack_a = CrashPack::builder(golden_config())
2786            .failure(golden_failure_info())
2787            .from_trace(&events_a)
2788            .build()
2789            .expect("crash pack builder should have failure metadata");
2790        let pack_b = CrashPack::builder(golden_config())
2791            .failure(golden_failure_info())
2792            .from_trace(&events_b)
2793            .build()
2794            .expect("crash pack builder should have failure metadata");
2795
2796        // Same equivalence class → same crash pack.
2797        assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
2798        assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
2799        assert_eq!(pack_a, pack_b);
2800
2801        crate::test_complete!("golden_equivalent_schedule_same_pack");
2802    }
2803
2804    #[test]
2805    fn golden_replay_prefix_round_trip() {
2806        use crate::trace::replay::{
2807            CompactRegionId, CompactTaskId, ReplayEvent, ReplayTrace, TraceMetadata,
2808        };
2809        use crate::trace::replayer::TraceReplayer;
2810
2811        init_test("golden_replay_prefix_round_trip");
2812
2813        // Build a ReplayTrace matching the golden scenario.
2814        let replay_events = vec![
2815            ReplayEvent::RngSeed { seed: 42 },
2816            ReplayEvent::RegionCreated {
2817                region: CompactRegionId(1),
2818                parent: None,
2819                at_tick: 0,
2820            },
2821            ReplayEvent::TaskSpawned {
2822                task: CompactTaskId(1),
2823                region: CompactRegionId(1),
2824                at_tick: 0,
2825            },
2826            ReplayEvent::TaskSpawned {
2827                task: CompactTaskId(2),
2828                region: CompactRegionId(1),
2829                at_tick: 0,
2830            },
2831            ReplayEvent::TaskScheduled {
2832                task: CompactTaskId(1),
2833                at_tick: 100,
2834            },
2835            ReplayEvent::TaskScheduled {
2836                task: CompactTaskId(2),
2837                at_tick: 100,
2838            },
2839            ReplayEvent::TaskCompleted {
2840                task: CompactTaskId(1),
2841                outcome: 0, // Ok
2842            },
2843        ];
2844
2845        let trace = ReplayTrace {
2846            metadata: TraceMetadata::new(42),
2847            events: replay_events.clone(),
2848            cursor: 0,
2849        };
2850
2851        // Build crash pack with the divergent prefix.
2852        let pack = CrashPack::builder(golden_config())
2853            .failure(golden_failure_info())
2854            .from_trace(&golden_failure_events())
2855            .divergent_prefix(replay_events.clone())
2856            .build()
2857            .expect("crash pack builder should have failure metadata");
2858
2859        assert!(pack.has_divergent_prefix());
2860        assert_eq!(pack.divergent_prefix.len(), 7);
2861
2862        // Verify the replayer can step through the divergent prefix
2863        // without any divergence errors.
2864        let mut replayer = TraceReplayer::new(trace);
2865        for expected_event in &replay_events {
2866            let actual = replayer.next().expect("replayer should have more events");
2867            assert_eq!(actual, expected_event);
2868        }
2869        assert!(replayer.is_completed());
2870
2871        crate::test_complete!("golden_replay_prefix_round_trip");
2872    }
2873
2874    #[test]
2875    fn golden_replay_serialization_round_trip() {
2876        use crate::trace::replay::{
2877            CompactRegionId, CompactTaskId, ReplayEvent, ReplayTrace, TraceMetadata,
2878        };
2879
2880        init_test("golden_replay_serialization_round_trip");
2881
2882        let replay_events = vec![
2883            ReplayEvent::RngSeed { seed: 42 },
2884            ReplayEvent::TaskSpawned {
2885                task: CompactTaskId(1),
2886                region: CompactRegionId(1),
2887                at_tick: 0,
2888            },
2889            ReplayEvent::TaskCompleted {
2890                task: CompactTaskId(1),
2891                outcome: 3, // Panicked
2892            },
2893        ];
2894
2895        let mut trace = ReplayTrace::new(TraceMetadata::new(42));
2896        for ev in &replay_events {
2897            trace.push(ev.clone());
2898        }
2899
2900        // Serialize → deserialize round trip.
2901        let bytes = trace.to_bytes().expect("serialize");
2902        let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
2903
2904        assert_eq!(loaded.metadata.seed, 42);
2905        assert_eq!(loaded.events.len(), 3);
2906        assert_eq!(loaded.events, replay_events);
2907
2908        crate::test_complete!("golden_replay_serialization_round_trip");
2909    }
2910
2911    #[test]
2912    fn golden_crash_pack_json_round_trip() {
2913        init_test("golden_crash_pack_json_round_trip");
2914
2915        let events = golden_failure_events();
2916        let pack = CrashPack::builder(golden_config())
2917            .failure(golden_failure_info())
2918            .from_trace(&events)
2919            .oracle_violations(vec!["invariant-x".into()])
2920            .build()
2921            .expect("crash pack builder should have failure metadata");
2922
2923        let writer = MemoryCrashPackWriter::new();
2924        writer.write(&pack).unwrap();
2925        let written = writer.written();
2926        let json = &written[0].1;
2927
2928        // Parse the JSON and verify key fields.
2929        let parsed: serde_json::Value = serde_json::from_str(json).unwrap();
2930        assert_eq!(parsed["manifest"]["config"]["seed"], 42);
2931        assert_eq!(parsed["manifest"]["config"]["config_hash"], 0xDEAD_u64);
2932        assert_eq!(parsed["manifest"]["event_count"], 6);
2933        assert_ne!(parsed["manifest"]["fingerprint"], 0);
2934        assert_eq!(parsed["oracle_violations"][0], "invariant-x");
2935
2936        // Canonical prefix should be present.
2937        let prefix = &parsed["canonical_prefix"];
2938        assert!(prefix.is_array());
2939        assert_eq!(prefix.as_array().unwrap().len(), 4); // 4 Foata layers
2940
2941        crate::test_complete!("golden_crash_pack_json_round_trip");
2942    }
2943
2944    #[test]
2945    fn golden_minimization_integration() {
2946        use crate::trace::divergence::{MinimizationConfig, minimize_divergent_prefix};
2947        use crate::trace::replay::{ReplayEvent, ReplayTrace, TraceMetadata};
2948
2949        init_test("golden_minimization_integration");
2950
2951        // Build a replay prefix: the failure "happens" at event index 5+.
2952        let replay_events: Vec<_> = (0..20)
2953            .map(|i| ReplayEvent::RngValue { value: i })
2954            .collect();
2955
2956        let trace = ReplayTrace {
2957            metadata: TraceMetadata::new(42),
2958            events: replay_events,
2959            cursor: 0,
2960        };
2961
2962        // Oracle: failure reproduces when prefix has >= 12 events.
2963        let threshold = 12;
2964        let result = minimize_divergent_prefix(&trace, &MinimizationConfig::default(), |prefix| {
2965            prefix.len() >= threshold
2966        });
2967
2968        assert_eq!(result.minimized_len, threshold);
2969        assert_eq!(result.original_len, 20);
2970        assert!(!result.truncated);
2971
2972        // The minimized prefix can be set on a crash pack.
2973        let pack = CrashPack::builder(golden_config())
2974            .failure(golden_failure_info())
2975            .from_trace(&golden_failure_events())
2976            .divergent_prefix(result.prefix.events)
2977            .build()
2978            .expect("crash pack builder should have failure metadata");
2979
2980        assert!(pack.has_divergent_prefix());
2981        assert_eq!(pack.divergent_prefix.len(), threshold);
2982
2983        crate::test_complete!("golden_minimization_integration");
2984    }
2985
2986    // =========================================================================
2987    // Crash Pack Walkthrough (bd-16jzr)
2988    //
2989    // A self-contained walkthrough that demonstrates the crash pack lifecycle:
2990    //
2991    //   1. Forced failure    — a task panics during execution
2992    //   2. Crash pack emit   — build & write the repro artifact
2993    //   3. Fingerprint       — canonical fingerprint is schedule-independent
2994    //   4. Replay command    — copy-paste one-liner for reproduction
2995    //   5. Minimization      — shrink the divergent prefix
2996    //
2997    // Run with:  cargo test --lib crashpack::tests::walkthrough
2998    // =========================================================================
2999
3000    /// Step 1: Build a crash pack from a simulated failure.
3001    ///
3002    /// A supervised task panics at virtual time 200ns. We record the
3003    /// deterministic seed, config hash, and trace events into a crash pack.
3004    #[test]
3005    fn walkthrough_01_forced_failure_and_emission() {
3006        init_test("walkthrough_01_forced_failure_and_emission");
3007
3008        // -- Simulate execution producing trace events --
3009        //
3010        // In a real Spork app, these events are emitted by the LabRuntime.
3011        // Here we construct them directly to show the data flow.
3012        let events = vec![
3013            TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3014            TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3015            TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3016            TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3017            TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3018            // Task 1 completes normally; task 2 will panic.
3019            TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3020        ];
3021
3022        // -- Record the failure --
3023        let failure = FailureInfo {
3024            task: tid(2),
3025            region: rid(1),
3026            outcome: FailureOutcome::Panicked {
3027                message: "assertion failed: balance >= 0".to_string(),
3028            },
3029            virtual_time: Time::from_nanos(200),
3030        };
3031
3032        // -- Build the crash pack --
3033        //
3034        // The builder computes the canonical prefix (Foata normal form),
3035        // fingerprint, and event count from the raw trace.
3036        let config = CrashPackConfig {
3037            seed: 42,
3038            config_hash: 0xCAFE,
3039            worker_count: 2,
3040            max_steps: Some(500),
3041            commit_hash: Some("a1b2c3d".to_string()),
3042        };
3043
3044        let pack = CrashPack::builder(config)
3045            .failure(failure)
3046            .from_trace(&events)
3047            .oracle_violations(vec!["balance-invariant".to_string()])
3048            .build()
3049            .expect("crash pack builder should have failure metadata");
3050
3051        // -- Verify the crash pack --
3052        assert_eq!(pack.seed(), 42);
3053        assert_eq!(pack.manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
3054        assert_eq!(pack.manifest.event_count, 6);
3055        assert!(
3056            pack.manifest.fingerprint != 0,
3057            "fingerprint should be non-zero"
3058        );
3059        assert!(pack.has_violations());
3060        assert_eq!(pack.oracle_violations, vec!["balance-invariant"]);
3061
3062        // The canonical prefix is non-empty (Foata layers).
3063        assert!(
3064            !pack.canonical_prefix.is_empty(),
3065            "canonical prefix should have Foata layers"
3066        );
3067
3068        // Manifest auto-populates the attachment table.
3069        assert!(
3070            pack.manifest
3071                .has_attachment(&AttachmentKind::CanonicalPrefix)
3072        );
3073        assert!(
3074            pack.manifest
3075                .has_attachment(&AttachmentKind::OracleViolations)
3076        );
3077
3078        crate::test_complete!("walkthrough_01_forced_failure_and_emission");
3079    }
3080
3081    /// Step 2: Write the crash pack to storage and read it back.
3082    ///
3083    /// The artifact filename is deterministic: same seed + config hash + fingerprint
3084    /// always produces the same path.
3085    #[test]
3086    fn walkthrough_02_write_and_read_artifact() {
3087        init_test("walkthrough_02_write_and_read_artifact");
3088
3089        let pack = walkthrough_pack();
3090
3091        // -- Write using the in-memory writer --
3092        let writer = MemoryCrashPackWriter::new();
3093        let artifact = writer.write(&pack).expect("write should succeed");
3094
3095        // Deterministic filename: crashpack-{seed:016x}-{fingerprint:016x}-v{ver}.json
3096        assert!(
3097            artifact.path().starts_with("crashpack-000000000000002a-"),
3098            "path should encode seed 42 (0x2a): {}",
3099            artifact.path()
3100        );
3101        assert!(
3102            artifact.path().ends_with("-v1.json"),
3103            "path should end with schema version: {}",
3104            artifact.path()
3105        );
3106
3107        // -- Read back and verify round-trip --
3108        let written = writer.written();
3109        assert_eq!(written.len(), 1);
3110        let json = &written[0].1;
3111        let parsed: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
3112
3113        // The manifest is at the top level.
3114        assert_eq!(parsed["manifest"]["config"]["seed"], 42);
3115        assert_eq!(parsed["manifest"]["schema_version"], 1);
3116
3117        // The failure info is present.
3118        assert!(
3119            parsed["failure"]["outcome"]["Panicked"]["message"]
3120                .as_str()
3121                .unwrap()
3122                .contains("balance >= 0"),
3123            "failure message should be preserved"
3124        );
3125
3126        crate::test_complete!("walkthrough_02_write_and_read_artifact");
3127    }
3128
3129    /// Step 3: Canonical fingerprint is schedule-independent.
3130    ///
3131    /// Two schedules that differ only in the order of independent events
3132    /// produce the same fingerprint (same Foata normal form).
3133    #[test]
3134    fn walkthrough_03_fingerprint_interpretation() {
3135        use crate::trace::canonicalize::trace_fingerprint;
3136
3137        init_test("walkthrough_03_fingerprint_interpretation");
3138
3139        // Schedule A: task 1 polled before task 2
3140        let schedule_a = vec![
3141            TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3142            TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3143            TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3144            TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3145            TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3146            TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3147        ];
3148
3149        // Schedule B: task 2 polled before task 1 (commuted independent events)
3150        let schedule_b = vec![
3151            TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3152            TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3153            TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3154            TraceEvent::poll(4, Time::from_nanos(100), tid(2), rid(1)), // swapped
3155            TraceEvent::poll(5, Time::from_nanos(100), tid(1), rid(1)), // swapped
3156            TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3157        ];
3158
3159        let fp_a = trace_fingerprint(&schedule_a);
3160        let fp_b = trace_fingerprint(&schedule_b);
3161
3162        // Same fingerprint: the two schedules are equivalent modulo
3163        // commutation of independent events (polls at the same virtual time
3164        // on different tasks in the same region).
3165        assert_eq!(
3166            fp_a, fp_b,
3167            "equivalent schedules should have the same canonical fingerprint"
3168        );
3169
3170        crate::test_complete!("walkthrough_03_fingerprint_interpretation");
3171    }
3172
3173    /// Step 4: Replay command generation.
3174    ///
3175    /// The crash pack generates a shell one-liner that reproduces the failure.
3176    /// Two modes: `cargo test` (development) and `asupersync trace replay` (CLI).
3177    #[test]
3178    fn walkthrough_04_replay_command() {
3179        init_test("walkthrough_04_replay_command");
3180
3181        let pack = walkthrough_pack();
3182
3183        // -- cargo test mode --
3184        let replay = pack.replay_command(None);
3185        assert_eq!(replay.program, "cargo");
3186        assert!(replay.args.contains(&"--seed".to_string()));
3187        assert!(replay.args.contains(&"42".to_string()));
3188
3189        // The command_line is a shell-ready string.
3190        assert!(
3191            replay.command_line.contains("cargo test"),
3192            "command line should contain cargo test: {}",
3193            replay.command_line
3194        );
3195        assert!(
3196            replay.command_line.contains("--seed 42"),
3197            "command line should contain seed: {}",
3198            replay.command_line
3199        );
3200
3201        // -- With artifact path --
3202        let replay_with_path = pack.replay_command(Some("/tmp/crashpacks/my_pack.json"));
3203        assert!(
3204            replay_with_path
3205                .command_line
3206                .contains("/tmp/crashpacks/my_pack.json"),
3207            "command line should reference artifact: {}",
3208            replay_with_path.command_line
3209        );
3210
3211        // -- CLI mode --
3212        let cli_replay =
3213            ReplayCommand::from_config_cli(&pack.manifest.config, "/tmp/crashpack.json");
3214        assert_eq!(cli_replay.program, "asupersync");
3215        assert!(
3216            cli_replay.command_line.contains("trace replay"),
3217            "CLI mode should use 'trace replay' subcommand: {}",
3218            cli_replay.command_line
3219        );
3220
3221        // -- Display shows the one-liner --
3222        let display = format!("{replay}");
3223        assert_eq!(display, replay.command_line);
3224
3225        crate::test_complete!("walkthrough_04_replay_command");
3226    }
3227
3228    /// Step 5: Prefix minimization shrinks the divergent prefix.
3229    ///
3230    /// Given a long replay trace, minimization finds the shortest prefix
3231    /// that still reproduces the failure. This is the "bisect" phase.
3232    #[test]
3233    fn walkthrough_05_minimization() {
3234        use crate::trace::divergence::{MinimizationConfig, minimize_divergent_prefix};
3235        use crate::trace::replay::{ReplayEvent, ReplayTrace, TraceMetadata};
3236
3237        init_test("walkthrough_05_minimization");
3238
3239        // -- Simulate a long replay trace (50 events) --
3240        let replay_events: Vec<_> = (0..50)
3241            .map(|i| ReplayEvent::RngValue { value: i })
3242            .collect();
3243
3244        let trace = ReplayTrace {
3245            metadata: TraceMetadata::new(42),
3246            events: replay_events,
3247            cursor: 0,
3248        };
3249
3250        // Oracle: the failure reproduces when prefix length >= 15.
3251        let failure_threshold = 15;
3252        let result = minimize_divergent_prefix(&trace, &MinimizationConfig::default(), |prefix| {
3253            prefix.len() >= failure_threshold
3254        });
3255
3256        assert_eq!(result.minimized_len, failure_threshold);
3257        assert_eq!(result.original_len, 50);
3258
3259        // -- Embed the minimized prefix into a crash pack --
3260        let config = CrashPackConfig {
3261            seed: 42,
3262            config_hash: 0xCAFE,
3263            worker_count: 2,
3264            max_steps: Some(500),
3265            commit_hash: Some("a1b2c3d".to_string()),
3266        };
3267
3268        let failure = FailureInfo {
3269            task: tid(2),
3270            region: rid(1),
3271            outcome: FailureOutcome::Panicked {
3272                message: "assertion failed: balance >= 0".to_string(),
3273            },
3274            virtual_time: Time::from_nanos(200),
3275        };
3276
3277        let pack = CrashPack::builder(config)
3278            .failure(failure)
3279            .divergent_prefix(result.prefix.events)
3280            .fingerprint(0xABCD)
3281            .build()
3282            .expect("crash pack builder should have failure metadata");
3283
3284        assert!(pack.has_divergent_prefix());
3285        assert_eq!(
3286            pack.divergent_prefix.len(),
3287            failure_threshold,
3288            "minimized prefix should be {failure_threshold} events, not {}",
3289            pack.divergent_prefix.len()
3290        );
3291
3292        // Attachment table reflects the divergent prefix.
3293        assert!(
3294            pack.manifest
3295                .has_attachment(&AttachmentKind::DivergentPrefix)
3296        );
3297        let att = pack
3298            .manifest
3299            .attachment(&AttachmentKind::DivergentPrefix)
3300            .unwrap();
3301        assert_eq!(att.item_count, failure_threshold as u64);
3302
3303        crate::test_complete!("walkthrough_05_minimization");
3304    }
3305
3306    /// Helper: build the walkthrough crash pack used by multiple steps.
3307    fn walkthrough_pack() -> CrashPack {
3308        let events = vec![
3309            TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3310            TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3311            TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3312            TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3313            TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3314            TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3315        ];
3316
3317        let config = CrashPackConfig {
3318            seed: 42,
3319            config_hash: 0xCAFE,
3320            worker_count: 2,
3321            max_steps: Some(500),
3322            commit_hash: Some("a1b2c3d".to_string()),
3323        };
3324
3325        let failure = FailureInfo {
3326            task: tid(2),
3327            region: rid(1),
3328            outcome: FailureOutcome::Panicked {
3329                message: "assertion failed: balance >= 0".to_string(),
3330            },
3331            virtual_time: Time::from_nanos(200),
3332        };
3333
3334        CrashPack::builder(config)
3335            .failure(failure)
3336            .from_trace(&events)
3337            .oracle_violations(vec!["balance-invariant".to_string()])
3338            .build()
3339            .expect("crash pack builder should have failure metadata")
3340    }
3341
3342    // --- wave 75 trait coverage ---
3343
3344    #[test]
3345    fn crash_pack_config_debug_clone_eq_default() {
3346        let c = CrashPackConfig::default();
3347        assert_eq!(c.seed, 0);
3348        assert_eq!(c.config_hash, 0);
3349        assert_eq!(c.worker_count, 1);
3350        assert_eq!(c.max_steps, None);
3351        assert_eq!(c.commit_hash, None);
3352        let c2 = c.clone();
3353        assert_eq!(c, c2);
3354        let dbg = format!("{c:?}");
3355        assert!(dbg.contains("CrashPackConfig"));
3356    }
3357
3358    #[test]
3359    fn failure_outcome_debug_clone_eq() {
3360        let e = FailureOutcome::Err;
3361        let e2 = e.clone();
3362        assert_eq!(e, e2);
3363        assert_ne!(
3364            e,
3365            FailureOutcome::Panicked {
3366                message: "boom".into()
3367            }
3368        );
3369        let c = FailureOutcome::Cancelled {
3370            cancel_kind: CancelKind::User,
3371        };
3372        let c2 = c.clone();
3373        assert_eq!(c, c2);
3374        let dbg = format!("{e:?}");
3375        assert!(dbg.contains("Err"));
3376    }
3377
3378    #[test]
3379    fn attachment_kind_debug_clone_eq() {
3380        let a = AttachmentKind::CanonicalPrefix;
3381        let a2 = a.clone();
3382        assert_eq!(a, a2);
3383        assert_ne!(a, AttachmentKind::DivergentPrefix);
3384        assert_ne!(a, AttachmentKind::EvidenceLedger);
3385        assert_ne!(a, AttachmentKind::SupervisionLog);
3386        assert_ne!(a, AttachmentKind::OracleViolations);
3387        let custom = AttachmentKind::Custom {
3388            tag: "my_data".into(),
3389        };
3390        let custom2 = custom.clone();
3391        assert_eq!(custom, custom2);
3392        let dbg = format!("{a:?}");
3393        assert!(dbg.contains("CanonicalPrefix"));
3394    }
3395
3396    #[test]
3397    fn manifest_validation_error_debug_clone_eq() {
3398        let e = ManifestValidationError::VersionTooNew {
3399            manifest_version: 5,
3400            supported_version: 1,
3401        };
3402        let e2 = e.clone();
3403        assert_eq!(e, e2);
3404        assert_ne!(
3405            e,
3406            ManifestValidationError::VersionTooOld {
3407                manifest_version: 0,
3408                minimum_version: 1,
3409            }
3410        );
3411        let dbg = format!("{e:?}");
3412        assert!(dbg.contains("VersionTooNew"));
3413    }
3414
3415    #[test]
3416    fn evidence_entry_snapshot_debug_clone_eq() {
3417        let s = EvidenceEntrySnapshot {
3418            birth: 0,
3419            death: 5,
3420            is_novel: true,
3421            persistence: Some(5),
3422        };
3423        let s2 = s.clone();
3424        assert_eq!(s, s2);
3425        let dbg = format!("{s:?}");
3426        assert!(dbg.contains("EvidenceEntrySnapshot"));
3427    }
3428
3429    #[test]
3430    fn supervision_snapshot_debug_clone_eq() {
3431        let s = SupervisionSnapshot {
3432            virtual_time: Time::from_secs(1),
3433            task: tid(1),
3434            region: rid(0),
3435            decision: "restart".into(),
3436            context: Some("attempt 2".into()),
3437        };
3438        let s2 = s.clone();
3439        assert_eq!(s, s2);
3440        let dbg = format!("{s:?}");
3441        assert!(dbg.contains("SupervisionSnapshot"));
3442    }
3443
3444    #[test]
3445    fn manifest_attachment_debug_clone_eq() {
3446        let a = ManifestAttachment {
3447            kind: AttachmentKind::EvidenceLedger,
3448            item_count: 10,
3449            size_hint_bytes: 256,
3450        };
3451        let a2 = a.clone();
3452        assert_eq!(a, a2);
3453        let dbg = format!("{a:?}");
3454        assert!(dbg.contains("ManifestAttachment"));
3455    }
3456
3457    #[test]
3458    fn crash_pack_manifest_debug_clone_eq() {
3459        let m = CrashPackManifest {
3460            schema_version: CRASHPACK_SCHEMA_VERSION,
3461            config: CrashPackConfig::default(),
3462            fingerprint: 0xABCD,
3463            event_count: 100,
3464            created_at: 0,
3465            attachments: vec![],
3466        };
3467        let m2 = m.clone();
3468        assert_eq!(m, m2);
3469        let dbg = format!("{m:?}");
3470        assert!(dbg.contains("CrashPackManifest"));
3471    }
3472}