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