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