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