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