1use serde::{Deserialize, Serialize};
35
36use crate::destination::Destination;
37use crate::error::Result;
38use crate::manifest::{
39 MANIFEST_FILENAME, RunManifest, SUCCESS_FILENAME, join_key, parse_success_marker,
40 success_marker_body,
41};
42use crate::pipeline::manifest_reconcile::{PartPresence, reconcile_manifest_against_listing};
43
44pub(crate) const MANIFEST_MAX_BYTES: u64 = 64 * 1024 * 1024;
56
57pub(crate) const SUCCESS_MARKER_MAX_BYTES: u64 = 4096;
64
65#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub enum ValidateDepth {
89 Light,
91 Sample,
93 #[default]
95 Full,
96}
97
98impl ValidateDepth {
99 fn runs_part_reconcile(self) -> bool {
103 !matches!(self, ValidateDepth::Light)
104 }
105
106 pub(crate) fn runs_part_download(self) -> bool {
113 matches!(self, ValidateDepth::Full)
114 }
115
116 pub fn label(self) -> &'static str {
120 match self {
121 ValidateDepth::Light => "light",
122 ValidateDepth::Sample => "sample",
123 ValidateDepth::Full => "full",
124 }
125 }
126}
127
128pub(crate) fn read_capped(dest: &dyn Destination, key: &str, max_bytes: u64) -> Result<Vec<u8>> {
146 match dest.head(key)? {
147 None => anyhow::bail!("'{key}' not found at the destination"),
148 Some(meta) => {
149 if meta.size_bytes > max_bytes {
150 anyhow::bail!(
151 "'{key}' is {} bytes, exceeding the {max_bytes}-byte control-artifact \
152 read cap — refusing to load it into memory (possible tampering)",
153 meta.size_bytes
154 );
155 }
156 dest.read(key)
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct ManifestVerification {
168 pub manifest_found: bool,
172 pub legacy_run: bool,
174 pub parts_verified: usize,
177 #[serde(default)]
182 pub parts_md5_verified: usize,
183 pub parts_failed: usize,
186 pub success_marker_consistent: bool,
191 pub manifest_self_consistent: bool,
194 pub passed: bool,
200 pub failures: Vec<Failure>,
204 #[serde(default = "default_depth_level")]
211 pub depth_level: String,
212}
213
214fn default_depth_level() -> String {
218 ValidateDepth::Full.label().to_string()
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(tag = "kind", rename_all = "snake_case")]
223pub enum Failure {
224 PartMissing { part_id: u32, path: String },
226 PartSizeMismatch {
228 part_id: u32,
229 path: String,
230 expected: u64,
231 actual: u64,
232 },
233 PartChecksumMismatch {
237 part_id: u32,
238 path: String,
239 expected: String,
240 actual: String,
241 },
242 SuccessMarkerMalformed { body_preview: String },
245 SuccessMarkerStale {
251 marker_fingerprint: String,
252 manifest_fingerprint: String,
253 },
254 ManifestSelfInconsistent { detail: String },
259 ManifestReadError { detail: String },
261 SuccessMarkerReadError { detail: String },
263 ListPrefixError { detail: String },
266 UntrackedObject { key: String, size_bytes: u64 },
270 ContentVerificationUnmet { size_only: usize, total: usize },
274 ManifestRequiredButAbsent { prefix: String },
282}
283
284impl Failure {
285 pub fn is_fatal(&self) -> bool {
293 !matches!(self, Failure::UntrackedObject { .. })
294 }
295
296 pub fn error_code(&self) -> &'static str {
305 match self {
306 Failure::PartMissing { .. } => "RIVET_VERIFY_PART_MISSING",
307 Failure::PartSizeMismatch { .. } => "RIVET_VERIFY_PART_SIZE_MISMATCH",
308 Failure::PartChecksumMismatch { .. } => "RIVET_VERIFY_PART_CHECKSUM_MISMATCH",
309 Failure::SuccessMarkerMalformed { .. } => "RIVET_VERIFY_SUCCESS_MALFORMED",
310 Failure::SuccessMarkerStale { .. } => "RIVET_VERIFY_SUCCESS_STALE",
311 Failure::ManifestSelfInconsistent { .. } => "RIVET_VERIFY_MANIFEST_INCONSISTENT",
312 Failure::ManifestReadError { .. } => "RIVET_VERIFY_MANIFEST_READ_ERROR",
313 Failure::SuccessMarkerReadError { .. } => "RIVET_VERIFY_SUCCESS_READ_ERROR",
314 Failure::ListPrefixError { .. } => "RIVET_VERIFY_LIST_ERROR",
315 Failure::UntrackedObject { .. } => "RIVET_VERIFY_UNTRACKED_OBJECT",
316 Failure::ContentVerificationUnmet { .. } => "RIVET_VERIFY_CONTENT_UNMET",
317 Failure::ManifestRequiredButAbsent { .. } => "RIVET_VERIFY_MANIFEST_REQUIRED",
318 }
319 }
320}
321
322impl std::fmt::Display for Failure {
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332 match self {
333 Failure::PartMissing { part_id, path } => {
334 write!(f, "part {} missing at {}", part_id, path)
335 }
336 Failure::PartSizeMismatch {
337 part_id,
338 path,
339 expected,
340 actual,
341 } => write!(
342 f,
343 "part {} size mismatch at {}: manifest {}, dest {}",
344 part_id, path, expected, actual
345 ),
346 Failure::PartChecksumMismatch {
347 part_id,
348 path,
349 expected,
350 actual,
351 } => write!(
352 f,
353 "part {} content mismatch at {}: manifest md5 {}, dest {}",
354 part_id, path, expected, actual
355 ),
356 Failure::SuccessMarkerMalformed { body_preview } => {
357 write!(f, "_SUCCESS body malformed: {body_preview:?}")
358 }
359 Failure::SuccessMarkerStale {
360 marker_fingerprint,
361 manifest_fingerprint,
362 } => write!(
363 f,
364 "_SUCCESS body {} != manifest fingerprint {} (stale marker)",
365 marker_fingerprint, manifest_fingerprint
366 ),
367 Failure::ManifestSelfInconsistent { detail } => {
368 write!(f, "manifest self-consistency: {detail}")
369 }
370 Failure::ManifestReadError { detail } => {
371 write!(f, "manifest read error: {detail}")
372 }
373 Failure::SuccessMarkerReadError { detail } => {
374 write!(f, "_SUCCESS read error: {detail}")
375 }
376 Failure::ListPrefixError { detail } => {
377 write!(f, "destination listing error: {detail}")
378 }
379 Failure::UntrackedObject { key, size_bytes } => {
380 write!(f, "untracked object: {} ({} bytes)", key, size_bytes)
381 }
382 Failure::ContentVerificationUnmet { size_only, total } => write!(
383 f,
384 "verify: content not met — {size_only} of {total} part(s) only \
385 size-verified (no store checksum); lower max_file_size so parts \
386 upload as a single PUT, or the backend exposes no checksum"
387 ),
388 Failure::ManifestRequiredButAbsent { prefix } => write!(
389 f,
390 "no manifest at {prefix}: a manifest was required here (operator \
391 pinned --prefix) but none was found — this prefix was never \
392 written, or the data was relocated. Run the export first, or \
393 drop --prefix to validate the config-resolved destination."
394 ),
395 }
396 }
397}
398
399impl ManifestVerification {
400 fn empty() -> Self {
405 Self {
406 manifest_found: false,
407 legacy_run: false,
408 parts_verified: 0,
409 parts_md5_verified: 0,
410 parts_failed: 0,
411 success_marker_consistent: false,
412 manifest_self_consistent: false,
413 passed: false,
414 failures: Vec::new(),
415 depth_level: default_depth_level(),
418 }
419 }
420
421 fn recompute_passed(&mut self) {
426 self.passed = self.manifest_found && !self.failures.iter().any(Failure::is_fatal);
427 }
428
429 pub fn enforce_content_policy(&mut self, require_content: bool) {
435 if require_content && self.manifest_found {
436 let size_only = self.parts_verified.saturating_sub(self.parts_md5_verified);
437 if size_only > 0 {
438 self.failures.push(Failure::ContentVerificationUnmet {
439 size_only,
440 total: self.parts_verified,
441 });
442 self.recompute_passed();
443 }
444 }
445 }
446
447 pub fn require_manifest_present(&mut self, prefix: &str) {
460 if !self.manifest_found && !self.has_failures() {
461 self.legacy_run = false;
462 self.failures.push(Failure::ManifestRequiredButAbsent {
463 prefix: prefix.to_string(),
464 });
465 self.recompute_passed();
466 }
467 }
468
469 pub fn legacy() -> Self {
473 Self {
477 legacy_run: true,
478 ..Self::empty()
479 }
480 }
481
482 pub fn has_failures(&self) -> bool {
486 !self.failures.is_empty()
487 }
488}
489
490pub fn verify_at_destination(
512 dest: &dyn Destination,
513 manifest_dir: &str,
514 depth: ValidateDepth,
515) -> Result<ManifestVerification> {
516 let manifest_key = join_key(manifest_dir, MANIFEST_FILENAME);
517 let success_key = join_key(manifest_dir, SUCCESS_FILENAME);
518
519 let with_depth = |mut v: ManifestVerification| -> ManifestVerification {
524 v.depth_level = depth.label().to_string();
525 v
526 };
527
528 let manifest_bytes = match dest.head(&manifest_key) {
537 Ok(None) => return Ok(with_depth(ManifestVerification::legacy())),
538 Ok(Some(_)) => match read_capped(dest, &manifest_key, MANIFEST_MAX_BYTES) {
539 Ok(b) => b,
540 Err(e) => {
541 let mut v = ManifestVerification::legacy();
542 v.legacy_run = false;
543 v.failures.push(Failure::ManifestReadError {
544 detail: format!("{e:#}"),
545 });
546 v.passed = false;
547 return Ok(with_depth(v));
548 }
549 },
550 Err(e) => {
551 let mut v = ManifestVerification::legacy();
556 v.legacy_run = false;
557 v.failures.push(Failure::ManifestReadError {
558 detail: format!("manifest head failed: {e:#}"),
559 });
560 v.passed = false;
561 return Ok(with_depth(v));
562 }
563 };
564
565 let manifest: RunManifest = match serde_json::from_slice(&manifest_bytes) {
566 Ok(m) => m,
567 Err(e) => {
568 return Ok(with_depth(ManifestVerification {
573 manifest_found: true,
574 failures: vec![Failure::ManifestSelfInconsistent {
575 detail: format!("manifest.json parse failed: {e}"),
576 }],
577 ..ManifestVerification::empty()
578 }));
579 }
580 };
581
582 let mut out = ManifestVerification {
587 manifest_found: true,
588 manifest_self_consistent: true,
589 passed: true,
590 depth_level: depth.label().to_string(),
591 ..ManifestVerification::empty()
592 };
593
594 if let Err(e) = manifest.validate_self_consistency() {
596 out.manifest_self_consistent = false;
597 out.failures.push(Failure::ManifestSelfInconsistent {
598 detail: format!("{e}"),
599 });
600 }
604
605 let reconciliation = if depth.runs_part_reconcile() {
628 match dest.list_prefix(manifest_dir) {
629 Ok(listing) => Some(reconcile_manifest_against_listing(
630 &manifest,
631 &listing,
632 manifest_dir,
633 )),
634 Err(e) => {
635 out.failures.push(Failure::ListPrefixError {
636 detail: format!("{e:#}"),
637 });
638 None
639 }
640 }
641 } else {
642 None
643 };
644 if let Some(rec) = &reconciliation {
645 for check in &rec.per_part {
646 match &check.presence {
647 PartPresence::Present { md5_verified } => {
648 out.parts_verified += 1;
649 if *md5_verified {
650 out.parts_md5_verified += 1;
651 }
652 }
653 PartPresence::SizeMismatch { expected, actual } => {
654 out.parts_failed += 1;
655 out.failures.push(Failure::PartSizeMismatch {
656 part_id: check.part_id,
657 path: check.path.clone(),
658 expected: *expected,
659 actual: *actual,
660 });
661 }
662 PartPresence::Missing => {
663 out.parts_failed += 1;
664 out.failures.push(Failure::PartMissing {
665 part_id: check.part_id,
666 path: check.path.clone(),
667 });
668 }
669 PartPresence::ChecksumMismatch { expected, actual } => {
670 out.parts_failed += 1;
671 out.failures.push(Failure::PartChecksumMismatch {
672 part_id: check.part_id,
673 path: check.path.clone(),
674 expected: expected.clone(),
675 actual: actual.clone(),
676 });
677 }
678 }
679 }
680 }
681
682 let success_head = match dest.head(&success_key) {
689 Ok(h) => h,
690 Err(e) => {
691 out.failures.push(Failure::SuccessMarkerReadError {
692 detail: format!("_SUCCESS head failed: {e:#}"),
693 });
694 out.recompute_passed();
695 return Ok(out);
696 }
697 };
698 match success_head {
699 None => {
700 }
706 Some(head) if head.size_bytes > SUCCESS_MARKER_MAX_BYTES => {
707 out.failures.push(Failure::SuccessMarkerMalformed {
712 body_preview: format!(
713 "(oversized: {} bytes exceeds the {SUCCESS_MARKER_MAX_BYTES}-byte _SUCCESS cap; not read)",
714 head.size_bytes
715 ),
716 });
717 out.recompute_passed();
718 return Ok(out);
719 }
720 Some(_) => match dest.read(&success_key) {
721 Err(e) => {
722 out.failures.push(Failure::SuccessMarkerReadError {
723 detail: format!("{e:#}"),
724 });
725 }
726 Ok(body) => {
727 let body_str = match std::str::from_utf8(&body) {
728 Ok(s) => s,
729 Err(_) => {
730 out.failures.push(Failure::SuccessMarkerMalformed {
731 body_preview: format!("(non-utf8, {} bytes)", body.len()),
732 });
733 out.recompute_passed();
734 return Ok(out);
735 }
736 };
737 match parse_success_marker(body_str) {
738 None => {
739 out.failures.push(Failure::SuccessMarkerMalformed {
740 body_preview: preview(body_str),
741 });
742 }
743 Some(marker_fp) => {
744 let manifest_fp = success_marker_body(&manifest_bytes);
745 let manifest_fp_trimmed = manifest_fp.trim_end_matches('\n');
749 if marker_fp == manifest_fp_trimmed {
750 out.success_marker_consistent = true;
751 } else {
752 out.failures.push(Failure::SuccessMarkerStale {
753 marker_fingerprint: marker_fp.to_string(),
754 manifest_fingerprint: manifest_fp_trimmed.to_string(),
755 });
756 }
757 }
758 }
759 }
760 },
761 }
762
763 if let Some(rec) = reconciliation {
770 for obj in rec.untracked {
771 out.failures.push(Failure::UntrackedObject {
772 key: obj.key,
773 size_bytes: obj.size_bytes,
774 });
775 }
776 }
777
778 out.recompute_passed();
779 Ok(out)
780}
781
782fn preview(s: &str) -> String {
784 let trimmed: String = s.chars().take(40).collect();
785 if s.chars().count() > 40 {
786 format!("{trimmed}…")
787 } else {
788 trimmed
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795 use crate::config::{DestinationConfig, DestinationType};
796 use crate::destination::local::LocalDestination;
797 use crate::manifest::{
798 MANIFEST_VERSION, ManifestDestination, ManifestPart, ManifestSource, ManifestStatus,
799 PartStatus, RunManifest,
800 };
801 use std::path::Path;
802
803 fn local_dest(base: &Path) -> LocalDestination {
804 LocalDestination::new(&DestinationConfig {
805 destination_type: DestinationType::Local,
806 path: Some(base.to_string_lossy().into_owned()),
807 ..Default::default()
808 })
809 .unwrap()
810 }
811
812 fn part(part_id: u32, rows: i64, size: u64, fp: &str) -> ManifestPart {
813 ManifestPart {
814 part_id,
815 path: format!("part-{part_id:06}.parquet"),
816 rows,
817 size_bytes: size,
818 content_fingerprint: fp.into(),
819 content_md5: String::new(),
820 status: PartStatus::Committed,
821 }
822 }
823
824 fn build_manifest(parts: Vec<ManifestPart>, status: ManifestStatus) -> RunManifest {
825 let row_count: i64 = parts
826 .iter()
827 .filter(|p| p.status == PartStatus::Committed)
828 .map(|p| p.rows)
829 .sum();
830 let part_count = parts
831 .iter()
832 .filter(|p| p.status == PartStatus::Committed)
833 .count() as u32;
834 RunManifest {
835 mode: "batch".to_string(),
836 manifest_version: MANIFEST_VERSION,
837 run_id: "r".into(),
838 export_name: "public.orders".into(),
839 started_at: "2026-05-21T12:00:00Z".into(),
840 finished_at: "2026-05-21T12:01:00Z".into(),
841 status,
842 source: ManifestSource {
843 engine: "postgres".into(),
844 schema: Some("public".into()),
845 table: Some("orders".into()),
846 extraction: None,
847 },
848 destination: ManifestDestination {
849 kind: "local".into(),
850 uri: "file:///tmp/out".into(),
851 },
852 format: "parquet".into(),
853 compression: "zstd".into(),
854 schema_fingerprint: "xxh3:0123456789abcdef".into(),
855 row_count,
856 part_count,
857 parts,
858 column_checksums: None,
859 checksum_key_column: None,
860 }
861 }
862
863 fn write_dataset(dir: &Path, m: &RunManifest, parts_with_bytes: &[(&str, &[u8])]) {
865 for (name, bytes) in parts_with_bytes {
866 std::fs::write(dir.join(name), bytes).unwrap();
867 }
868 let body = serde_json::to_vec_pretty(m).unwrap();
869 std::fs::write(dir.join(MANIFEST_FILENAME), &body).unwrap();
870 if matches!(m.status, ManifestStatus::Success) {
871 std::fs::write(dir.join(SUCCESS_FILENAME), success_marker_body(&body)).unwrap();
872 }
873 }
874
875 #[test]
878 fn happy_path_verifies_all_parts_and_success_marker() {
879 let dir = tempfile::tempdir().unwrap();
880 let m = build_manifest(
881 vec![
882 part(1, 10, 4, "xxh3:1111111111111111"),
883 part(2, 20, 5, "xxh3:2222222222222222"),
884 ],
885 ManifestStatus::Success,
886 );
887 write_dataset(
888 dir.path(),
889 &m,
890 &[
891 ("part-000001.parquet", b"AAAA"),
892 ("part-000002.parquet", b"BBBBB"),
893 ],
894 );
895 let dest = local_dest(dir.path());
896
897 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
898 assert!(v.manifest_found);
899 assert!(!v.legacy_run);
900 assert_eq!(v.parts_verified, 2);
901 assert_eq!(v.parts_failed, 0);
902 assert!(v.success_marker_consistent);
903 assert!(v.manifest_self_consistent);
904 assert!(v.passed);
905 assert!(v.failures.is_empty());
906 }
907
908 #[test]
911 fn no_manifest_returns_legacy_run_label() {
912 let dir = tempfile::tempdir().unwrap();
914 let dest = local_dest(dir.path());
915 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
916 assert!(!v.manifest_found);
917 assert!(v.legacy_run);
918 assert_eq!(v.parts_verified, 0);
919 assert!(!v.passed);
920 assert!(v.failures.is_empty(), "no failures, just a legacy label");
921 }
922
923 #[test]
926 fn missing_part_is_flagged_with_part_id_and_path() {
927 let dir = tempfile::tempdir().unwrap();
928 let m = build_manifest(
929 vec![
930 part(1, 10, 4, "xxh3:1111111111111111"),
931 part(2, 20, 5, "xxh3:2222222222222222"),
932 ],
933 ManifestStatus::Success,
934 );
935 write_dataset(
936 dir.path(),
937 &m,
938 &[("part-000001.parquet", b"AAAA")], );
940 let dest = local_dest(dir.path());
941
942 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
943 assert_eq!(v.parts_verified, 1);
944 assert_eq!(v.parts_failed, 1);
945 assert!(!v.passed);
946 assert!(
947 v.failures
948 .iter()
949 .any(|f| matches!(f, Failure::PartMissing { part_id: 2, .. }))
950 );
951 }
952
953 #[test]
954 fn part_size_mismatch_is_flagged_with_expected_and_actual() {
955 let dir = tempfile::tempdir().unwrap();
956 let m = build_manifest(
957 vec![part(1, 10, 4, "xxh3:1111111111111111")],
958 ManifestStatus::Success,
959 );
960 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"OOPSIE")]);
962 let dest = local_dest(dir.path());
963
964 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
965 assert!(!v.passed);
966 let mismatch = v
967 .failures
968 .iter()
969 .find_map(|f| match f {
970 Failure::PartSizeMismatch {
971 part_id,
972 expected,
973 actual,
974 ..
975 } => Some((*part_id, *expected, *actual)),
976 _ => None,
977 })
978 .expect("must surface the size mismatch");
979 assert_eq!(mismatch, (1, 4, 6));
980 }
981
982 #[test]
985 fn stale_success_marker_is_flagged_as_inconsistent() {
986 let dir = tempfile::tempdir().unwrap();
990 let m = build_manifest(
991 vec![part(1, 10, 4, "xxh3:1111111111111111")],
992 ManifestStatus::Success,
993 );
994 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
995 std::fs::write(
996 dir.path().join(SUCCESS_FILENAME),
997 success_marker_body(b"different manifest body"),
998 )
999 .unwrap();
1000 let dest = local_dest(dir.path());
1001
1002 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1003 assert!(!v.success_marker_consistent);
1004 assert!(!v.passed);
1005 assert!(
1006 v.failures
1007 .iter()
1008 .any(|f| matches!(f, Failure::SuccessMarkerStale { .. }))
1009 );
1010 }
1011
1012 #[test]
1013 fn malformed_success_marker_body_is_flagged() {
1014 let dir = tempfile::tempdir().unwrap();
1015 let m = build_manifest(
1016 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1017 ManifestStatus::Success,
1018 );
1019 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1020 std::fs::write(dir.path().join(SUCCESS_FILENAME), b"not even xxh3 shaped").unwrap();
1021 let dest = local_dest(dir.path());
1022
1023 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1024 assert!(!v.passed);
1025 assert!(
1026 v.failures
1027 .iter()
1028 .any(|f| matches!(f, Failure::SuccessMarkerMalformed { .. }))
1029 );
1030 }
1031
1032 #[test]
1033 fn absent_success_marker_does_not_fail_validation_alone() {
1034 let dir = tempfile::tempdir().unwrap();
1038 let m = build_manifest(
1039 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1040 ManifestStatus::Failed,
1041 );
1042 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1043 assert!(!dir.path().join(SUCCESS_FILENAME).exists());
1046 let dest = local_dest(dir.path());
1047
1048 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1049 assert!(v.manifest_found);
1050 assert!(
1051 !v.success_marker_consistent,
1052 "no marker => false (no signal)"
1053 );
1054 assert!(v.passed);
1056 assert!(v.failures.is_empty());
1057 }
1058
1059 #[test]
1062 fn self_inconsistent_manifest_is_flagged_but_part_check_still_runs() {
1063 let dir = tempfile::tempdir().unwrap();
1064 let mut m = build_manifest(
1065 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1066 ManifestStatus::Success,
1067 );
1068 m.row_count = 9999; let body = serde_json::to_vec_pretty(&m).unwrap();
1071 std::fs::write(dir.path().join("part-000001.parquet"), b"AAAA").unwrap();
1072 std::fs::write(dir.path().join(MANIFEST_FILENAME), &body).unwrap();
1073 std::fs::write(
1074 dir.path().join(SUCCESS_FILENAME),
1075 success_marker_body(&body),
1076 )
1077 .unwrap();
1078 let dest = local_dest(dir.path());
1079
1080 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1081 assert!(v.manifest_found);
1082 assert!(!v.manifest_self_consistent);
1083 assert!(!v.passed);
1084 assert_eq!(v.parts_verified, 1);
1087 assert!(
1088 v.failures
1089 .iter()
1090 .any(|f| matches!(f, Failure::ManifestSelfInconsistent { .. }))
1091 );
1092 }
1093
1094 #[test]
1097 fn verify_counters_track_present_and_failed_parts() {
1098 let dir = tempfile::tempdir().unwrap();
1103 let m = build_manifest(
1104 vec![
1105 part(1, 10, 4, "xxh3:1111111111111111"),
1106 part(2, 10, 4, "xxh3:2222222222222222"),
1107 ],
1108 ManifestStatus::Success,
1109 );
1110 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1112 let dest = local_dest(dir.path());
1113
1114 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1115 assert!(v.manifest_found, "manifest is at the prefix");
1116 assert_eq!(v.parts_verified, 1, "part 1 is present at its size");
1117 assert_eq!(v.parts_failed, 1, "part 2 is missing");
1118 assert!(
1119 v.failures
1120 .iter()
1121 .any(|f| matches!(f, Failure::PartMissing { part_id: 2, .. })),
1122 "the missing part is named in failures: {:?}",
1123 v.failures
1124 );
1125 assert!(!v.passed, "a missing part must fail the verdict");
1126 }
1127
1128 #[test]
1129 fn read_capped_boundary_is_exact() {
1130 let dir = tempfile::tempdir().unwrap();
1133 std::fs::write(dir.path().join("at-cap.bin"), vec![0u8; 8]).unwrap();
1134 std::fs::write(dir.path().join("over-cap.bin"), vec![0u8; 9]).unwrap();
1135 let dest = local_dest(dir.path());
1136 assert_eq!(
1137 read_capped(&dest, "at-cap.bin", 8)
1138 .expect("exactly at cap loads")
1139 .len(),
1140 8
1141 );
1142 let err = read_capped(&dest, "over-cap.bin", 8).expect_err("over cap refuses");
1143 assert!(
1144 err.to_string().contains("read cap"),
1145 "actionable message: {err:#}"
1146 );
1147 }
1148
1149 #[test]
1150 fn preview_truncates_only_past_forty_chars() {
1151 let s39: String = "x".repeat(39);
1152 let s40: String = "x".repeat(40);
1153 let s41: String = "x".repeat(41);
1154 assert_eq!(preview(&s39), s39, "under the limit: unchanged");
1155 assert_eq!(
1156 preview(&s40),
1157 s40,
1158 "exactly at the limit: unchanged (`>` not `>=`)"
1159 );
1160 assert_eq!(
1161 preview(&s41),
1162 format!("{s40}\u{2026}"),
1163 "past the limit: 40 chars + ellipsis"
1164 );
1165 }
1166
1167 #[test]
1168 fn untracked_object_under_prefix_is_flagged() {
1169 let dir = tempfile::tempdir().unwrap();
1170 let m = build_manifest(
1171 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1172 ManifestStatus::Success,
1173 );
1174 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1175 std::fs::write(dir.path().join("rogue.parquet"), b"XX").unwrap();
1176 let dest = local_dest(dir.path());
1177
1178 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1179 assert!(
1180 v.failures.iter().any(
1181 |f| matches!(f, Failure::UntrackedObject { key, .. } if key == "rogue.parquet")
1182 )
1183 );
1184 assert!(v.passed);
1188 }
1189
1190 #[test]
1191 fn quarantine_prefix_objects_are_silently_ignored() {
1192 let dir = tempfile::tempdir().unwrap();
1193 let m = build_manifest(
1194 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1195 ManifestStatus::Success,
1196 );
1197 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1198 std::fs::create_dir_all(dir.path().join(crate::manifest::QUARANTINE_PREFIX)).unwrap();
1199 std::fs::write(
1200 dir.path()
1201 .join(crate::manifest::QUARANTINE_PREFIX)
1202 .join("old.parquet"),
1203 b"OO",
1204 )
1205 .unwrap();
1206 let dest = local_dest(dir.path());
1207
1208 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1209 assert!(v.passed);
1210 assert!(
1211 !v.failures
1212 .iter()
1213 .any(|f| matches!(f, Failure::UntrackedObject { .. })),
1214 "quarantine_prefix is the legitimate home for these — must not flag"
1215 );
1216 }
1217
1218 #[test]
1219 fn doctor_probe_is_not_flagged_as_untracked() {
1220 let dir = tempfile::tempdir().unwrap();
1226 let m = build_manifest(
1227 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1228 ManifestStatus::Success,
1229 );
1230 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1231 std::fs::write(
1232 dir.path().join(crate::manifest::DOCTOR_PROBE_FILENAME),
1233 b"ok",
1234 )
1235 .unwrap();
1236 let dest = local_dest(dir.path());
1237
1238 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1239 assert!(
1240 !v.has_failures(),
1241 "doctor probe must not surface as a failure: {:?}",
1242 v.failures
1243 );
1244 assert!(v.passed);
1245 }
1246
1247 #[test]
1250 fn verifies_in_subdirectory_when_manifest_dir_is_non_empty() {
1251 let outer = tempfile::tempdir().unwrap();
1252 std::fs::create_dir_all(outer.path().join("sub/run")).unwrap();
1253 let m = build_manifest(
1254 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1255 ManifestStatus::Success,
1256 );
1257 let body = serde_json::to_vec_pretty(&m).unwrap();
1258 std::fs::write(outer.path().join("sub/run/part-000001.parquet"), b"AAAA").unwrap();
1259 std::fs::write(outer.path().join("sub/run").join(MANIFEST_FILENAME), &body).unwrap();
1260 std::fs::write(
1261 outer.path().join("sub/run").join(SUCCESS_FILENAME),
1262 success_marker_body(&body),
1263 )
1264 .unwrap();
1265 let dest = local_dest(outer.path());
1266
1267 let v = verify_at_destination(&dest, "sub/run", ValidateDepth::Full).unwrap();
1268 assert!(v.passed);
1269 assert_eq!(v.parts_verified, 1);
1270
1271 let v2 = verify_at_destination(&dest, "sub/run/", ValidateDepth::Full).unwrap();
1273 assert!(v2.passed);
1274 }
1275
1276 struct ListFails(LocalDestination);
1282 impl crate::destination::Destination for ListFails {
1283 fn write(&self, p: &Path, k: &str) -> Result<crate::destination::WriteOutcome> {
1284 self.0.write(p, k)
1285 }
1286 fn capabilities(&self) -> crate::destination::DestinationCapabilities {
1287 self.0.capabilities()
1288 }
1289 fn head(&self, k: &str) -> Result<Option<crate::destination::ObjectMeta>> {
1290 self.0.head(k)
1291 }
1292 fn read(&self, k: &str) -> Result<Vec<u8>> {
1293 self.0.read(k)
1294 }
1295 fn list_prefix(&self, _: &str) -> Result<Vec<crate::destination::ObjectMeta>> {
1296 anyhow::bail!("listing unavailable")
1297 }
1298 }
1299
1300 #[test]
1301 fn list_failure_cannot_certify_parts_and_fails_the_audit() {
1302 let dir = tempfile::tempdir().unwrap();
1303 let m = build_manifest(vec![part(0, 3, 3, "xxh3:0")], ManifestStatus::Success);
1304 write_dataset(dir.path(), &m, &[("part-000000.parquet", b"abc")]);
1305 let dest = ListFails(local_dest(dir.path()));
1306
1307 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1308 assert!(v.manifest_found);
1310 assert!(v.manifest_self_consistent);
1311 assert!(
1313 !v.passed,
1314 "an audit that cannot list the prefix must not pass"
1315 );
1316 assert_eq!(v.parts_verified, 0);
1317 assert!(
1318 v.failures
1319 .iter()
1320 .any(|f| matches!(f, Failure::ListPrefixError { .. })),
1321 "expected a ListPrefixError, got: {:?}",
1322 v.failures
1323 );
1324 }
1325
1326 struct ManifestReadFails(LocalDestination);
1332 impl crate::destination::Destination for ManifestReadFails {
1333 fn write(&self, p: &Path, k: &str) -> Result<crate::destination::WriteOutcome> {
1334 self.0.write(p, k)
1335 }
1336 fn capabilities(&self) -> crate::destination::DestinationCapabilities {
1337 self.0.capabilities()
1338 }
1339 fn head(&self, k: &str) -> Result<Option<crate::destination::ObjectMeta>> {
1340 self.0.head(k)
1341 }
1342 fn read(&self, k: &str) -> Result<Vec<u8>> {
1343 if k.ends_with(MANIFEST_FILENAME) {
1344 anyhow::bail!("permission denied (simulated)")
1345 }
1346 self.0.read(k)
1347 }
1348 fn list_prefix(&self, p: &str) -> Result<Vec<crate::destination::ObjectMeta>> {
1349 self.0.list_prefix(p)
1350 }
1351 }
1352
1353 #[test]
1354 fn unreadable_manifest_is_explicit_failure_not_legacy() {
1355 let dir = tempfile::tempdir().unwrap();
1359 let m = build_manifest(
1360 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1361 ManifestStatus::Success,
1362 );
1363 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1364 let dest = ManifestReadFails(local_dest(dir.path()));
1365
1366 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1367 assert!(!v.manifest_found);
1368 assert!(!v.legacy_run, "a read error is not the M6 legacy label");
1369 assert!(!v.passed);
1370 assert!(v.has_failures(), "orchestrators need a reason to refuse");
1371 assert!(
1372 matches!(v.failures.as_slice(), [Failure::ManifestReadError { .. }]),
1373 "expected exactly one ManifestReadError, got: {:?}",
1374 v.failures
1375 );
1376 }
1377
1378 struct ManifestHeadFails(LocalDestination);
1381 impl crate::destination::Destination for ManifestHeadFails {
1382 fn write(&self, p: &Path, k: &str) -> Result<crate::destination::WriteOutcome> {
1383 self.0.write(p, k)
1384 }
1385 fn capabilities(&self) -> crate::destination::DestinationCapabilities {
1386 self.0.capabilities()
1387 }
1388 fn head(&self, k: &str) -> Result<Option<crate::destination::ObjectMeta>> {
1389 if k.ends_with(MANIFEST_FILENAME) {
1390 anyhow::bail!("io timeout (simulated)")
1391 }
1392 self.0.head(k)
1393 }
1394 fn read(&self, k: &str) -> Result<Vec<u8>> {
1395 self.0.read(k)
1396 }
1397 fn list_prefix(&self, p: &str) -> Result<Vec<crate::destination::ObjectMeta>> {
1398 self.0.list_prefix(p)
1399 }
1400 }
1401
1402 #[test]
1403 fn manifest_head_error_is_explicit_failure_not_legacy() {
1404 let dir = tempfile::tempdir().unwrap();
1405 let m = build_manifest(
1406 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1407 ManifestStatus::Success,
1408 );
1409 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1410 let dest = ManifestHeadFails(local_dest(dir.path()));
1411
1412 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1413 assert!(!v.manifest_found);
1414 assert!(!v.legacy_run);
1415 assert!(!v.passed);
1416 assert!(
1417 matches!(
1418 v.failures.as_slice(),
1419 [Failure::ManifestReadError { detail }] if detail.contains("manifest head failed")
1420 ),
1421 "expected one ManifestReadError naming the head step, got: {:?}",
1422 v.failures
1423 );
1424 }
1425
1426 struct SuccessMarkerOversized(LocalDestination);
1432 impl crate::destination::Destination for SuccessMarkerOversized {
1433 fn write(&self, p: &Path, k: &str) -> Result<crate::destination::WriteOutcome> {
1434 self.0.write(p, k)
1435 }
1436 fn capabilities(&self) -> crate::destination::DestinationCapabilities {
1437 self.0.capabilities()
1438 }
1439 fn head(&self, k: &str) -> Result<Option<crate::destination::ObjectMeta>> {
1440 if k.ends_with(crate::manifest::SUCCESS_FILENAME) {
1441 return Ok(Some(crate::destination::ObjectMeta {
1442 key: k.to_string(),
1443 size_bytes: SUCCESS_MARKER_MAX_BYTES * 4,
1444 content_md5: None,
1445 }));
1446 }
1447 self.0.head(k)
1448 }
1449 fn read(&self, k: &str) -> Result<Vec<u8>> {
1450 assert!(
1451 !k.ends_with(crate::manifest::SUCCESS_FILENAME),
1452 "verify must NOT read an oversized _SUCCESS — the size cap must short-circuit \
1453 before materialising it into memory (the OOM this guards)"
1454 );
1455 self.0.read(k)
1456 }
1457 fn list_prefix(&self, p: &str) -> Result<Vec<crate::destination::ObjectMeta>> {
1458 self.0.list_prefix(p)
1459 }
1460 }
1461
1462 #[test]
1463 fn oversized_success_marker_is_malformed_and_never_read() {
1464 let dir = tempfile::tempdir().unwrap();
1465 let m = build_manifest(
1466 vec![part(1, 10, 4, "xxh3:1111111111111111")],
1467 ManifestStatus::Success,
1468 );
1469 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]);
1470 let dest = SuccessMarkerOversized(local_dest(dir.path()));
1471
1472 let v = verify_at_destination(&dest, "", ValidateDepth::Full).unwrap();
1473 assert!(
1474 v.failures.iter().any(|f| matches!(
1475 f,
1476 Failure::SuccessMarkerMalformed { body_preview } if body_preview.contains("oversized")
1477 )),
1478 "an oversized _SUCCESS must yield SuccessMarkerMalformed(oversized) without reading it, got: {:?}",
1479 v.failures
1480 );
1481 }
1482
1483 #[test]
1484 fn passed_is_derived_advisory_failures_do_not_fail() {
1485 let mut v = ManifestVerification {
1487 manifest_found: true,
1488 ..ManifestVerification::empty()
1489 };
1490 v.failures.push(Failure::UntrackedObject {
1491 key: "stray.parquet".into(),
1492 size_bytes: 9,
1493 });
1494 v.recompute_passed();
1495 assert!(v.passed, "untracked surplus is advisory, not fatal");
1496
1497 v.failures.push(Failure::PartMissing {
1499 part_id: 1,
1500 path: "part-000001.parquet".into(),
1501 });
1502 v.recompute_passed();
1503 assert!(!v.passed, "a missing part is fatal");
1504
1505 let mut legacy = ManifestVerification::empty();
1507 legacy.recompute_passed();
1508 assert!(!legacy.passed, "no manifest found → cannot certify");
1509 }
1510
1511 #[test]
1512 fn verify_content_policy_fails_only_size_only_parts() {
1513 let base = ManifestVerification {
1515 manifest_found: true,
1516 parts_verified: 3,
1517 parts_md5_verified: 2,
1518 ..ManifestVerification::empty()
1519 };
1520 let mut sz = base.clone();
1522 sz.recompute_passed();
1523 sz.enforce_content_policy(false);
1524 assert!(sz.passed, "size-only OK under verify: size");
1525
1526 let mut ct = base.clone();
1528 ct.recompute_passed();
1529 ct.enforce_content_policy(true);
1530 assert!(!ct.passed, "a size-only part fails verify: content");
1531 assert!(
1532 ct.failures.iter().any(|f| matches!(
1533 f,
1534 Failure::ContentVerificationUnmet {
1535 size_only: 1,
1536 total: 3
1537 }
1538 )),
1539 "expected ContentVerificationUnmet, got: {:?}",
1540 ct.failures
1541 );
1542
1543 let mut all = ManifestVerification {
1545 parts_md5_verified: 3,
1546 ..base
1547 };
1548 all.recompute_passed();
1549 all.enforce_content_policy(true);
1550 assert!(
1551 all.passed && all.failures.is_empty(),
1552 "all md5 meets verify: content"
1553 );
1554 }
1555
1556 #[test]
1559 fn require_manifest_escalates_legacy_to_fatal_absent() {
1560 let mut v = ManifestVerification::legacy();
1565 assert!(v.legacy_run && !v.has_failures());
1566
1567 v.require_manifest_present("exports/2026-06-09/orders/");
1568
1569 assert!(!v.legacy_run, "no longer the benign legacy-run label");
1570 assert!(!v.passed, "an absent-but-required manifest cannot pass");
1571 assert!(
1572 matches!(
1573 v.failures.as_slice(),
1574 [Failure::ManifestRequiredButAbsent { prefix }]
1575 if prefix == "exports/2026-06-09/orders/"
1576 ),
1577 "expected one ManifestRequiredButAbsent naming the prefix, got: {:?}",
1578 v.failures
1579 );
1580 }
1581
1582 #[test]
1583 fn require_manifest_is_noop_on_a_real_passing_manifest() {
1584 let mut v = ManifestVerification {
1587 manifest_found: true,
1588 manifest_self_consistent: true,
1589 parts_verified: 1,
1590 passed: true,
1591 ..ManifestVerification::empty()
1592 };
1593 v.require_manifest_present("exports/orders/");
1594 assert!(
1595 v.passed && v.failures.is_empty(),
1596 "real dataset still passes"
1597 );
1598 }
1599
1600 #[test]
1601 fn require_manifest_does_not_double_flag_a_read_error() {
1602 let mut v = ManifestVerification::legacy();
1606 v.legacy_run = false;
1607 v.failures.push(Failure::ManifestReadError {
1608 detail: "permission denied".into(),
1609 });
1610 v.recompute_passed();
1611
1612 v.require_manifest_present("exports/orders/");
1613
1614 assert!(
1615 matches!(v.failures.as_slice(), [Failure::ManifestReadError { .. }]),
1616 "must leave the existing read-error verdict alone, got: {:?}",
1617 v.failures
1618 );
1619 }
1620
1621 #[test]
1624 fn light_depth_skips_part_reconcile_even_when_a_part_is_missing() {
1625 let dir = tempfile::tempdir().unwrap();
1632 let m = build_manifest(
1633 vec![
1634 part(1, 10, 4, "xxh3:1111111111111111"),
1635 part(2, 20, 5, "xxh3:2222222222222222"),
1636 ],
1637 ManifestStatus::Success,
1638 );
1639 write_dataset(dir.path(), &m, &[]);
1641 let dest = local_dest(dir.path());
1642
1643 let v = verify_at_destination(&dest, "", ValidateDepth::Light).unwrap();
1644 assert_eq!(v.depth_level, "light");
1645 assert_eq!(
1646 v.parts_verified, 0,
1647 "light skips the listing — no part is ever verified"
1648 );
1649 assert_eq!(
1650 v.parts_failed, 0,
1651 "no part reconcile means no part failures"
1652 );
1653 assert!(
1654 !v.failures.iter().any(|f| matches!(
1655 f,
1656 Failure::PartMissing { .. } | Failure::ListPrefixError { .. }
1657 )),
1658 "light must not surface part or list failures, got: {:?}",
1659 v.failures
1660 );
1661 assert!(
1662 v.success_marker_consistent,
1663 "_SUCCESS is still checked at light depth"
1664 );
1665 assert!(v.manifest_self_consistent);
1666 assert!(
1667 v.passed,
1668 "manifest + _SUCCESS are consistent, so a light pass certifies it"
1669 );
1670 }
1671
1672 #[test]
1673 fn light_depth_never_lists_so_a_list_failure_cannot_trip() {
1674 let dir = tempfile::tempdir().unwrap();
1679 let m = build_manifest(vec![part(0, 3, 3, "xxh3:0")], ManifestStatus::Success);
1680 write_dataset(dir.path(), &m, &[("part-000000.parquet", b"abc")]);
1681 let dest = ListFails(local_dest(dir.path()));
1682
1683 let v = verify_at_destination(&dest, "", ValidateDepth::Light).unwrap();
1684 assert_eq!(v.depth_level, "light");
1685 assert!(
1686 !v.failures
1687 .iter()
1688 .any(|f| matches!(f, Failure::ListPrefixError { .. })),
1689 "light never lists, so a failing list_prefix cannot surface, got: {:?}",
1690 v.failures
1691 );
1692 assert_eq!(v.parts_verified, 0);
1693 assert!(v.passed, "manifest + _SUCCESS consistent → light passes");
1694 }
1695
1696 #[test]
1697 fn sample_depth_runs_part_reconcile_like_full() {
1698 let dir = tempfile::tempdir().unwrap();
1702 let m = build_manifest(
1703 vec![
1704 part(1, 10, 4, "xxh3:1111111111111111"),
1705 part(2, 20, 5, "xxh3:2222222222222222"),
1706 ],
1707 ManifestStatus::Success,
1708 );
1709 write_dataset(dir.path(), &m, &[("part-000001.parquet", b"AAAA")]); let dest = local_dest(dir.path());
1711
1712 let v = verify_at_destination(&dest, "", ValidateDepth::Sample).unwrap();
1713 assert_eq!(v.depth_level, "sample");
1714 assert_eq!(v.parts_verified, 1);
1715 assert_eq!(v.parts_failed, 1);
1716 assert!(!v.passed);
1717 assert!(
1718 v.failures
1719 .iter()
1720 .any(|f| matches!(f, Failure::PartMissing { part_id: 2, .. })),
1721 "sample reconciles parts just like full, got: {:?}",
1722 v.failures
1723 );
1724 }
1725
1726 #[test]
1727 fn depth_level_is_stamped_on_every_verdict_shape() {
1728 let dir = tempfile::tempdir().unwrap();
1732 let dest = local_dest(dir.path()); for depth in [
1734 ValidateDepth::Light,
1735 ValidateDepth::Sample,
1736 ValidateDepth::Full,
1737 ] {
1738 let v = verify_at_destination(&dest, "", depth).unwrap();
1739 assert!(v.legacy_run, "empty prefix is the legacy shape");
1740 assert_eq!(
1741 v.depth_level,
1742 depth.label(),
1743 "legacy verdict must still carry its depth label"
1744 );
1745 }
1746 }
1747
1748 #[test]
1749 fn error_code_is_stable_and_distinct_per_variant() {
1750 let cases: &[(Failure, &str)] = &[
1754 (
1755 Failure::PartMissing {
1756 part_id: 1,
1757 path: "p".into(),
1758 },
1759 "RIVET_VERIFY_PART_MISSING",
1760 ),
1761 (
1762 Failure::PartSizeMismatch {
1763 part_id: 1,
1764 path: "p".into(),
1765 expected: 1,
1766 actual: 2,
1767 },
1768 "RIVET_VERIFY_PART_SIZE_MISMATCH",
1769 ),
1770 (
1771 Failure::PartChecksumMismatch {
1772 part_id: 1,
1773 path: "p".into(),
1774 expected: "a".into(),
1775 actual: "b".into(),
1776 },
1777 "RIVET_VERIFY_PART_CHECKSUM_MISMATCH",
1778 ),
1779 (
1780 Failure::SuccessMarkerMalformed {
1781 body_preview: "x".into(),
1782 },
1783 "RIVET_VERIFY_SUCCESS_MALFORMED",
1784 ),
1785 (
1786 Failure::SuccessMarkerStale {
1787 marker_fingerprint: "a".into(),
1788 manifest_fingerprint: "b".into(),
1789 },
1790 "RIVET_VERIFY_SUCCESS_STALE",
1791 ),
1792 (
1793 Failure::ManifestSelfInconsistent { detail: "d".into() },
1794 "RIVET_VERIFY_MANIFEST_INCONSISTENT",
1795 ),
1796 (
1797 Failure::ManifestReadError { detail: "d".into() },
1798 "RIVET_VERIFY_MANIFEST_READ_ERROR",
1799 ),
1800 (
1801 Failure::SuccessMarkerReadError { detail: "d".into() },
1802 "RIVET_VERIFY_SUCCESS_READ_ERROR",
1803 ),
1804 (
1805 Failure::ListPrefixError { detail: "d".into() },
1806 "RIVET_VERIFY_LIST_ERROR",
1807 ),
1808 (
1809 Failure::UntrackedObject {
1810 key: "k".into(),
1811 size_bytes: 1,
1812 },
1813 "RIVET_VERIFY_UNTRACKED_OBJECT",
1814 ),
1815 (
1816 Failure::ContentVerificationUnmet {
1817 size_only: 1,
1818 total: 2,
1819 },
1820 "RIVET_VERIFY_CONTENT_UNMET",
1821 ),
1822 (
1823 Failure::ManifestRequiredButAbsent { prefix: "p".into() },
1824 "RIVET_VERIFY_MANIFEST_REQUIRED",
1825 ),
1826 ];
1827 for (failure, code) in cases {
1828 assert_eq!(&failure.error_code(), code, "code for {failure:?}");
1829 assert!(
1830 failure.error_code().starts_with("RIVET_VERIFY_"),
1831 "every code shares the RIVET_VERIFY_ prefix"
1832 );
1833 }
1834 }
1835}