1use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::time::{Duration, Instant};
34
35use crate::oracle::{ExecutionResult, Verdict, VerificationResult};
36use crate::Language;
37
38#[derive(Clone, Debug, Serialize, Deserialize)]
46pub struct VerificationPath {
47 pub test_id: String,
49
50 pub source_language: Option<Language>,
52
53 pub target_language: Option<Language>,
55
56 pub verdict: Option<Verdict>,
58
59 pub duration_ns: u64,
61
62 pub source_result: Option<ExecutionSummary>,
64
65 pub target_result: Option<ExecutionSummary>,
67
68 contributions: Vec<f32>,
70
71 confidence: f32,
73
74 pub metadata: HashMap<String, serde_json::Value>,
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct ExecutionSummary {
81 pub exit_code: i32,
83
84 pub stdout_len: usize,
86
87 pub stderr_len: usize,
89
90 pub duration_ms: u64,
92
93 pub stdout_hash: u64,
95}
96
97impl From<&ExecutionResult> for ExecutionSummary {
98 fn from(result: &ExecutionResult) -> Self {
99 Self {
100 exit_code: result.exit_code,
101 stdout_len: result.stdout.len(),
102 stderr_len: result.stderr.len(),
103 duration_ms: result.duration_ms,
104 stdout_hash: hash_string(&result.stdout),
105 }
106 }
107}
108
109impl VerificationPath {
110 pub fn new(test_id: impl Into<String>) -> Self {
112 Self {
113 test_id: test_id.into(),
114 source_language: None,
115 target_language: None,
116 verdict: None,
117 duration_ns: 0,
118 source_result: None,
119 target_result: None,
120 contributions: Vec::new(),
121 confidence: 1.0,
122 metadata: HashMap::new(),
123 }
124 }
125
126 #[must_use]
128 pub fn with_verdict(mut self, verdict: Verdict) -> Self {
129 self.confidence = match &verdict {
131 Verdict::Pass => 1.0,
132 Verdict::OutputMismatch { .. } => 0.0,
133 Verdict::Timeout { .. } => 0.3,
134 Verdict::RuntimeError { .. } => 0.0,
135 };
136 self.verdict = Some(verdict);
137 self
138 }
139
140 #[must_use]
142 pub fn with_duration(mut self, duration: Duration) -> Self {
143 self.duration_ns = duration.as_nanos() as u64;
144 self
145 }
146
147 #[must_use]
149 pub fn with_languages(mut self, source: Language, target: Language) -> Self {
150 self.source_language = Some(source);
151 self.target_language = Some(target);
152 self
153 }
154
155 #[must_use]
157 pub fn with_source_result(mut self, result: &ExecutionResult) -> Self {
158 self.source_result = Some(ExecutionSummary::from(result));
159 self
160 }
161
162 #[must_use]
164 pub fn with_target_result(mut self, result: &ExecutionResult) -> Self {
165 self.target_result = Some(ExecutionSummary::from(result));
166 self
167 }
168
169 #[must_use]
171 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
172 self.metadata.insert(key.into(), value);
173 self
174 }
175
176 #[must_use]
178 pub fn with_contributions(mut self, contributions: Vec<f32>) -> Self {
179 self.contributions = contributions;
180 self
181 }
182
183 pub fn feature_contributions(&self) -> &[f32] {
185 &self.contributions
186 }
187
188 pub fn confidence(&self) -> f32 {
190 self.confidence
191 }
192
193 pub fn passed(&self) -> bool {
195 matches!(self.verdict, Some(Verdict::Pass))
196 }
197
198 pub fn to_bytes(&self) -> Vec<u8> {
200 let mut bytes = Vec::new();
201
202 bytes.extend_from_slice(self.test_id.as_bytes());
204 bytes.push(0);
205
206 bytes.extend_from_slice(&self.duration_ns.to_le_bytes());
208
209 bytes.extend_from_slice(&self.confidence.to_le_bytes());
211
212 let verdict_byte = match &self.verdict {
214 Some(Verdict::Pass) => 1u8,
215 Some(Verdict::OutputMismatch { .. }) => 2u8,
216 Some(Verdict::Timeout { .. }) => 3u8,
217 Some(Verdict::RuntimeError { .. }) => 4u8,
218 None => 0u8,
219 };
220 bytes.push(verdict_byte);
221
222 bytes
223 }
224
225 pub fn explain(&self) -> String {
227 use std::fmt::Write;
228
229 let mut explanation = format!("Test: {}\n", self.test_id);
230
231 if let Some(src) = &self.source_language {
232 let _ = writeln!(explanation, "Source: {src:?}");
233 }
234 if let Some(tgt) = &self.target_language {
235 let _ = writeln!(explanation, "Target: {tgt:?}");
236 }
237
238 let _ = writeln!(
239 explanation,
240 "Duration: {:.2}ms",
241 self.duration_ns as f64 / 1_000_000.0
242 );
243
244 if let Some(ref verdict) = self.verdict {
245 let _ = writeln!(explanation, "Verdict: {verdict:?}");
246 }
247
248 let _ = write!(explanation, "Confidence: {:.1}%", self.confidence * 100.0);
249
250 explanation
251 }
252}
253
254impl From<&VerificationResult> for VerificationPath {
255 fn from(result: &VerificationResult) -> Self {
256 let mut path = Self::new(format!(
257 "{}-to-{}",
258 result.source_language, result.target_language
259 ))
260 .with_languages(result.source_language, result.target_language)
261 .with_verdict(result.verdict.clone());
262
263 if let Some(ref src) = result.source_result {
264 path = path.with_source_result(src);
265 }
266 if let Some(ref tgt) = result.target_result {
267 path = path.with_target_result(tgt);
268 }
269
270 path
271 }
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize)]
280pub struct AuditTrace {
281 pub sequence: u64,
283
284 pub timestamp_ns: u64,
286
287 pub path: VerificationPath,
289}
290
291#[derive(Clone, Debug, Serialize, Deserialize)]
293pub struct HashChainEntry {
294 pub sequence: u64,
296
297 pub prev_hash: [u8; 32],
299
300 pub hash: [u8; 32],
302
303 pub trace: AuditTrace,
305}
306
307#[derive(Debug)]
313pub struct AuditCollector {
314 entries: Vec<HashChainEntry>,
316
317 next_sequence: u64,
319
320 suite_id: String,
322
323 stats: AuditStats,
325}
326
327#[derive(Clone, Debug, Default, Serialize, Deserialize)]
329pub struct AuditStats {
330 pub total: usize,
332
333 pub passed: usize,
335
336 pub failed: usize,
338
339 pub timeouts: usize,
341
342 pub errors: usize,
344
345 pub total_duration_ns: u64,
347}
348
349impl AuditCollector {
350 pub fn new(suite_id: impl Into<String>) -> Self {
352 Self {
353 entries: Vec::new(),
354 next_sequence: 0,
355 suite_id: suite_id.into(),
356 stats: AuditStats::default(),
357 }
358 }
359
360 pub fn suite_id(&self) -> &str {
362 &self.suite_id
363 }
364
365 pub fn record(&mut self, path: VerificationPath) -> &HashChainEntry {
367 self.stats.total += 1;
369 self.stats.total_duration_ns += path.duration_ns;
370
371 match &path.verdict {
372 Some(Verdict::Pass) => self.stats.passed += 1,
373 Some(Verdict::OutputMismatch { .. }) => self.stats.failed += 1,
374 Some(Verdict::Timeout { .. }) => self.stats.timeouts += 1,
375 Some(Verdict::RuntimeError { .. }) => self.stats.errors += 1,
376 None => {}
377 }
378
379 let timestamp_ns = std::time::SystemTime::now()
380 .duration_since(std::time::UNIX_EPOCH)
381 .map(|d| d.as_nanos() as u64)
382 .unwrap_or(0);
383
384 let trace = AuditTrace {
385 sequence: self.next_sequence,
386 timestamp_ns,
387 path,
388 };
389
390 let prev_hash = self.entries.last().map_or([0u8; 32], |e| e.hash);
392
393 let hash = self.compute_hash(&trace, &prev_hash);
395
396 let entry = HashChainEntry {
397 sequence: self.next_sequence,
398 prev_hash,
399 hash,
400 trace,
401 };
402
403 self.entries.push(entry);
404 self.next_sequence += 1;
405
406 &self.entries[self.entries.len() - 1]
409 }
410
411 fn compute_hash(&self, trace: &AuditTrace, prev_hash: &[u8; 32]) -> [u8; 32] {
413 use std::collections::hash_map::DefaultHasher;
414 use std::hash::{Hash, Hasher};
415
416 let mut hasher = DefaultHasher::new();
417
418 prev_hash.hash(&mut hasher);
419 trace.sequence.hash(&mut hasher);
420 trace.timestamp_ns.hash(&mut hasher);
421 trace.path.test_id.hash(&mut hasher);
422 trace.path.duration_ns.hash(&mut hasher);
423
424 let hash_value = hasher.finish();
425
426 let mut result = [0u8; 32];
427 for i in 0..4 {
428 result[i * 8..(i + 1) * 8].copy_from_slice(&hash_value.to_le_bytes());
429 }
430
431 result
432 }
433
434 pub fn entries(&self) -> &[HashChainEntry] {
436 &self.entries
437 }
438
439 pub fn len(&self) -> usize {
441 self.entries.len()
442 }
443
444 pub fn is_empty(&self) -> bool {
446 self.entries.is_empty()
447 }
448
449 pub fn stats(&self) -> &AuditStats {
451 &self.stats
452 }
453
454 pub fn verify_chain(&self) -> ChainVerification {
456 contract_pre_oracle_verdict!(input);
457 let mut entries_verified = 0;
458
459 for (i, entry) in self.entries.iter().enumerate() {
460 if i == 0 {
462 if entry.prev_hash != [0u8; 32] {
463 return ChainVerification {
464 valid: false,
465 entries_verified,
466 first_break: Some(0),
467 };
468 }
469 } else {
470 let expected_prev = self.entries[i - 1].hash;
471 if entry.prev_hash != expected_prev {
472 return ChainVerification {
473 valid: false,
474 entries_verified,
475 first_break: Some(i),
476 };
477 }
478 }
479
480 let computed_hash = self.compute_hash(&entry.trace, &entry.prev_hash);
482 if entry.hash != computed_hash {
483 return ChainVerification {
484 valid: false,
485 entries_verified,
486 first_break: Some(i),
487 };
488 }
489
490 entries_verified += 1;
491 }
492
493 ChainVerification {
494 valid: true,
495 entries_verified,
496 first_break: None,
497 }
498 }
499
500 pub fn recent(&self, n: usize) -> Vec<&HashChainEntry> {
502 self.entries.iter().rev().take(n).collect()
503 }
504
505 pub fn failures(&self) -> Vec<&HashChainEntry> {
507 self.entries
508 .iter()
509 .filter(|e| !e.trace.path.passed())
510 .collect()
511 }
512
513 pub fn to_json(&self) -> Result<String, serde_json::Error> {
519 #[derive(Serialize)]
520 struct Export<'a> {
521 suite_id: &'a str,
522 chain_length: usize,
523 verified: bool,
524 stats: &'a AuditStats,
525 entries: &'a [HashChainEntry],
526 }
527
528 let verification = self.verify_chain();
529
530 let export = Export {
531 suite_id: &self.suite_id,
532 chain_length: self.entries.len(),
533 verified: verification.valid,
534 stats: &self.stats,
535 entries: &self.entries,
536 };
537
538 serde_json::to_string_pretty(&export)
539 }
540}
541
542#[derive(Clone, Debug, Serialize, Deserialize)]
544pub struct ChainVerification {
545 pub valid: bool,
547
548 pub entries_verified: usize,
550
551 pub first_break: Option<usize>,
553}
554
555#[derive(Debug)]
561pub struct VerificationTimer {
562 start: Instant,
563 test_id: String,
564}
565
566impl VerificationTimer {
567 pub fn start(test_id: impl Into<String>) -> Self {
569 Self {
570 start: Instant::now(),
571 test_id: test_id.into(),
572 }
573 }
574
575 pub fn stop(self) -> VerificationPath {
577 let duration = self.start.elapsed();
578 VerificationPath::new(self.test_id).with_duration(duration)
579 }
580
581 pub fn stop_with_verdict(self, verdict: Verdict) -> VerificationPath {
583 let duration = self.start.elapsed();
584 VerificationPath::new(self.test_id)
585 .with_duration(duration)
586 .with_verdict(verdict)
587 }
588}
589
590fn hash_string(s: &str) -> u64 {
596 use std::collections::hash_map::DefaultHasher;
597 use std::hash::{Hash, Hasher};
598
599 let mut hasher = DefaultHasher::new();
600 s.hash(&mut hasher);
601 hasher.finish()
602}
603
604#[must_use]
606pub fn new_audit_collector() -> AuditCollector {
607 let suite_id = format!(
608 "suite-{}",
609 std::time::SystemTime::now()
610 .duration_since(std::time::UNIX_EPOCH)
611 .map(|d| d.as_millis())
612 .unwrap_or(0)
613 );
614 AuditCollector::new(suite_id)
615}
616
617#[cfg(test)]
622#[allow(clippy::unwrap_used)]
623mod tests {
624 use super::*;
625 use crate::oracle::Phase;
626
627 #[test]
628 fn test_verification_path_creation() {
629 let path = VerificationPath::new("test_001");
630 assert_eq!(path.test_id, "test_001");
631 assert_eq!(path.confidence(), 1.0);
632 assert!(path.verdict.is_none());
633 }
634
635 #[test]
636 fn test_verification_path_with_verdict_pass() {
637 let path = VerificationPath::new("test").with_verdict(Verdict::Pass);
638 assert!(path.passed());
639 assert_eq!(path.confidence(), 1.0);
640 }
641
642 #[test]
643 fn test_verification_path_with_verdict_mismatch() {
644 let path = VerificationPath::new("test").with_verdict(Verdict::OutputMismatch {
645 expected: "a".into(),
646 actual: "b".into(),
647 });
648 assert!(!path.passed());
649 assert_eq!(path.confidence(), 0.0);
650 }
651
652 #[test]
653 fn test_verification_path_with_verdict_timeout() {
654 let path = VerificationPath::new("test").with_verdict(Verdict::Timeout {
655 phase: Phase::Source,
656 limit_ms: 5000,
657 });
658 assert!(!path.passed());
659 assert_eq!(path.confidence(), 0.3);
660 }
661
662 #[test]
663 fn test_verification_path_with_verdict_error() {
664 let path = VerificationPath::new("test").with_verdict(Verdict::RuntimeError {
665 phase: Phase::Target,
666 error: "error".into(),
667 });
668 assert!(!path.passed());
669 assert_eq!(path.confidence(), 0.0);
670 }
671
672 #[test]
673 fn test_verification_path_with_duration() {
674 let path = VerificationPath::new("test").with_duration(Duration::from_millis(100));
675 assert_eq!(path.duration_ns, 100_000_000);
676 }
677
678 #[test]
679 fn test_verification_path_with_languages() {
680 let path = VerificationPath::new("test").with_languages(Language::Python, Language::Rust);
681 assert_eq!(path.source_language, Some(Language::Python));
682 assert_eq!(path.target_language, Some(Language::Rust));
683 }
684
685 #[test]
686 fn test_verification_path_explain() {
687 let path = VerificationPath::new("test_001")
688 .with_duration(Duration::from_millis(50))
689 .with_verdict(Verdict::Pass);
690 let explanation = path.explain();
691 assert!(explanation.contains("test_001"));
692 assert!(explanation.contains("50.00ms"));
693 assert!(explanation.contains("Pass"));
694 }
695
696 #[test]
697 fn test_verification_path_to_bytes() {
698 let path = VerificationPath::new("test");
699 let bytes = path.to_bytes();
700 assert!(!bytes.is_empty());
701 }
702
703 #[test]
704 fn test_execution_summary_from_result() {
705 let result = ExecutionResult {
706 stdout: "hello".to_string(),
707 stderr: "err".to_string(),
708 exit_code: 0,
709 duration_ms: 100,
710 };
711 let summary = ExecutionSummary::from(&result);
712 assert_eq!(summary.exit_code, 0);
713 assert_eq!(summary.stdout_len, 5);
714 assert_eq!(summary.stderr_len, 3);
715 assert_eq!(summary.duration_ms, 100);
716 }
717
718 #[test]
719 fn test_audit_collector_creation() {
720 let collector = AuditCollector::new("suite-001");
721 assert_eq!(collector.suite_id(), "suite-001");
722 assert!(collector.is_empty());
723 }
724
725 #[test]
726 fn test_audit_collector_record() {
727 let mut collector = AuditCollector::new("suite");
728 let path = VerificationPath::new("test_001").with_verdict(Verdict::Pass);
729
730 let entry = collector.record(path);
731
732 assert_eq!(entry.sequence, 0);
733 assert_eq!(entry.prev_hash, [0u8; 32]);
734 assert_eq!(collector.len(), 1);
735 }
736
737 #[test]
738 fn test_audit_collector_stats() {
739 let mut collector = AuditCollector::new("suite");
740
741 collector.record(VerificationPath::new("t1").with_verdict(Verdict::Pass));
742 collector.record(VerificationPath::new("t2").with_verdict(Verdict::Pass));
743 collector.record(
744 VerificationPath::new("t3").with_verdict(Verdict::OutputMismatch {
745 expected: "a".into(),
746 actual: "b".into(),
747 }),
748 );
749 collector.record(VerificationPath::new("t4").with_verdict(Verdict::Timeout {
750 phase: Phase::Source,
751 limit_ms: 5000,
752 }));
753
754 let stats = collector.stats();
755 assert_eq!(stats.total, 4);
756 assert_eq!(stats.passed, 2);
757 assert_eq!(stats.failed, 1);
758 assert_eq!(stats.timeouts, 1);
759 }
760
761 #[test]
762 fn test_audit_collector_hash_chain_linkage() {
763 let mut collector = AuditCollector::new("suite");
764
765 collector.record(VerificationPath::new("t1"));
766 collector.record(VerificationPath::new("t2"));
767 collector.record(VerificationPath::new("t3"));
768
769 let entries = collector.entries();
770
771 assert_eq!(entries[0].prev_hash, [0u8; 32]);
772 assert_eq!(entries[1].prev_hash, entries[0].hash);
773 assert_eq!(entries[2].prev_hash, entries[1].hash);
774 }
775
776 #[test]
777 fn test_audit_collector_verify_chain() {
778 let mut collector = AuditCollector::new("suite");
779
780 collector.record(VerificationPath::new("t1").with_verdict(Verdict::Pass));
781 collector.record(VerificationPath::new("t2").with_verdict(Verdict::Pass));
782
783 let verification = collector.verify_chain();
784 assert!(verification.valid);
785 assert_eq!(verification.entries_verified, 2);
786 assert!(verification.first_break.is_none());
787 }
788
789 #[test]
790 fn test_audit_collector_recent() {
791 let mut collector = AuditCollector::new("suite");
792
793 for i in 0..5 {
794 collector.record(VerificationPath::new(format!("t{}", i)));
795 }
796
797 let recent = collector.recent(3);
798 assert_eq!(recent.len(), 3);
799 assert_eq!(recent[0].sequence, 4);
800 assert_eq!(recent[1].sequence, 3);
801 assert_eq!(recent[2].sequence, 2);
802 }
803
804 #[test]
805 fn test_audit_collector_failures() {
806 let mut collector = AuditCollector::new("suite");
807
808 collector.record(VerificationPath::new("t1").with_verdict(Verdict::Pass));
809 collector.record(
810 VerificationPath::new("t2").with_verdict(Verdict::OutputMismatch {
811 expected: "a".into(),
812 actual: "b".into(),
813 }),
814 );
815 collector.record(VerificationPath::new("t3").with_verdict(Verdict::Pass));
816
817 let failures = collector.failures();
818 assert_eq!(failures.len(), 1);
819 assert_eq!(failures[0].trace.path.test_id, "t2");
820 }
821
822 #[test]
823 fn test_audit_collector_to_json() {
824 let mut collector = AuditCollector::new("suite");
825 collector.record(VerificationPath::new("t1").with_verdict(Verdict::Pass));
826
827 let json = collector.to_json().unwrap();
828 assert!(json.contains("suite"));
829 assert!(json.contains("verified"));
830 assert!(json.contains("stats"));
831 }
832
833 #[test]
834 fn test_verification_timer() {
835 let timer = VerificationTimer::start("test");
836 std::thread::sleep(Duration::from_millis(10));
837 let path = timer.stop();
838
839 assert_eq!(path.test_id, "test");
840 assert!(path.duration_ns > 0);
841 }
842
843 #[test]
844 fn test_verification_timer_with_verdict() {
845 let timer = VerificationTimer::start("test");
846 let path = timer.stop_with_verdict(Verdict::Pass);
847
848 assert!(path.passed());
849 }
850
851 #[test]
852 fn test_new_audit_collector() {
853 let collector = new_audit_collector();
854 assert!(collector.suite_id().starts_with("suite-"));
855 }
856
857 #[test]
858 fn test_verification_path_with_metadata() {
859 let path = VerificationPath::new("test").with_metadata("key", serde_json::json!("value"));
860 assert_eq!(path.metadata.len(), 1);
861 }
862
863 #[test]
864 fn test_verification_path_with_contributions() {
865 let contributions = vec![0.1, 0.2, 0.3];
866 let path = VerificationPath::new("test").with_contributions(contributions.clone());
867 assert_eq!(path.feature_contributions(), &contributions);
868 }
869
870 #[test]
871 fn test_verification_path_with_source_result() {
872 let result = ExecutionResult {
873 stdout: "out".to_string(),
874 stderr: "".to_string(),
875 exit_code: 0,
876 duration_ms: 10,
877 };
878 let path = VerificationPath::new("test").with_source_result(&result);
879 assert!(path.source_result.is_some());
880 }
881
882 #[test]
883 fn test_verification_path_with_target_result() {
884 let result = ExecutionResult {
885 stdout: "out".to_string(),
886 stderr: "".to_string(),
887 exit_code: 0,
888 duration_ms: 10,
889 };
890 let path = VerificationPath::new("test").with_target_result(&result);
891 assert!(path.target_result.is_some());
892 }
893
894 #[test]
895 fn test_verification_path_from_result() {
896 let result = VerificationResult {
897 source_code: "print(1)".to_string(),
898 source_language: Language::Python,
899 target_code: "fn main() {}".to_string(),
900 target_language: Language::Rust,
901 verdict: Verdict::Pass,
902 source_result: None,
903 target_result: None,
904 };
905
906 let path = VerificationPath::from(&result);
907 assert!(path.passed());
908 assert_eq!(path.source_language, Some(Language::Python));
909 assert_eq!(path.target_language, Some(Language::Rust));
910 }
911
912 #[test]
913 fn test_chain_verification_serialization() {
914 let verification = ChainVerification {
915 valid: true,
916 entries_verified: 5,
917 first_break: None,
918 };
919
920 let json = serde_json::to_string(&verification).unwrap();
921 let deserialized: ChainVerification = serde_json::from_str(&json).unwrap();
922
923 assert_eq!(verification.valid, deserialized.valid);
924 assert_eq!(verification.entries_verified, deserialized.entries_verified);
925 }
926
927 #[test]
928 fn test_hash_string() {
929 let hash1 = hash_string("hello");
930 let hash2 = hash_string("hello");
931 let hash3 = hash_string("world");
932
933 assert_eq!(hash1, hash2);
934 assert_ne!(hash1, hash3);
935 }
936}
937
938#[cfg(test)]
939mod proptests {
940 use super::*;
941 use crate::oracle::Phase;
942 use proptest::prelude::*;
943
944 proptest! {
945 #![proptest_config(ProptestConfig::with_cases(100))]
946
947 #[test]
948 fn prop_hash_chain_always_valid(n in 1usize..20) {
949 let mut collector = AuditCollector::new("prop-test");
950
951 for i in 0..n {
952 collector.record(
953 VerificationPath::new(format!("t{}", i))
954 .with_verdict(Verdict::Pass)
955 );
956 }
957
958 let verification = collector.verify_chain();
959 prop_assert!(verification.valid);
960 prop_assert_eq!(verification.entries_verified, n);
961 }
962
963 #[test]
964 fn prop_sequence_numbers_monotonic(n in 2usize..20) {
965 let mut collector = AuditCollector::new("test");
966
967 for i in 0..n {
968 collector.record(VerificationPath::new(format!("t{}", i)));
969 }
970
971 let entries = collector.entries();
972 for i in 1..entries.len() {
973 prop_assert!(entries[i].sequence > entries[i-1].sequence);
974 }
975 }
976
977 #[test]
978 fn prop_stats_consistent(
979 passed in 0usize..10,
980 failed in 0usize..10
981 ) {
982 let mut collector = AuditCollector::new("test");
983
984 for i in 0..passed {
985 collector.record(
986 VerificationPath::new(format!("pass{}", i))
987 .with_verdict(Verdict::Pass)
988 );
989 }
990
991 for i in 0..failed {
992 collector.record(
993 VerificationPath::new(format!("fail{}", i))
994 .with_verdict(Verdict::OutputMismatch {
995 expected: "a".into(),
996 actual: "b".into(),
997 })
998 );
999 }
1000
1001 let stats = collector.stats();
1002 prop_assert_eq!(stats.total, passed + failed);
1003 prop_assert_eq!(stats.passed, passed);
1004 prop_assert_eq!(stats.failed, failed);
1005 }
1006
1007 #[test]
1008 fn prop_to_bytes_deterministic(test_id in "[a-z]{1,20}") {
1009 let path1 = VerificationPath::new(&test_id);
1010 let path2 = VerificationPath::new(&test_id);
1011
1012 let bytes1 = path1.to_bytes();
1013 let bytes2 = path2.to_bytes();
1014
1015 prop_assert_eq!(bytes1, bytes2);
1016 }
1017
1018 #[test]
1019 fn prop_confidence_bounded(verdict_type in 0u8..4) {
1020 let verdict = match verdict_type {
1021 0 => Verdict::Pass,
1022 1 => Verdict::OutputMismatch { expected: "a".into(), actual: "b".into() },
1023 2 => Verdict::Timeout { phase: Phase::Source, limit_ms: 5000 },
1024 _ => Verdict::RuntimeError { phase: Phase::Target, error: "err".into() },
1025 };
1026
1027 let path = VerificationPath::new("test").with_verdict(verdict);
1028 let confidence = path.confidence();
1029
1030 prop_assert!(confidence >= 0.0);
1031 prop_assert!(confidence <= 1.0);
1032 }
1033 }
1034}