Skip to main content

coding_agent_search/
crash_replay.rs

1//! Deterministic crash/replay harness for state-machine proof tests.
2//!
3//! The harness is intentionally small and data-only: production code exposes
4//! named checkpoints, tests simulate a crash at each checkpoint, then restart
5//! and verify invariants. The resulting report can be saved as a JSON artifact
6//! for later replay or review.
7
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeSet;
10use std::error::Error;
11use std::fmt;
12use std::fs;
13use std::io;
14use std::path::{Path, PathBuf};
15
16pub const CRASH_REPLAY_SCHEMA_VERSION: &str = "1";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct CrashReplayCheckpoint {
20    pub id: String,
21    pub ordinal: u32,
22    pub description: String,
23}
24
25impl CrashReplayCheckpoint {
26    pub fn new(ordinal: u32, id: impl Into<String>, description: impl Into<String>) -> Self {
27        Self {
28            id: id.into(),
29            ordinal,
30            description: description.into(),
31        }
32    }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum CrashReplayPhase {
38    AdvanceToCheckpoint,
39    InjectCrash,
40    Restart,
41    CheckInvariants,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct CrashReplayEvent {
46    pub checkpoint_id: String,
47    pub phase: CrashReplayPhase,
48    pub ok: bool,
49    pub detail: String,
50}
51
52impl CrashReplayEvent {
53    fn ok(
54        checkpoint: &CrashReplayCheckpoint,
55        phase: CrashReplayPhase,
56        detail: impl Into<String>,
57    ) -> Self {
58        Self {
59            checkpoint_id: checkpoint.id.clone(),
60            phase,
61            ok: true,
62            detail: detail.into(),
63        }
64    }
65
66    fn failed(
67        checkpoint: &CrashReplayCheckpoint,
68        phase: CrashReplayPhase,
69        detail: impl Into<String>,
70    ) -> Self {
71        Self {
72            checkpoint_id: checkpoint.id.clone(),
73            phase,
74            ok: false,
75            detail: detail.into(),
76        }
77    }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct CrashReplayInvariant {
82    pub checkpoint_id: String,
83    pub name: String,
84    pub passed: bool,
85    pub detail: String,
86}
87
88impl CrashReplayInvariant {
89    pub fn passed(
90        checkpoint: &CrashReplayCheckpoint,
91        name: impl Into<String>,
92        detail: impl Into<String>,
93    ) -> Self {
94        Self {
95            checkpoint_id: checkpoint.id.clone(),
96            name: name.into(),
97            passed: true,
98            detail: detail.into(),
99        }
100    }
101
102    pub fn failed(
103        checkpoint: &CrashReplayCheckpoint,
104        name: impl Into<String>,
105        detail: impl Into<String>,
106    ) -> Self {
107        Self {
108            checkpoint_id: checkpoint.id.clone(),
109            name: name.into(),
110            passed: false,
111            detail: detail.into(),
112        }
113    }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum CrashReplayVerdict {
119    Clean,
120    Failed,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct CrashReplayReport {
125    pub schema_version: String,
126    pub scenario_id: String,
127    pub state_machine: String,
128    pub verdict: CrashReplayVerdict,
129    pub checkpoints: Vec<CrashReplayCheckpoint>,
130    pub events: Vec<CrashReplayEvent>,
131    pub invariants: Vec<CrashReplayInvariant>,
132}
133
134impl CrashReplayReport {
135    pub fn validate(&self) -> Result<(), CrashReplayValidationError> {
136        if self.schema_version != CRASH_REPLAY_SCHEMA_VERSION {
137            return Err(CrashReplayValidationError::UnsupportedSchemaVersion {
138                expected: CRASH_REPLAY_SCHEMA_VERSION,
139                actual: self.schema_version.clone(),
140            });
141        }
142        if self.scenario_id.trim().is_empty() {
143            return Err(CrashReplayValidationError::EmptyScenarioId);
144        }
145        if self.state_machine.trim().is_empty() {
146            return Err(CrashReplayValidationError::EmptyStateMachine);
147        }
148        if self.checkpoints.is_empty() {
149            return Err(CrashReplayValidationError::NoCheckpoints);
150        }
151        if self.verdict == CrashReplayVerdict::Clean && self.invariants.is_empty() {
152            return Err(CrashReplayValidationError::CleanReportWithoutInvariants);
153        }
154
155        let mut checkpoint_ids = BTreeSet::new();
156        let mut previous_ordinal = None;
157        for (index, checkpoint) in self.checkpoints.iter().enumerate() {
158            if checkpoint.id.trim().is_empty() {
159                return Err(CrashReplayValidationError::EmptyCheckpointId { index });
160            }
161            if checkpoint.description.trim().is_empty() {
162                return Err(CrashReplayValidationError::EmptyCheckpointDescription { index });
163            }
164            if let Some(previous) = previous_ordinal
165                && checkpoint.ordinal <= previous
166            {
167                return Err(CrashReplayValidationError::NonMonotoneCheckpointOrdinal {
168                    index,
169                    previous,
170                    current: checkpoint.ordinal,
171                });
172            }
173            if !checkpoint_ids.insert(checkpoint.id.as_str()) {
174                return Err(CrashReplayValidationError::DuplicateCheckpointId {
175                    index,
176                    checkpoint_id: checkpoint.id.clone(),
177                });
178            }
179            previous_ordinal = Some(checkpoint.ordinal);
180        }
181
182        let mut checked_checkpoints = BTreeSet::new();
183        for (index, event) in self.events.iter().enumerate() {
184            if event.checkpoint_id.trim().is_empty() {
185                return Err(CrashReplayValidationError::EmptyEventCheckpointId { index });
186            }
187            if !checkpoint_ids.contains(event.checkpoint_id.as_str()) {
188                return Err(CrashReplayValidationError::UnknownEventCheckpoint {
189                    index,
190                    checkpoint_id: event.checkpoint_id.clone(),
191                });
192            }
193            if event.detail.trim().is_empty() {
194                return Err(CrashReplayValidationError::EmptyEventDetail { index });
195            }
196            if event.ok && event.phase == CrashReplayPhase::CheckInvariants {
197                checked_checkpoints.insert(event.checkpoint_id.as_str());
198            }
199        }
200
201        let mut invariant_checkpoints = BTreeSet::new();
202        for (index, invariant) in self.invariants.iter().enumerate() {
203            if invariant.checkpoint_id.trim().is_empty() {
204                return Err(CrashReplayValidationError::EmptyInvariantCheckpointId { index });
205            }
206            if !checkpoint_ids.contains(invariant.checkpoint_id.as_str()) {
207                return Err(CrashReplayValidationError::UnknownInvariantCheckpoint {
208                    index,
209                    checkpoint_id: invariant.checkpoint_id.clone(),
210                });
211            }
212            if invariant.name.trim().is_empty() {
213                return Err(CrashReplayValidationError::EmptyInvariantName { index });
214            }
215            if invariant.detail.trim().is_empty() {
216                return Err(CrashReplayValidationError::EmptyInvariantDetail { index });
217            }
218            if invariant.passed {
219                invariant_checkpoints.insert(invariant.checkpoint_id.as_str());
220            }
221        }
222        if self.verdict == CrashReplayVerdict::Clean
223            && (self.events.iter().any(|event| !event.ok)
224                || self.invariants.iter().any(|invariant| !invariant.passed))
225        {
226            return Err(CrashReplayValidationError::CleanReportContainsFailure);
227        }
228        if self.verdict == CrashReplayVerdict::Clean {
229            if self.events.is_empty() {
230                return Err(CrashReplayValidationError::CleanReportWithoutEvents);
231            }
232            for checkpoint in &self.checkpoints {
233                if !checked_checkpoints.contains(checkpoint.id.as_str()) {
234                    return Err(
235                        CrashReplayValidationError::CleanReportMissingCheckpointEvent {
236                            checkpoint_id: checkpoint.id.clone(),
237                        },
238                    );
239                }
240                if !invariant_checkpoints.contains(checkpoint.id.as_str()) {
241                    return Err(
242                        CrashReplayValidationError::CleanReportMissingCheckpointInvariant {
243                            checkpoint_id: checkpoint.id.clone(),
244                        },
245                    );
246                }
247            }
248        }
249
250        Ok(())
251    }
252
253    pub fn save_json(&self, path: &Path) -> Result<(), CrashReplayIoError> {
254        self.validate()?;
255        if let Some(parent) = path
256            .parent()
257            .filter(|parent| !parent.as_os_str().is_empty())
258        {
259            fs::create_dir_all(parent)?;
260        }
261        let json = serde_json::to_vec_pretty(self)?;
262        let temp_path = write_crash_replay_json_temp_file(path, &json)?;
263        replace_crash_replay_json_from_temp(&temp_path, path)?;
264        Ok(())
265    }
266
267    pub fn load_json(path: &Path) -> Result<Self, CrashReplayIoError> {
268        let bytes = fs::read(path)?;
269        let report: Self = serde_json::from_slice(&bytes)?;
270        report.validate()?;
271        Ok(report)
272    }
273}
274
275fn write_crash_replay_json_temp_file(path: &Path, contents: &[u8]) -> io::Result<PathBuf> {
276    for _ in 0..100 {
277        let temp_path = unique_crash_replay_json_temp_path(path)?;
278        match write_crash_replay_json_temp_file_at(&temp_path, contents) {
279            Ok(()) => return Ok(temp_path),
280            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => continue,
281            Err(err) => return Err(err),
282        }
283    }
284
285    Err(io::Error::new(
286        io::ErrorKind::AlreadyExists,
287        format!(
288            "failed to allocate unique crash replay temp path for {}",
289            path.display()
290        ),
291    ))
292}
293
294fn write_crash_replay_json_temp_file_at(path: &Path, contents: &[u8]) -> io::Result<()> {
295    use std::io::Write;
296
297    let mut file = fs::OpenOptions::new()
298        .write(true)
299        .create_new(true)
300        .open(path)?;
301    file.write_all(contents)?;
302    file.sync_all()
303}
304
305fn replace_crash_replay_json_from_temp(temp_path: &Path, final_path: &Path) -> io::Result<()> {
306    fs::rename(temp_path, final_path)?;
307    sync_parent_directory(final_path)
308}
309
310#[cfg(not(windows))]
311fn sync_parent_directory(path: &Path) -> io::Result<()> {
312    let Some(parent) = path.parent() else {
313        return Ok(());
314    };
315    fs::File::open(parent)?.sync_all()
316}
317
318#[cfg(windows)]
319fn sync_parent_directory(_path: &Path) -> io::Result<()> {
320    Ok(())
321}
322
323fn unique_crash_replay_json_temp_path(path: &Path) -> io::Result<PathBuf> {
324    let timestamp = std::time::SystemTime::now()
325        .duration_since(std::time::UNIX_EPOCH)
326        .unwrap_or_default()
327        .as_nanos();
328    let nonce = crash_replay_temp_path_nonce()?;
329    let file_name = path
330        .file_name()
331        .and_then(|name| name.to_str())
332        .unwrap_or("crash-replay-report.json");
333
334    Ok(path.with_file_name(format!(".{file_name}.tmp.{timestamp}.{nonce:016x}")))
335}
336
337fn crash_replay_temp_path_nonce() -> io::Result<u64> {
338    use ring::rand::SecureRandom;
339
340    let mut random_bytes = [0u8; 8];
341    ring::rand::SystemRandom::new()
342        .fill(&mut random_bytes)
343        .map_err(|_| io::Error::other("failed to generate crash replay temp path nonce"))?;
344    Ok(u64::from_le_bytes(random_bytes))
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct CrashReplayError {
349    pub action: String,
350    pub detail: String,
351}
352
353impl CrashReplayError {
354    pub fn new(action: impl Into<String>, detail: impl Into<String>) -> Self {
355        Self {
356            action: action.into(),
357            detail: detail.into(),
358        }
359    }
360
361    pub fn from_error(action: impl Into<String>, error: impl fmt::Display) -> Self {
362        Self::new(action, error.to_string())
363    }
364}
365
366impl fmt::Display for CrashReplayError {
367    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368        write!(f, "{}: {}", self.action, self.detail)
369    }
370}
371
372impl Error for CrashReplayError {}
373
374#[derive(Debug)]
375pub enum CrashReplayValidationError {
376    UnsupportedSchemaVersion {
377        expected: &'static str,
378        actual: String,
379    },
380    EmptyScenarioId,
381    EmptyStateMachine,
382    NoCheckpoints,
383    EmptyCheckpointId {
384        index: usize,
385    },
386    EmptyCheckpointDescription {
387        index: usize,
388    },
389    DuplicateCheckpointId {
390        index: usize,
391        checkpoint_id: String,
392    },
393    NonMonotoneCheckpointOrdinal {
394        index: usize,
395        previous: u32,
396        current: u32,
397    },
398    CleanReportWithoutInvariants,
399    CleanReportWithoutEvents,
400    CleanReportContainsFailure,
401    CleanReportMissingCheckpointEvent {
402        checkpoint_id: String,
403    },
404    CleanReportMissingCheckpointInvariant {
405        checkpoint_id: String,
406    },
407    EmptyEventCheckpointId {
408        index: usize,
409    },
410    UnknownEventCheckpoint {
411        index: usize,
412        checkpoint_id: String,
413    },
414    EmptyEventDetail {
415        index: usize,
416    },
417    EmptyInvariantCheckpointId {
418        index: usize,
419    },
420    UnknownInvariantCheckpoint {
421        index: usize,
422        checkpoint_id: String,
423    },
424    EmptyInvariantName {
425        index: usize,
426    },
427    EmptyInvariantDetail {
428        index: usize,
429    },
430}
431
432impl fmt::Display for CrashReplayValidationError {
433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434        match self {
435            Self::UnsupportedSchemaVersion { expected, actual } => {
436                write!(
437                    f,
438                    "unsupported crash replay schema version {actual}; expected {expected}"
439                )
440            }
441            Self::EmptyScenarioId => write!(f, "crash replay scenario_id cannot be empty"),
442            Self::EmptyStateMachine => write!(f, "crash replay state_machine cannot be empty"),
443            Self::NoCheckpoints => write!(f, "crash replay report must include checkpoints"),
444            Self::EmptyCheckpointId { index } => {
445                write!(f, "crash replay checkpoint #{index} has an empty id")
446            }
447            Self::EmptyCheckpointDescription { index } => write!(
448                f,
449                "crash replay checkpoint #{index} has an empty description"
450            ),
451            Self::DuplicateCheckpointId {
452                index,
453                checkpoint_id,
454            } => write!(
455                f,
456                "crash replay checkpoint #{index} duplicates checkpoint id {checkpoint_id}"
457            ),
458            Self::NonMonotoneCheckpointOrdinal {
459                index,
460                previous,
461                current,
462            } => write!(
463                f,
464                "crash replay checkpoint #{index} ordinal {current} must be greater than previous ordinal {previous}"
465            ),
466            Self::CleanReportWithoutInvariants => {
467                write!(f, "clean crash replay report must include invariants")
468            }
469            Self::CleanReportWithoutEvents => {
470                write!(f, "clean crash replay report must include events")
471            }
472            Self::CleanReportContainsFailure => {
473                write!(
474                    f,
475                    "clean crash replay report contains failed events or invariants"
476                )
477            }
478            Self::CleanReportMissingCheckpointEvent { checkpoint_id } => write!(
479                f,
480                "clean crash replay report has no successful invariant-check event for checkpoint {checkpoint_id}"
481            ),
482            Self::CleanReportMissingCheckpointInvariant { checkpoint_id } => write!(
483                f,
484                "clean crash replay report has no passing invariant for checkpoint {checkpoint_id}"
485            ),
486            Self::EmptyEventCheckpointId { index } => {
487                write!(f, "crash replay event #{index} has an empty checkpoint id")
488            }
489            Self::UnknownEventCheckpoint {
490                index,
491                checkpoint_id,
492            } => write!(
493                f,
494                "crash replay event #{index} references unknown checkpoint {checkpoint_id}"
495            ),
496            Self::EmptyEventDetail { index } => {
497                write!(f, "crash replay event #{index} has an empty detail")
498            }
499            Self::EmptyInvariantCheckpointId { index } => write!(
500                f,
501                "crash replay invariant #{index} has an empty checkpoint id"
502            ),
503            Self::UnknownInvariantCheckpoint {
504                index,
505                checkpoint_id,
506            } => write!(
507                f,
508                "crash replay invariant #{index} references unknown checkpoint {checkpoint_id}"
509            ),
510            Self::EmptyInvariantName { index } => {
511                write!(f, "crash replay invariant #{index} has an empty name")
512            }
513            Self::EmptyInvariantDetail { index } => {
514                write!(f, "crash replay invariant #{index} has an empty detail")
515            }
516        }
517    }
518}
519
520impl Error for CrashReplayValidationError {}
521
522#[derive(Debug)]
523pub enum CrashReplayIoError {
524    Io(io::Error),
525    Json(serde_json::Error),
526    Validation(CrashReplayValidationError),
527}
528
529impl fmt::Display for CrashReplayIoError {
530    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531        match self {
532            Self::Io(err) => write!(f, "crash replay I/O error: {err}"),
533            Self::Json(err) => write!(f, "crash replay JSON error: {err}"),
534            Self::Validation(err) => write!(f, "crash replay validation error: {err}"),
535        }
536    }
537}
538
539impl Error for CrashReplayIoError {
540    fn source(&self) -> Option<&(dyn Error + 'static)> {
541        match self {
542            Self::Io(err) => Some(err),
543            Self::Json(err) => Some(err),
544            Self::Validation(err) => Some(err),
545        }
546    }
547}
548
549impl From<io::Error> for CrashReplayIoError {
550    fn from(err: io::Error) -> Self {
551        Self::Io(err)
552    }
553}
554
555impl From<serde_json::Error> for CrashReplayIoError {
556    fn from(err: serde_json::Error) -> Self {
557        Self::Json(err)
558    }
559}
560
561impl From<CrashReplayValidationError> for CrashReplayIoError {
562    fn from(err: CrashReplayValidationError) -> Self {
563        Self::Validation(err)
564    }
565}
566
567pub fn replay_named_checkpoints<S, MakeState, Advance, Restart, Check>(
568    scenario_id: impl Into<String>,
569    state_machine: impl Into<String>,
570    mut checkpoints: Vec<CrashReplayCheckpoint>,
571    mut make_state: MakeState,
572    mut advance_to_checkpoint: Advance,
573    mut restart: Restart,
574    mut check_invariants: Check,
575) -> CrashReplayReport
576where
577    MakeState: FnMut() -> Result<S, CrashReplayError>,
578    Advance: FnMut(&mut S, &CrashReplayCheckpoint) -> Result<(), CrashReplayError>,
579    Restart: FnMut(&mut S) -> Result<(), CrashReplayError>,
580    Check: FnMut(&S, &CrashReplayCheckpoint) -> Vec<CrashReplayInvariant>,
581{
582    checkpoints.sort_by_key(|checkpoint| checkpoint.ordinal);
583    let mut report = CrashReplayReport {
584        schema_version: CRASH_REPLAY_SCHEMA_VERSION.to_string(),
585        scenario_id: scenario_id.into(),
586        state_machine: state_machine.into(),
587        verdict: CrashReplayVerdict::Clean,
588        checkpoints: checkpoints.clone(),
589        events: Vec::new(),
590        invariants: Vec::new(),
591    };
592
593    if checkpoints.is_empty() {
594        report.verdict = CrashReplayVerdict::Failed;
595        return report;
596    }
597
598    for checkpoint in checkpoints {
599        let mut state = match make_state() {
600            Ok(state) => state,
601            Err(err) => {
602                report.verdict = CrashReplayVerdict::Failed;
603                report.events.push(CrashReplayEvent::failed(
604                    &checkpoint,
605                    CrashReplayPhase::AdvanceToCheckpoint,
606                    format!("failed creating fresh state: {err}"),
607                ));
608                continue;
609            }
610        };
611
612        match advance_to_checkpoint(&mut state, &checkpoint) {
613            Ok(()) => report.events.push(CrashReplayEvent::ok(
614                &checkpoint,
615                CrashReplayPhase::AdvanceToCheckpoint,
616                "advanced to checkpoint",
617            )),
618            Err(err) => {
619                report.verdict = CrashReplayVerdict::Failed;
620                report.events.push(CrashReplayEvent::failed(
621                    &checkpoint,
622                    CrashReplayPhase::AdvanceToCheckpoint,
623                    err.to_string(),
624                ));
625                continue;
626            }
627        }
628
629        report.events.push(CrashReplayEvent::ok(
630            &checkpoint,
631            CrashReplayPhase::InjectCrash,
632            "simulated process stop at named checkpoint",
633        ));
634
635        match restart(&mut state) {
636            Ok(()) => report.events.push(CrashReplayEvent::ok(
637                &checkpoint,
638                CrashReplayPhase::Restart,
639                "restart action completed",
640            )),
641            Err(err) => {
642                report.verdict = CrashReplayVerdict::Failed;
643                report.events.push(CrashReplayEvent::failed(
644                    &checkpoint,
645                    CrashReplayPhase::Restart,
646                    err.to_string(),
647                ));
648                continue;
649            }
650        }
651
652        let invariants = check_invariants(&state, &checkpoint);
653        if invariants.is_empty() {
654            report.verdict = CrashReplayVerdict::Failed;
655            report.events.push(CrashReplayEvent::failed(
656                &checkpoint,
657                CrashReplayPhase::CheckInvariants,
658                "checkpoint produced no invariants",
659            ));
660            continue;
661        }
662
663        let failed = invariants.iter().any(|invariant| !invariant.passed);
664        if failed {
665            report.verdict = CrashReplayVerdict::Failed;
666        }
667        report.events.push(CrashReplayEvent {
668            checkpoint_id: checkpoint.id.clone(),
669            phase: CrashReplayPhase::CheckInvariants,
670            ok: !failed,
671            detail: format!("{} invariant(s) evaluated", invariants.len()),
672        });
673        report.invariants.extend(invariants);
674    }
675
676    report
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682    use crate::policy_registry::{
683        PolicyControllerStatus, PolicyFallbackState, policy_registry_snapshot,
684    };
685    use crate::search::policy::{
686        CHUNKING_STRATEGY_VERSION, SEMANTIC_SCHEMA_VERSION, SemanticPolicy,
687    };
688    use crate::search::semantic_manifest::{
689        ArtifactRecord, BuildCheckpoint, SemanticManifest, TierKind,
690    };
691    use serde_json::{Value, json};
692    use std::path::PathBuf;
693    use tempfile::TempDir;
694
695    #[derive(Debug)]
696    struct SemanticReplayState {
697        temp_dir: TempDir,
698        loaded: Option<SemanticManifest>,
699    }
700
701    impl SemanticReplayState {
702        fn data_dir(&self) -> &Path {
703            self.temp_dir.path()
704        }
705    }
706
707    fn semantic_checkpoint() -> BuildCheckpoint {
708        BuildCheckpoint {
709            tier: TierKind::Fast,
710            embedder_id: "fnv1a-384".to_string(),
711            last_offset: 8,
712            docs_embedded: 13,
713            conversations_processed: 2,
714            total_conversations: 5,
715            db_fingerprint: "semantic-fp".to_string(),
716            schema_version: SEMANTIC_SCHEMA_VERSION,
717            chunking_version: CHUNKING_STRATEGY_VERSION,
718            saved_at_ms: 1_700_000_000_000,
719            last_message_id: None,
720        }
721    }
722
723    fn semantic_artifact() -> ArtifactRecord {
724        ArtifactRecord {
725            tier: TierKind::Fast,
726            embedder_id: "fnv1a-384".to_string(),
727            model_revision: "hash".to_string(),
728            schema_version: SEMANTIC_SCHEMA_VERSION,
729            chunking_version: CHUNKING_STRATEGY_VERSION,
730            dimension: 384,
731            doc_count: 13,
732            conversation_count: 5,
733            db_fingerprint: "semantic-fp".to_string(),
734            index_path: "vector_index/fast.fsvi".to_string(),
735            size_bytes: 4096,
736            started_at_ms: 1_700_000_000_000,
737            completed_at_ms: 1_700_000_060_000,
738            ready: true,
739        }
740    }
741
742    #[test]
743    fn semantic_manifest_state_machine_replays_checkpoint_and_publish_crashes() {
744        let checkpoints = vec![
745            CrashReplayCheckpoint::new(
746                10,
747                "semantic_checkpoint_saved",
748                "semantic checkpoint persisted before artifact publish",
749            ),
750            CrashReplayCheckpoint::new(
751                20,
752                "semantic_artifact_published",
753                "semantic artifact published and checkpoint cleared",
754            ),
755        ];
756
757        let report =
758            replay_named_checkpoints(
759                "semantic-manifest-save-restart",
760                "semantic_manifest",
761                checkpoints,
762                || {
763                    Ok(SemanticReplayState {
764                        temp_dir: tempfile::tempdir()
765                            .map_err(|err| CrashReplayError::from_error("create tempdir", err))?,
766                        loaded: None,
767                    })
768                },
769                |state, checkpoint| {
770                    let mut manifest = SemanticManifest::default();
771                    manifest.refresh_backlog(5, "semantic-fp");
772                    manifest.save_checkpoint(semantic_checkpoint());
773                    if checkpoint.id == "semantic_artifact_published" {
774                        manifest.publish_artifact(semantic_artifact());
775                    }
776                    manifest
777                        .save(state.data_dir())
778                        .map_err(|err| CrashReplayError::from_error("save semantic manifest", err))
779                },
780                |state| {
781                    state.loaded = SemanticManifest::load(state.data_dir()).map_err(|err| {
782                        CrashReplayError::from_error("load semantic manifest", err)
783                    })?;
784                    Ok(())
785                },
786                |state, checkpoint| {
787                    let mut invariants = Vec::new();
788                    let Some(manifest) = &state.loaded else {
789                        return vec![CrashReplayInvariant::failed(
790                            checkpoint,
791                            "semantic_manifest_loaded",
792                            "manifest did not load after restart",
793                        )];
794                    };
795
796                    invariants.push(CrashReplayInvariant::passed(
797                        checkpoint,
798                        "semantic_manifest_loaded",
799                        "manifest loaded after restart",
800                    ));
801                    match checkpoint.id.as_str() {
802                        "semantic_checkpoint_saved" => {
803                            invariants.push(if manifest.checkpoint.is_some()
804                            && manifest.fast_tier.is_none()
805                        {
806                            CrashReplayInvariant::passed(
807                                checkpoint,
808                                "checkpoint_without_torn_artifact",
809                                "restart sees resumable checkpoint and no half-published artifact",
810                            )
811                        } else {
812                            CrashReplayInvariant::failed(
813                                checkpoint,
814                                "checkpoint_without_torn_artifact",
815                                format!(
816                                    "checkpoint={:?} fast_tier={:?}",
817                                    manifest.checkpoint, manifest.fast_tier
818                                ),
819                            )
820                        });
821                        }
822                        "semantic_artifact_published" => {
823                            invariants.push(if manifest.checkpoint.is_none()
824                            && manifest.fast_tier.as_ref().is_some_and(|artifact| artifact.ready)
825                        {
826                            CrashReplayInvariant::passed(
827                                checkpoint,
828                                "published_artifact_clears_checkpoint",
829                                "restart sees ready artifact and no stale matching checkpoint",
830                            )
831                        } else {
832                            CrashReplayInvariant::failed(
833                                checkpoint,
834                                "published_artifact_clears_checkpoint",
835                                format!(
836                                    "checkpoint={:?} fast_tier={:?}",
837                                    manifest.checkpoint, manifest.fast_tier
838                                ),
839                            )
840                        });
841                        }
842                        _ => invariants.push(CrashReplayInvariant::failed(
843                            checkpoint,
844                            "known_checkpoint",
845                            "unexpected semantic checkpoint",
846                        )),
847                    }
848                    invariants
849                },
850            );
851
852        assert_eq!(report.verdict, CrashReplayVerdict::Clean);
853        assert_eq!(report.checkpoints.len(), 2);
854        assert_eq!(report.invariants.len(), 4);
855        assert!(
856            report.validate().is_ok(),
857            "semantic replay report should validate: {report:?}"
858        );
859    }
860
861    #[derive(Debug)]
862    struct PolicyReplayState {
863        pipeline: Value,
864        semantic_available: bool,
865        semantic_fallback_mode: Option<&'static str>,
866        snapshot_statuses: Vec<(String, PolicyControllerStatus, PolicyFallbackState)>,
867    }
868
869    fn policy_pipeline_fixture(mode: &str, reason: &str) -> Value {
870        json!({
871            "pipeline_channel_size": 128,
872            "pipeline_max_message_bytes_in_flight": 1048576,
873            "page_prep_workers": 12,
874            "staged_merge_workers": 4,
875            "staged_shard_builders": 8,
876            "controller_mode": "auto",
877            "controller_restore_clear_samples": 3,
878            "controller_restore_hold_ms": 5000,
879            "controller_loadavg_high_watermark_1m": 1.75,
880            "controller_loadavg_low_watermark_1m": 0.75,
881            "runtime": {
882                "controller_mode": mode,
883                "controller_reason": reason
884            }
885        })
886    }
887
888    #[test]
889    fn policy_registry_state_machine_replays_deterministic_controller_snapshots() {
890        let checkpoints = vec![
891            CrashReplayCheckpoint::new(
892                10,
893                "semantic_fallback_snapshot",
894                "semantic controller reports lexical fallback",
895            ),
896            CrashReplayCheckpoint::new(
897                20,
898                "lexical_throttle_snapshot",
899                "lexical rebuild controller reports pressure fallback",
900            ),
901        ];
902
903        let report = replay_named_checkpoints(
904            "policy-registry-recompute-restart",
905            "policy_registry",
906            checkpoints,
907            || {
908                Ok(PolicyReplayState {
909                    pipeline: policy_pipeline_fixture("steady", "pipeline settings active"),
910                    semantic_available: true,
911                    semantic_fallback_mode: None,
912                    snapshot_statuses: Vec::new(),
913                })
914            },
915            |state, checkpoint| {
916                match checkpoint.id.as_str() {
917                    "semantic_fallback_snapshot" => {
918                        state.semantic_available = false;
919                        state.semantic_fallback_mode = Some("lexical");
920                    }
921                    "lexical_throttle_snapshot" => {
922                        state.pipeline =
923                            policy_pipeline_fixture("throttled", "load pressure reduced workers");
924                    }
925                    _ => {
926                        return Err(CrashReplayError::new(
927                            "advance policy checkpoint",
928                            "unknown checkpoint",
929                        ));
930                    }
931                }
932                Ok(())
933            },
934            |state| {
935                let policy = SemanticPolicy::compiled_defaults();
936                let snapshot = policy_registry_snapshot(
937                    &policy,
938                    state.semantic_available,
939                    state.semantic_fallback_mode,
940                    &state.pipeline,
941                );
942                state.snapshot_statuses = snapshot
943                    .controllers
944                    .into_iter()
945                    .map(|controller| {
946                        (
947                            controller.controller_id,
948                            controller.status,
949                            controller.fallback_state,
950                        )
951                    })
952                    .collect();
953                Ok(())
954            },
955            |state, checkpoint| {
956                let ids: Vec<_> = state
957                    .snapshot_statuses
958                    .iter()
959                    .map(|(id, _, _)| id.as_str())
960                    .collect();
961                let mut invariants =
962                    vec![if ids == ["lexical_rebuild_pipeline", "semantic_search"] {
963                        CrashReplayInvariant::passed(
964                            checkpoint,
965                            "controller_ids_sorted",
966                            "controller ids are deterministic and sorted",
967                        )
968                    } else {
969                        CrashReplayInvariant::failed(
970                            checkpoint,
971                            "controller_ids_sorted",
972                            format!("unexpected controller ids: {ids:?}"),
973                        )
974                    }];
975
976                let expected_controller = match checkpoint.id.as_str() {
977                    "semantic_fallback_snapshot" => "semantic_search",
978                    "lexical_throttle_snapshot" => "lexical_rebuild_pipeline",
979                    _ => "unknown",
980                };
981                let controller = state
982                    .snapshot_statuses
983                    .iter()
984                    .find(|(id, _, _)| id == expected_controller);
985                invariants.push(match controller {
986                    Some((
987                        _id,
988                        PolicyControllerStatus::Fallback,
989                        PolicyFallbackState::Conservative,
990                    )) => CrashReplayInvariant::passed(
991                        checkpoint,
992                        "conservative_fallback_reported",
993                        "checkpoint recompute reports conservative fallback",
994                    ),
995                    other => CrashReplayInvariant::failed(
996                        checkpoint,
997                        "conservative_fallback_reported",
998                        format!("unexpected controller status: {other:?}"),
999                    ),
1000                });
1001                invariants
1002            },
1003        );
1004
1005        assert_eq!(report.verdict, CrashReplayVerdict::Clean);
1006        assert!(
1007            report.validate().is_ok(),
1008            "policy replay report should validate: {report:?}"
1009        );
1010    }
1011
1012    #[derive(Debug)]
1013    struct LexicalPublishFixtureState {
1014        temp_dir: TempDir,
1015        live_path: PathBuf,
1016        staged_path: PathBuf,
1017        backup_path: PathBuf,
1018    }
1019
1020    impl LexicalPublishFixtureState {
1021        fn new() -> Result<Self, CrashReplayError> {
1022            let temp_dir = tempfile::tempdir()
1023                .map_err(|err| CrashReplayError::from_error("create tempdir", err))?;
1024            let live_path = temp_dir.path().join("live-generation.txt");
1025            let staged_path = temp_dir.path().join("staged-generation.txt");
1026            let backup_path = temp_dir.path().join("live-generation.bak");
1027            fs::write(&live_path, "old-generation")
1028                .map_err(|err| CrashReplayError::from_error("seed live generation", err))?;
1029            Ok(Self {
1030                temp_dir,
1031                live_path,
1032                staged_path,
1033                backup_path,
1034            })
1035        }
1036
1037        fn write_staged(&self) -> Result<(), CrashReplayError> {
1038            fs::write(&self.staged_path, "new-generation")
1039                .map_err(|err| CrashReplayError::from_error("write staged generation", err))
1040        }
1041
1042        fn park_live(&self) -> Result<(), CrashReplayError> {
1043            fs::rename(&self.live_path, &self.backup_path)
1044                .map_err(|err| CrashReplayError::from_error("park live generation", err))
1045        }
1046
1047        fn publish_staged(&self) -> Result<(), CrashReplayError> {
1048            fs::rename(&self.staged_path, &self.live_path)
1049                .map_err(|err| CrashReplayError::from_error("publish staged generation", err))
1050        }
1051    }
1052
1053    #[test]
1054    fn lexical_publish_fixture_replays_park_and_swap_crash_windows() {
1055        let checkpoints = vec![
1056            CrashReplayCheckpoint::new(
1057                10,
1058                "staged_written",
1059                "staged generation exists before live path is touched",
1060            ),
1061            CrashReplayCheckpoint::new(
1062                20,
1063                "live_parked",
1064                "live generation has been parked but staged is not yet live",
1065            ),
1066            CrashReplayCheckpoint::new(
1067                30,
1068                "staged_published",
1069                "staged generation has been promoted to live",
1070            ),
1071        ];
1072
1073        let report = replay_named_checkpoints(
1074            "lexical-publish-fixture-restart",
1075            "lexical_publish",
1076            checkpoints,
1077            LexicalPublishFixtureState::new,
1078            |state, checkpoint| {
1079                state.write_staged()?;
1080                match checkpoint.id.as_str() {
1081                    "staged_written" => {}
1082                    "live_parked" => {
1083                        state.park_live()?;
1084                    }
1085                    "staged_published" => {
1086                        state.park_live()?;
1087                        state.publish_staged()?;
1088                    }
1089                    _ => {
1090                        return Err(CrashReplayError::new(
1091                            "advance lexical publish checkpoint",
1092                            "unknown checkpoint",
1093                        ));
1094                    }
1095                }
1096                Ok(())
1097            },
1098            |state| {
1099                if !state.live_path.exists() && state.backup_path.exists() {
1100                    fs::rename(&state.backup_path, &state.live_path)
1101                        .map_err(|err| CrashReplayError::from_error("restore parked live", err))?;
1102                }
1103                Ok(())
1104            },
1105            |state, checkpoint| {
1106                let live = fs::read_to_string(&state.live_path).ok();
1107                let expected = match checkpoint.id.as_str() {
1108                    "staged_written" | "live_parked" => "old-generation",
1109                    "staged_published" => "new-generation",
1110                    _ => "unknown",
1111                };
1112
1113                vec![
1114                    if state.temp_dir.path().exists() {
1115                        CrashReplayInvariant::passed(
1116                            checkpoint,
1117                            "fixture_root_retained",
1118                            "fixture root remains available for artifact inspection",
1119                        )
1120                    } else {
1121                        CrashReplayInvariant::failed(
1122                            checkpoint,
1123                            "fixture_root_retained",
1124                            "fixture root disappeared before invariant checks",
1125                        )
1126                    },
1127                    if live.as_deref() == Some(expected) {
1128                        CrashReplayInvariant::passed(
1129                            checkpoint,
1130                            "live_generation_is_old_or_new",
1131                            format!("live generation recovered as {expected}"),
1132                        )
1133                    } else {
1134                        CrashReplayInvariant::failed(
1135                            checkpoint,
1136                            "live_generation_is_old_or_new",
1137                            format!("expected {expected}, got {live:?}"),
1138                        )
1139                    },
1140                ]
1141            },
1142        );
1143
1144        assert_eq!(report.verdict, CrashReplayVerdict::Clean);
1145        assert!(
1146            report.validate().is_ok(),
1147            "lexical publish replay report should validate: {report:?}"
1148        );
1149    }
1150
1151    #[derive(Debug)]
1152    struct BackupRecoveryFixtureState {
1153        temp_dir: TempDir,
1154        canonical_db: PathBuf,
1155        backup_dir: PathBuf,
1156        manifest: Option<Value>,
1157    }
1158
1159    impl BackupRecoveryFixtureState {
1160        fn new() -> Result<Self, CrashReplayError> {
1161            let temp_dir = tempfile::tempdir()
1162                .map_err(|err| CrashReplayError::from_error("create tempdir", err))?;
1163            let canonical_db = temp_dir.path().join("cass.db");
1164            let backup_dir = temp_dir.path().join("backup");
1165            fs::write(&canonical_db, "canonical-main")
1166                .map_err(|err| CrashReplayError::from_error("seed canonical db", err))?;
1167            fs::write(temp_dir.path().join("cass.db-wal"), "canonical-wal")
1168                .map_err(|err| CrashReplayError::from_error("seed canonical wal", err))?;
1169            fs::create_dir_all(&backup_dir)
1170                .map_err(|err| CrashReplayError::from_error("create backup dir", err))?;
1171            Ok(Self {
1172                temp_dir,
1173                canonical_db,
1174                backup_dir,
1175                manifest: None,
1176            })
1177        }
1178
1179        fn copy_main(&self) -> Result<(), CrashReplayError> {
1180            fs::copy(&self.canonical_db, self.backup_dir.join("cass.db"))
1181                .map(|_| ())
1182                .map_err(|err| CrashReplayError::from_error("copy backup main", err))
1183        }
1184
1185        fn copy_wal_and_manifest(&self) -> Result<(), CrashReplayError> {
1186            fs::copy(
1187                self.temp_dir.path().join("cass.db-wal"),
1188                self.backup_dir.join("cass.db-wal"),
1189            )
1190            .map_err(|err| CrashReplayError::from_error("copy backup wal", err))?;
1191            let manifest = json!({
1192                "schema_version": 1,
1193                "complete": true,
1194                "files": ["cass.db", "cass.db-wal"],
1195            });
1196            let bytes = serde_json::to_vec_pretty(&manifest)
1197                .map_err(|err| CrashReplayError::from_error("encode backup manifest", err))?;
1198            fs::write(self.backup_dir.join("manifest.json"), bytes)
1199                .map_err(|err| CrashReplayError::from_error("write backup manifest", err))
1200        }
1201    }
1202
1203    #[test]
1204    fn backup_recovery_fixture_replays_incomplete_and_complete_bundle_crashes() {
1205        let checkpoints = vec![
1206            CrashReplayCheckpoint::new(
1207                10,
1208                "backup_main_copied",
1209                "backup main file copied before bundle manifest exists",
1210            ),
1211            CrashReplayCheckpoint::new(
1212                20,
1213                "backup_manifest_written",
1214                "backup sidecars and manifest mark the bundle complete",
1215            ),
1216        ];
1217
1218        let report = replay_named_checkpoints(
1219            "backup-recovery-fixture-restart",
1220            "backup_recovery",
1221            checkpoints,
1222            BackupRecoveryFixtureState::new,
1223            |state, checkpoint| {
1224                state.copy_main()?;
1225                match checkpoint.id.as_str() {
1226                    "backup_main_copied" => {}
1227                    "backup_manifest_written" => {
1228                        state.copy_wal_and_manifest()?;
1229                    }
1230                    _ => {
1231                        return Err(CrashReplayError::new(
1232                            "advance backup recovery checkpoint",
1233                            "unknown checkpoint",
1234                        ));
1235                    }
1236                }
1237                Ok(())
1238            },
1239            |state| {
1240                let manifest_path = state.backup_dir.join("manifest.json");
1241                state.manifest = if manifest_path.exists() {
1242                    let bytes = fs::read(&manifest_path)
1243                        .map_err(|err| CrashReplayError::from_error("read backup manifest", err))?;
1244                    Some(serde_json::from_slice(&bytes).map_err(|err| {
1245                        CrashReplayError::from_error("parse backup manifest", err)
1246                    })?)
1247                } else {
1248                    None
1249                };
1250                Ok(())
1251            },
1252            |state, checkpoint| {
1253                let canonical = fs::read_to_string(&state.canonical_db).ok();
1254                let mut invariants = vec![if canonical.as_deref() == Some("canonical-main") {
1255                    CrashReplayInvariant::passed(
1256                        checkpoint,
1257                        "canonical_db_preserved",
1258                        "restart did not replace the canonical DB from an incomplete backup",
1259                    )
1260                } else {
1261                    CrashReplayInvariant::failed(
1262                        checkpoint,
1263                        "canonical_db_preserved",
1264                        format!("unexpected canonical DB content: {canonical:?}"),
1265                    )
1266                }];
1267
1268                match checkpoint.id.as_str() {
1269                    "backup_main_copied" => {
1270                        invariants.push(if state.manifest.is_none() {
1271                            CrashReplayInvariant::passed(
1272                                checkpoint,
1273                                "partial_backup_not_marked_complete",
1274                                "main-only backup has no manifest and is not advertised recoverable",
1275                            )
1276                        } else {
1277                            CrashReplayInvariant::failed(
1278                                checkpoint,
1279                                "partial_backup_not_marked_complete",
1280                                format!("unexpected manifest: {:?}", state.manifest),
1281                            )
1282                        });
1283                    }
1284                    "backup_manifest_written" => {
1285                        let complete = state
1286                            .manifest
1287                            .as_ref()
1288                            .and_then(|manifest| manifest.get("complete"))
1289                            .and_then(Value::as_bool)
1290                            == Some(true);
1291                        let files_match = state
1292                            .manifest
1293                            .as_ref()
1294                            .and_then(|manifest| manifest.get("files"))
1295                            .and_then(Value::as_array)
1296                            .map(|files| {
1297                                let mut names = files.iter().filter_map(Value::as_str);
1298                                matches!(
1299                                    (names.next(), names.next(), names.next()),
1300                                    (Some("cass.db"), Some("cass.db-wal"), None)
1301                                )
1302                            })
1303                            == Some(true);
1304                        let wal_exists = state.backup_dir.join("cass.db-wal").exists();
1305                        invariants.push(if complete && files_match && wal_exists {
1306                            CrashReplayInvariant::passed(
1307                                checkpoint,
1308                                "complete_backup_manifest_matches_sidecars",
1309                                "complete manifest is present only with expected sidecars",
1310                            )
1311                        } else {
1312                            CrashReplayInvariant::failed(
1313                                checkpoint,
1314                                "complete_backup_manifest_matches_sidecars",
1315                                format!(
1316                                    "complete={complete} files_match={files_match} wal_exists={wal_exists}"
1317                                ),
1318                            )
1319                        });
1320                    }
1321                    _ => invariants.push(CrashReplayInvariant::failed(
1322                        checkpoint,
1323                        "known_backup_checkpoint",
1324                        "unexpected backup checkpoint",
1325                    )),
1326                }
1327                invariants
1328            },
1329        );
1330
1331        assert_eq!(report.verdict, CrashReplayVerdict::Clean);
1332        assert!(
1333            report.validate().is_ok(),
1334            "backup recovery replay report should validate: {report:?}"
1335        );
1336    }
1337
1338    #[test]
1339    fn crash_replay_report_round_trips_as_artifact_manifest()
1340    -> Result<(), Box<dyn std::error::Error>> {
1341        let temp_dir = tempfile::tempdir()?;
1342        let path = temp_dir
1343            .path()
1344            .join("artifacts/crash-replay/crash-replay-report.json");
1345        let checkpoints = vec![CrashReplayCheckpoint::new(
1346            1,
1347            "only_checkpoint",
1348            "single checkpoint for artifact round-trip",
1349        )];
1350        let report = replay_named_checkpoints(
1351            "artifact-round-trip",
1352            "harness",
1353            checkpoints,
1354            || Ok(()),
1355            |_state, _checkpoint| Ok(()),
1356            |_state| Ok(()),
1357            |_state, checkpoint| {
1358                vec![CrashReplayInvariant::passed(
1359                    checkpoint,
1360                    "round_trip_invariant",
1361                    "round-trip invariant passed",
1362                )]
1363            },
1364        );
1365
1366        report.save_json(&path)?;
1367        let loaded = CrashReplayReport::load_json(&path)?;
1368
1369        assert_eq!(loaded, report);
1370        Ok(())
1371    }
1372
1373    #[cfg(unix)]
1374    #[test]
1375    fn crash_replay_report_save_json_replaces_existing_symlink_without_following()
1376    -> Result<(), Box<dyn std::error::Error>> {
1377        use std::os::unix::fs::symlink;
1378
1379        let temp_dir = tempfile::tempdir()?;
1380        let outside_dir = tempfile::tempdir()?;
1381        let report_dir = temp_dir.path().join("artifacts/crash-replay");
1382        fs::create_dir_all(&report_dir)?;
1383        let path = report_dir.join("crash-replay-report.json");
1384        let protected_target = outside_dir.path().join("protected-report.json");
1385        fs::write(&protected_target, "untouched")?;
1386        symlink(&protected_target, &path)?;
1387
1388        let checkpoints = vec![CrashReplayCheckpoint::new(
1389            1,
1390            "only_checkpoint",
1391            "single checkpoint for symlink replacement",
1392        )];
1393        let report = replay_named_checkpoints(
1394            "symlink-replacement",
1395            "harness",
1396            checkpoints,
1397            || Ok(()),
1398            |_state, _checkpoint| Ok(()),
1399            |_state| Ok(()),
1400            |_state, checkpoint| {
1401                vec![CrashReplayInvariant::passed(
1402                    checkpoint,
1403                    "symlink_invariant",
1404                    "symlink replacement invariant passed",
1405                )]
1406            },
1407        );
1408
1409        report.save_json(&path)?;
1410
1411        assert_eq!(
1412            fs::read_to_string(&protected_target)?,
1413            "untouched",
1414            "save_json must replace the report-path symlink, not follow it"
1415        );
1416        assert!(
1417            !fs::symlink_metadata(&path)?.file_type().is_symlink(),
1418            "report path should become a regular JSON file"
1419        );
1420        assert_eq!(CrashReplayReport::load_json(&path)?, report);
1421        Ok(())
1422    }
1423
1424    #[test]
1425    fn crash_replay_validation_rejects_untrustworthy_clean_reports() {
1426        let checkpoint = CrashReplayCheckpoint::new(1, "checkpoint", "validation checkpoint");
1427        let report = CrashReplayReport {
1428            schema_version: CRASH_REPLAY_SCHEMA_VERSION.to_string(),
1429            scenario_id: "bad-clean-report".to_string(),
1430            state_machine: "harness".to_string(),
1431            verdict: CrashReplayVerdict::Clean,
1432            checkpoints: vec![checkpoint.clone()],
1433            events: vec![CrashReplayEvent {
1434                checkpoint_id: checkpoint.id.clone(),
1435                phase: CrashReplayPhase::CheckInvariants,
1436                ok: true,
1437                detail: "checked".to_string(),
1438            }],
1439            invariants: vec![CrashReplayInvariant::failed(
1440                &checkpoint,
1441                "must_not_fail",
1442                "intentional validation failure",
1443            )],
1444        };
1445
1446        assert!(matches!(
1447            report.validate(),
1448            Err(CrashReplayValidationError::CleanReportContainsFailure)
1449        ));
1450
1451        let duplicate_checkpoint = CrashReplayCheckpoint {
1452            ordinal: 2,
1453            ..checkpoint.clone()
1454        };
1455        let duplicate_report = CrashReplayReport {
1456            checkpoints: vec![checkpoint.clone(), duplicate_checkpoint],
1457            ..report.clone()
1458        };
1459        assert!(matches!(
1460            duplicate_report.validate(),
1461            Err(CrashReplayValidationError::DuplicateCheckpointId { .. })
1462        ));
1463
1464        let missing_check_event_report = CrashReplayReport {
1465            events: vec![CrashReplayEvent {
1466                checkpoint_id: checkpoint.id.clone(),
1467                phase: CrashReplayPhase::AdvanceToCheckpoint,
1468                ok: true,
1469                detail: "advanced".to_string(),
1470            }],
1471            invariants: vec![CrashReplayInvariant::passed(
1472                &checkpoint,
1473                "passing_but_unchecked",
1474                "invariant exists but no check event proves it ran",
1475            )],
1476            ..report
1477        };
1478        assert!(matches!(
1479            missing_check_event_report.validate(),
1480            Err(CrashReplayValidationError::CleanReportMissingCheckpointEvent { .. })
1481        ));
1482    }
1483}