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            cursor_exhausted: false,
721        }
722    }
723
724    fn semantic_artifact() -> ArtifactRecord {
725        ArtifactRecord {
726            tier: TierKind::Fast,
727            embedder_id: "fnv1a-384".to_string(),
728            model_revision: "hash".to_string(),
729            schema_version: SEMANTIC_SCHEMA_VERSION,
730            chunking_version: CHUNKING_STRATEGY_VERSION,
731            dimension: 384,
732            doc_count: 13,
733            conversation_count: 5,
734            db_fingerprint: "semantic-fp".to_string(),
735            index_path: "vector_index/fast.fsvi".to_string(),
736            size_bytes: 4096,
737            started_at_ms: 1_700_000_000_000,
738            completed_at_ms: 1_700_000_060_000,
739            ready: true,
740        }
741    }
742
743    #[test]
744    fn semantic_manifest_state_machine_replays_checkpoint_and_publish_crashes() {
745        let checkpoints = vec![
746            CrashReplayCheckpoint::new(
747                10,
748                "semantic_checkpoint_saved",
749                "semantic checkpoint persisted before artifact publish",
750            ),
751            CrashReplayCheckpoint::new(
752                20,
753                "semantic_artifact_published",
754                "semantic artifact published and checkpoint cleared",
755            ),
756        ];
757
758        let report =
759            replay_named_checkpoints(
760                "semantic-manifest-save-restart",
761                "semantic_manifest",
762                checkpoints,
763                || {
764                    Ok(SemanticReplayState {
765                        temp_dir: tempfile::tempdir()
766                            .map_err(|err| CrashReplayError::from_error("create tempdir", err))?,
767                        loaded: None,
768                    })
769                },
770                |state, checkpoint| {
771                    let mut manifest = SemanticManifest::default();
772                    manifest.refresh_backlog(5, "semantic-fp");
773                    manifest.save_checkpoint(semantic_checkpoint());
774                    if checkpoint.id == "semantic_artifact_published" {
775                        manifest.publish_artifact(semantic_artifact());
776                    }
777                    manifest
778                        .save(state.data_dir())
779                        .map_err(|err| CrashReplayError::from_error("save semantic manifest", err))
780                },
781                |state| {
782                    state.loaded = SemanticManifest::load(state.data_dir()).map_err(|err| {
783                        CrashReplayError::from_error("load semantic manifest", err)
784                    })?;
785                    Ok(())
786                },
787                |state, checkpoint| {
788                    let mut invariants = Vec::new();
789                    let Some(manifest) = &state.loaded else {
790                        return vec![CrashReplayInvariant::failed(
791                            checkpoint,
792                            "semantic_manifest_loaded",
793                            "manifest did not load after restart",
794                        )];
795                    };
796
797                    invariants.push(CrashReplayInvariant::passed(
798                        checkpoint,
799                        "semantic_manifest_loaded",
800                        "manifest loaded after restart",
801                    ));
802                    match checkpoint.id.as_str() {
803                        "semantic_checkpoint_saved" => {
804                            invariants.push(if manifest.checkpoint.is_some()
805                            && manifest.fast_tier.is_none()
806                        {
807                            CrashReplayInvariant::passed(
808                                checkpoint,
809                                "checkpoint_without_torn_artifact",
810                                "restart sees resumable checkpoint and no half-published artifact",
811                            )
812                        } else {
813                            CrashReplayInvariant::failed(
814                                checkpoint,
815                                "checkpoint_without_torn_artifact",
816                                format!(
817                                    "checkpoint={:?} fast_tier={:?}",
818                                    manifest.checkpoint, manifest.fast_tier
819                                ),
820                            )
821                        });
822                        }
823                        "semantic_artifact_published" => {
824                            invariants.push(if manifest.checkpoint.is_none()
825                            && manifest.fast_tier.as_ref().is_some_and(|artifact| artifact.ready)
826                        {
827                            CrashReplayInvariant::passed(
828                                checkpoint,
829                                "published_artifact_clears_checkpoint",
830                                "restart sees ready artifact and no stale matching checkpoint",
831                            )
832                        } else {
833                            CrashReplayInvariant::failed(
834                                checkpoint,
835                                "published_artifact_clears_checkpoint",
836                                format!(
837                                    "checkpoint={:?} fast_tier={:?}",
838                                    manifest.checkpoint, manifest.fast_tier
839                                ),
840                            )
841                        });
842                        }
843                        _ => invariants.push(CrashReplayInvariant::failed(
844                            checkpoint,
845                            "known_checkpoint",
846                            "unexpected semantic checkpoint",
847                        )),
848                    }
849                    invariants
850                },
851            );
852
853        assert_eq!(report.verdict, CrashReplayVerdict::Clean);
854        assert_eq!(report.checkpoints.len(), 2);
855        assert_eq!(report.invariants.len(), 4);
856        assert!(
857            report.validate().is_ok(),
858            "semantic replay report should validate: {report:?}"
859        );
860    }
861
862    #[derive(Debug)]
863    struct PolicyReplayState {
864        pipeline: Value,
865        semantic_available: bool,
866        semantic_fallback_mode: Option<&'static str>,
867        snapshot_statuses: Vec<(String, PolicyControllerStatus, PolicyFallbackState)>,
868    }
869
870    fn policy_pipeline_fixture(mode: &str, reason: &str) -> Value {
871        json!({
872            "pipeline_channel_size": 128,
873            "pipeline_max_message_bytes_in_flight": 1048576,
874            "page_prep_workers": 12,
875            "staged_merge_workers": 4,
876            "staged_shard_builders": 8,
877            "controller_mode": "auto",
878            "controller_restore_clear_samples": 3,
879            "controller_restore_hold_ms": 5000,
880            "controller_loadavg_high_watermark_1m": 1.75,
881            "controller_loadavg_low_watermark_1m": 0.75,
882            "runtime": {
883                "controller_mode": mode,
884                "controller_reason": reason
885            }
886        })
887    }
888
889    #[test]
890    fn policy_registry_state_machine_replays_deterministic_controller_snapshots() {
891        let checkpoints = vec![
892            CrashReplayCheckpoint::new(
893                10,
894                "semantic_fallback_snapshot",
895                "semantic controller reports lexical fallback",
896            ),
897            CrashReplayCheckpoint::new(
898                20,
899                "lexical_throttle_snapshot",
900                "lexical rebuild controller reports pressure fallback",
901            ),
902        ];
903
904        let report = replay_named_checkpoints(
905            "policy-registry-recompute-restart",
906            "policy_registry",
907            checkpoints,
908            || {
909                Ok(PolicyReplayState {
910                    pipeline: policy_pipeline_fixture("steady", "pipeline settings active"),
911                    semantic_available: true,
912                    semantic_fallback_mode: None,
913                    snapshot_statuses: Vec::new(),
914                })
915            },
916            |state, checkpoint| {
917                match checkpoint.id.as_str() {
918                    "semantic_fallback_snapshot" => {
919                        state.semantic_available = false;
920                        state.semantic_fallback_mode = Some("lexical");
921                    }
922                    "lexical_throttle_snapshot" => {
923                        state.pipeline =
924                            policy_pipeline_fixture("throttled", "load pressure reduced workers");
925                    }
926                    _ => {
927                        return Err(CrashReplayError::new(
928                            "advance policy checkpoint",
929                            "unknown checkpoint",
930                        ));
931                    }
932                }
933                Ok(())
934            },
935            |state| {
936                let policy = SemanticPolicy::compiled_defaults();
937                let snapshot = policy_registry_snapshot(
938                    &policy,
939                    state.semantic_available,
940                    state.semantic_fallback_mode,
941                    &state.pipeline,
942                );
943                state.snapshot_statuses = snapshot
944                    .controllers
945                    .into_iter()
946                    .map(|controller| {
947                        (
948                            controller.controller_id,
949                            controller.status,
950                            controller.fallback_state,
951                        )
952                    })
953                    .collect();
954                Ok(())
955            },
956            |state, checkpoint| {
957                let ids: Vec<_> = state
958                    .snapshot_statuses
959                    .iter()
960                    .map(|(id, _, _)| id.as_str())
961                    .collect();
962                let mut invariants =
963                    vec![if ids == ["lexical_rebuild_pipeline", "semantic_search"] {
964                        CrashReplayInvariant::passed(
965                            checkpoint,
966                            "controller_ids_sorted",
967                            "controller ids are deterministic and sorted",
968                        )
969                    } else {
970                        CrashReplayInvariant::failed(
971                            checkpoint,
972                            "controller_ids_sorted",
973                            format!("unexpected controller ids: {ids:?}"),
974                        )
975                    }];
976
977                let expected_controller = match checkpoint.id.as_str() {
978                    "semantic_fallback_snapshot" => "semantic_search",
979                    "lexical_throttle_snapshot" => "lexical_rebuild_pipeline",
980                    _ => "unknown",
981                };
982                let controller = state
983                    .snapshot_statuses
984                    .iter()
985                    .find(|(id, _, _)| id == expected_controller);
986                invariants.push(match controller {
987                    Some((
988                        _id,
989                        PolicyControllerStatus::Fallback,
990                        PolicyFallbackState::Conservative,
991                    )) => CrashReplayInvariant::passed(
992                        checkpoint,
993                        "conservative_fallback_reported",
994                        "checkpoint recompute reports conservative fallback",
995                    ),
996                    other => CrashReplayInvariant::failed(
997                        checkpoint,
998                        "conservative_fallback_reported",
999                        format!("unexpected controller status: {other:?}"),
1000                    ),
1001                });
1002                invariants
1003            },
1004        );
1005
1006        assert_eq!(report.verdict, CrashReplayVerdict::Clean);
1007        assert!(
1008            report.validate().is_ok(),
1009            "policy replay report should validate: {report:?}"
1010        );
1011    }
1012
1013    #[derive(Debug)]
1014    struct LexicalPublishFixtureState {
1015        temp_dir: TempDir,
1016        live_path: PathBuf,
1017        staged_path: PathBuf,
1018        backup_path: PathBuf,
1019    }
1020
1021    impl LexicalPublishFixtureState {
1022        fn new() -> Result<Self, CrashReplayError> {
1023            let temp_dir = tempfile::tempdir()
1024                .map_err(|err| CrashReplayError::from_error("create tempdir", err))?;
1025            let live_path = temp_dir.path().join("live-generation.txt");
1026            let staged_path = temp_dir.path().join("staged-generation.txt");
1027            let backup_path = temp_dir.path().join("live-generation.bak");
1028            fs::write(&live_path, "old-generation")
1029                .map_err(|err| CrashReplayError::from_error("seed live generation", err))?;
1030            Ok(Self {
1031                temp_dir,
1032                live_path,
1033                staged_path,
1034                backup_path,
1035            })
1036        }
1037
1038        fn write_staged(&self) -> Result<(), CrashReplayError> {
1039            fs::write(&self.staged_path, "new-generation")
1040                .map_err(|err| CrashReplayError::from_error("write staged generation", err))
1041        }
1042
1043        fn park_live(&self) -> Result<(), CrashReplayError> {
1044            fs::rename(&self.live_path, &self.backup_path)
1045                .map_err(|err| CrashReplayError::from_error("park live generation", err))
1046        }
1047
1048        fn publish_staged(&self) -> Result<(), CrashReplayError> {
1049            fs::rename(&self.staged_path, &self.live_path)
1050                .map_err(|err| CrashReplayError::from_error("publish staged generation", err))
1051        }
1052    }
1053
1054    #[test]
1055    fn lexical_publish_fixture_replays_park_and_swap_crash_windows() {
1056        let checkpoints = vec![
1057            CrashReplayCheckpoint::new(
1058                10,
1059                "staged_written",
1060                "staged generation exists before live path is touched",
1061            ),
1062            CrashReplayCheckpoint::new(
1063                20,
1064                "live_parked",
1065                "live generation has been parked but staged is not yet live",
1066            ),
1067            CrashReplayCheckpoint::new(
1068                30,
1069                "staged_published",
1070                "staged generation has been promoted to live",
1071            ),
1072        ];
1073
1074        let report = replay_named_checkpoints(
1075            "lexical-publish-fixture-restart",
1076            "lexical_publish",
1077            checkpoints,
1078            LexicalPublishFixtureState::new,
1079            |state, checkpoint| {
1080                state.write_staged()?;
1081                match checkpoint.id.as_str() {
1082                    "staged_written" => {}
1083                    "live_parked" => {
1084                        state.park_live()?;
1085                    }
1086                    "staged_published" => {
1087                        state.park_live()?;
1088                        state.publish_staged()?;
1089                    }
1090                    _ => {
1091                        return Err(CrashReplayError::new(
1092                            "advance lexical publish checkpoint",
1093                            "unknown checkpoint",
1094                        ));
1095                    }
1096                }
1097                Ok(())
1098            },
1099            |state| {
1100                if !state.live_path.exists() && state.backup_path.exists() {
1101                    fs::rename(&state.backup_path, &state.live_path)
1102                        .map_err(|err| CrashReplayError::from_error("restore parked live", err))?;
1103                }
1104                Ok(())
1105            },
1106            |state, checkpoint| {
1107                let live = fs::read_to_string(&state.live_path).ok();
1108                let expected = match checkpoint.id.as_str() {
1109                    "staged_written" | "live_parked" => "old-generation",
1110                    "staged_published" => "new-generation",
1111                    _ => "unknown",
1112                };
1113
1114                vec![
1115                    if state.temp_dir.path().exists() {
1116                        CrashReplayInvariant::passed(
1117                            checkpoint,
1118                            "fixture_root_retained",
1119                            "fixture root remains available for artifact inspection",
1120                        )
1121                    } else {
1122                        CrashReplayInvariant::failed(
1123                            checkpoint,
1124                            "fixture_root_retained",
1125                            "fixture root disappeared before invariant checks",
1126                        )
1127                    },
1128                    if live.as_deref() == Some(expected) {
1129                        CrashReplayInvariant::passed(
1130                            checkpoint,
1131                            "live_generation_is_old_or_new",
1132                            format!("live generation recovered as {expected}"),
1133                        )
1134                    } else {
1135                        CrashReplayInvariant::failed(
1136                            checkpoint,
1137                            "live_generation_is_old_or_new",
1138                            format!("expected {expected}, got {live:?}"),
1139                        )
1140                    },
1141                ]
1142            },
1143        );
1144
1145        assert_eq!(report.verdict, CrashReplayVerdict::Clean);
1146        assert!(
1147            report.validate().is_ok(),
1148            "lexical publish replay report should validate: {report:?}"
1149        );
1150    }
1151
1152    #[derive(Debug)]
1153    struct BackupRecoveryFixtureState {
1154        temp_dir: TempDir,
1155        canonical_db: PathBuf,
1156        backup_dir: PathBuf,
1157        manifest: Option<Value>,
1158    }
1159
1160    impl BackupRecoveryFixtureState {
1161        fn new() -> Result<Self, CrashReplayError> {
1162            let temp_dir = tempfile::tempdir()
1163                .map_err(|err| CrashReplayError::from_error("create tempdir", err))?;
1164            let canonical_db = temp_dir.path().join("cass.db");
1165            let backup_dir = temp_dir.path().join("backup");
1166            fs::write(&canonical_db, "canonical-main")
1167                .map_err(|err| CrashReplayError::from_error("seed canonical db", err))?;
1168            fs::write(temp_dir.path().join("cass.db-wal"), "canonical-wal")
1169                .map_err(|err| CrashReplayError::from_error("seed canonical wal", err))?;
1170            fs::create_dir_all(&backup_dir)
1171                .map_err(|err| CrashReplayError::from_error("create backup dir", err))?;
1172            Ok(Self {
1173                temp_dir,
1174                canonical_db,
1175                backup_dir,
1176                manifest: None,
1177            })
1178        }
1179
1180        fn copy_main(&self) -> Result<(), CrashReplayError> {
1181            fs::copy(&self.canonical_db, self.backup_dir.join("cass.db"))
1182                .map(|_| ())
1183                .map_err(|err| CrashReplayError::from_error("copy backup main", err))
1184        }
1185
1186        fn copy_wal_and_manifest(&self) -> Result<(), CrashReplayError> {
1187            fs::copy(
1188                self.temp_dir.path().join("cass.db-wal"),
1189                self.backup_dir.join("cass.db-wal"),
1190            )
1191            .map_err(|err| CrashReplayError::from_error("copy backup wal", err))?;
1192            let manifest = json!({
1193                "schema_version": 1,
1194                "complete": true,
1195                "files": ["cass.db", "cass.db-wal"],
1196            });
1197            let bytes = serde_json::to_vec_pretty(&manifest)
1198                .map_err(|err| CrashReplayError::from_error("encode backup manifest", err))?;
1199            fs::write(self.backup_dir.join("manifest.json"), bytes)
1200                .map_err(|err| CrashReplayError::from_error("write backup manifest", err))
1201        }
1202    }
1203
1204    #[test]
1205    fn backup_recovery_fixture_replays_incomplete_and_complete_bundle_crashes() {
1206        let checkpoints = vec![
1207            CrashReplayCheckpoint::new(
1208                10,
1209                "backup_main_copied",
1210                "backup main file copied before bundle manifest exists",
1211            ),
1212            CrashReplayCheckpoint::new(
1213                20,
1214                "backup_manifest_written",
1215                "backup sidecars and manifest mark the bundle complete",
1216            ),
1217        ];
1218
1219        let report = replay_named_checkpoints(
1220            "backup-recovery-fixture-restart",
1221            "backup_recovery",
1222            checkpoints,
1223            BackupRecoveryFixtureState::new,
1224            |state, checkpoint| {
1225                state.copy_main()?;
1226                match checkpoint.id.as_str() {
1227                    "backup_main_copied" => {}
1228                    "backup_manifest_written" => {
1229                        state.copy_wal_and_manifest()?;
1230                    }
1231                    _ => {
1232                        return Err(CrashReplayError::new(
1233                            "advance backup recovery checkpoint",
1234                            "unknown checkpoint",
1235                        ));
1236                    }
1237                }
1238                Ok(())
1239            },
1240            |state| {
1241                let manifest_path = state.backup_dir.join("manifest.json");
1242                state.manifest = if manifest_path.exists() {
1243                    let bytes = fs::read(&manifest_path)
1244                        .map_err(|err| CrashReplayError::from_error("read backup manifest", err))?;
1245                    Some(serde_json::from_slice(&bytes).map_err(|err| {
1246                        CrashReplayError::from_error("parse backup manifest", err)
1247                    })?)
1248                } else {
1249                    None
1250                };
1251                Ok(())
1252            },
1253            |state, checkpoint| {
1254                let canonical = fs::read_to_string(&state.canonical_db).ok();
1255                let mut invariants = vec![if canonical.as_deref() == Some("canonical-main") {
1256                    CrashReplayInvariant::passed(
1257                        checkpoint,
1258                        "canonical_db_preserved",
1259                        "restart did not replace the canonical DB from an incomplete backup",
1260                    )
1261                } else {
1262                    CrashReplayInvariant::failed(
1263                        checkpoint,
1264                        "canonical_db_preserved",
1265                        format!("unexpected canonical DB content: {canonical:?}"),
1266                    )
1267                }];
1268
1269                match checkpoint.id.as_str() {
1270                    "backup_main_copied" => {
1271                        invariants.push(if state.manifest.is_none() {
1272                            CrashReplayInvariant::passed(
1273                                checkpoint,
1274                                "partial_backup_not_marked_complete",
1275                                "main-only backup has no manifest and is not advertised recoverable",
1276                            )
1277                        } else {
1278                            CrashReplayInvariant::failed(
1279                                checkpoint,
1280                                "partial_backup_not_marked_complete",
1281                                format!("unexpected manifest: {:?}", state.manifest),
1282                            )
1283                        });
1284                    }
1285                    "backup_manifest_written" => {
1286                        let complete = state
1287                            .manifest
1288                            .as_ref()
1289                            .and_then(|manifest| manifest.get("complete"))
1290                            .and_then(Value::as_bool)
1291                            == Some(true);
1292                        let files_match = state
1293                            .manifest
1294                            .as_ref()
1295                            .and_then(|manifest| manifest.get("files"))
1296                            .and_then(Value::as_array)
1297                            .map(|files| {
1298                                let mut names = files.iter().filter_map(Value::as_str);
1299                                matches!(
1300                                    (names.next(), names.next(), names.next()),
1301                                    (Some("cass.db"), Some("cass.db-wal"), None)
1302                                )
1303                            })
1304                            == Some(true);
1305                        let wal_exists = state.backup_dir.join("cass.db-wal").exists();
1306                        invariants.push(if complete && files_match && wal_exists {
1307                            CrashReplayInvariant::passed(
1308                                checkpoint,
1309                                "complete_backup_manifest_matches_sidecars",
1310                                "complete manifest is present only with expected sidecars",
1311                            )
1312                        } else {
1313                            CrashReplayInvariant::failed(
1314                                checkpoint,
1315                                "complete_backup_manifest_matches_sidecars",
1316                                format!(
1317                                    "complete={complete} files_match={files_match} wal_exists={wal_exists}"
1318                                ),
1319                            )
1320                        });
1321                    }
1322                    _ => invariants.push(CrashReplayInvariant::failed(
1323                        checkpoint,
1324                        "known_backup_checkpoint",
1325                        "unexpected backup checkpoint",
1326                    )),
1327                }
1328                invariants
1329            },
1330        );
1331
1332        assert_eq!(report.verdict, CrashReplayVerdict::Clean);
1333        assert!(
1334            report.validate().is_ok(),
1335            "backup recovery replay report should validate: {report:?}"
1336        );
1337    }
1338
1339    #[test]
1340    fn crash_replay_report_round_trips_as_artifact_manifest()
1341    -> Result<(), Box<dyn std::error::Error>> {
1342        let temp_dir = tempfile::tempdir()?;
1343        let path = temp_dir
1344            .path()
1345            .join("artifacts/crash-replay/crash-replay-report.json");
1346        let checkpoints = vec![CrashReplayCheckpoint::new(
1347            1,
1348            "only_checkpoint",
1349            "single checkpoint for artifact round-trip",
1350        )];
1351        let report = replay_named_checkpoints(
1352            "artifact-round-trip",
1353            "harness",
1354            checkpoints,
1355            || Ok(()),
1356            |_state, _checkpoint| Ok(()),
1357            |_state| Ok(()),
1358            |_state, checkpoint| {
1359                vec![CrashReplayInvariant::passed(
1360                    checkpoint,
1361                    "round_trip_invariant",
1362                    "round-trip invariant passed",
1363                )]
1364            },
1365        );
1366
1367        report.save_json(&path)?;
1368        let loaded = CrashReplayReport::load_json(&path)?;
1369
1370        assert_eq!(loaded, report);
1371        Ok(())
1372    }
1373
1374    #[cfg(unix)]
1375    #[test]
1376    fn crash_replay_report_save_json_replaces_existing_symlink_without_following()
1377    -> Result<(), Box<dyn std::error::Error>> {
1378        use std::os::unix::fs::symlink;
1379
1380        let temp_dir = tempfile::tempdir()?;
1381        let outside_dir = tempfile::tempdir()?;
1382        let report_dir = temp_dir.path().join("artifacts/crash-replay");
1383        fs::create_dir_all(&report_dir)?;
1384        let path = report_dir.join("crash-replay-report.json");
1385        let protected_target = outside_dir.path().join("protected-report.json");
1386        fs::write(&protected_target, "untouched")?;
1387        symlink(&protected_target, &path)?;
1388
1389        let checkpoints = vec![CrashReplayCheckpoint::new(
1390            1,
1391            "only_checkpoint",
1392            "single checkpoint for symlink replacement",
1393        )];
1394        let report = replay_named_checkpoints(
1395            "symlink-replacement",
1396            "harness",
1397            checkpoints,
1398            || Ok(()),
1399            |_state, _checkpoint| Ok(()),
1400            |_state| Ok(()),
1401            |_state, checkpoint| {
1402                vec![CrashReplayInvariant::passed(
1403                    checkpoint,
1404                    "symlink_invariant",
1405                    "symlink replacement invariant passed",
1406                )]
1407            },
1408        );
1409
1410        report.save_json(&path)?;
1411
1412        assert_eq!(
1413            fs::read_to_string(&protected_target)?,
1414            "untouched",
1415            "save_json must replace the report-path symlink, not follow it"
1416        );
1417        assert!(
1418            !fs::symlink_metadata(&path)?.file_type().is_symlink(),
1419            "report path should become a regular JSON file"
1420        );
1421        assert_eq!(CrashReplayReport::load_json(&path)?, report);
1422        Ok(())
1423    }
1424
1425    #[test]
1426    fn crash_replay_validation_rejects_untrustworthy_clean_reports() {
1427        let checkpoint = CrashReplayCheckpoint::new(1, "checkpoint", "validation checkpoint");
1428        let report = CrashReplayReport {
1429            schema_version: CRASH_REPLAY_SCHEMA_VERSION.to_string(),
1430            scenario_id: "bad-clean-report".to_string(),
1431            state_machine: "harness".to_string(),
1432            verdict: CrashReplayVerdict::Clean,
1433            checkpoints: vec![checkpoint.clone()],
1434            events: vec![CrashReplayEvent {
1435                checkpoint_id: checkpoint.id.clone(),
1436                phase: CrashReplayPhase::CheckInvariants,
1437                ok: true,
1438                detail: "checked".to_string(),
1439            }],
1440            invariants: vec![CrashReplayInvariant::failed(
1441                &checkpoint,
1442                "must_not_fail",
1443                "intentional validation failure",
1444            )],
1445        };
1446
1447        assert!(matches!(
1448            report.validate(),
1449            Err(CrashReplayValidationError::CleanReportContainsFailure)
1450        ));
1451
1452        let duplicate_checkpoint = CrashReplayCheckpoint {
1453            ordinal: 2,
1454            ..checkpoint.clone()
1455        };
1456        let duplicate_report = CrashReplayReport {
1457            checkpoints: vec![checkpoint.clone(), duplicate_checkpoint],
1458            ..report.clone()
1459        };
1460        assert!(matches!(
1461            duplicate_report.validate(),
1462            Err(CrashReplayValidationError::DuplicateCheckpointId { .. })
1463        ));
1464
1465        let missing_check_event_report = CrashReplayReport {
1466            events: vec![CrashReplayEvent {
1467                checkpoint_id: checkpoint.id.clone(),
1468                phase: CrashReplayPhase::AdvanceToCheckpoint,
1469                ok: true,
1470                detail: "advanced".to_string(),
1471            }],
1472            invariants: vec![CrashReplayInvariant::passed(
1473                &checkpoint,
1474                "passing_but_unchecked",
1475                "invariant exists but no check event proves it ran",
1476            )],
1477            ..report
1478        };
1479        assert!(matches!(
1480            missing_check_event_report.validate(),
1481            Err(CrashReplayValidationError::CleanReportMissingCheckpointEvent { .. })
1482        ));
1483    }
1484}