Skip to main content

verificar/
audit.rs

1//! Verification Audit Trail
2//!
3//! Provides tamper-evident audit logging for test verification decisions.
4//!
5//! # Features
6//!
7//! - **Decision Path Tracking**: Record verification decisions with full context
8//! - **Hash Chain Provenance**: Tamper-evident audit trails for compliance
9//! - **Verdict Analysis**: Track pass/fail patterns and flaky tests
10//!
11//! # Toyota Way: 失敗を隠さない (Shippai wo kakusanai)
12//! Never hide failures - every verification decision is auditable.
13//!
14//! # Example
15//!
16//! ```rust,ignore
17//! use verificar::audit::{AuditCollector, VerificationPath};
18//! use verificar::oracle::Verdict;
19//!
20//! let mut collector = AuditCollector::new("test-suite-001");
21//!
22//! let path = VerificationPath::new("test_addition")
23//!     .with_verdict(Verdict::Pass)
24//!     .with_duration(std::time::Duration::from_millis(50));
25//!
26//! collector.record(path);
27//!
28//! assert!(collector.verify_chain().valid);
29//! ```
30
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::time::{Duration, Instant};
34
35use crate::oracle::{ExecutionResult, Verdict, VerificationResult};
36use crate::Language;
37
38// =============================================================================
39// Verification Decision Path
40// =============================================================================
41
42/// Decision path for a verification execution.
43///
44/// Captures all relevant information for auditing a test verification decision.
45#[derive(Clone, Debug, Serialize, Deserialize)]
46pub struct VerificationPath {
47    /// Test case identifier
48    pub test_id: String,
49
50    /// Source language
51    pub source_language: Option<Language>,
52
53    /// Target language
54    pub target_language: Option<Language>,
55
56    /// Verification verdict
57    pub verdict: Option<Verdict>,
58
59    /// Execution duration in nanoseconds
60    pub duration_ns: u64,
61
62    /// Source execution result summary
63    pub source_result: Option<ExecutionSummary>,
64
65    /// Target execution result summary
66    pub target_result: Option<ExecutionSummary>,
67
68    /// Feature contributions (for ML-enhanced verification)
69    contributions: Vec<f32>,
70
71    /// Confidence score for this decision
72    confidence: f32,
73
74    /// Additional metadata
75    pub metadata: HashMap<String, serde_json::Value>,
76}
77
78/// Summary of an execution result for audit purposes.
79#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct ExecutionSummary {
81    /// Exit code
82    pub exit_code: i32,
83
84    /// Output length in bytes
85    pub stdout_len: usize,
86
87    /// Error output length in bytes
88    pub stderr_len: usize,
89
90    /// Execution duration in milliseconds
91    pub duration_ms: u64,
92
93    /// Hash of stdout content (for integrity)
94    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    /// Create a new verification path for a test case.
111    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    /// Set the verification verdict.
127    #[must_use]
128    pub fn with_verdict(mut self, verdict: Verdict) -> Self {
129        // Adjust confidence based on verdict
130        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    /// Set execution duration.
141    #[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    /// Set source and target languages.
148    #[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    /// Set source execution result.
156    #[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    /// Set target execution result.
163    #[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    /// Add metadata entry.
170    #[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    /// Set feature contributions (for ML analysis).
177    #[must_use]
178    pub fn with_contributions(mut self, contributions: Vec<f32>) -> Self {
179        self.contributions = contributions;
180        self
181    }
182
183    /// Get feature contributions.
184    pub fn feature_contributions(&self) -> &[f32] {
185        &self.contributions
186    }
187
188    /// Get confidence score.
189    pub fn confidence(&self) -> f32 {
190        self.confidence
191    }
192
193    /// Check if this verification passed.
194    pub fn passed(&self) -> bool {
195        matches!(self.verdict, Some(Verdict::Pass))
196    }
197
198    /// Serialize to bytes for hashing.
199    pub fn to_bytes(&self) -> Vec<u8> {
200        let mut bytes = Vec::new();
201
202        // Test ID
203        bytes.extend_from_slice(self.test_id.as_bytes());
204        bytes.push(0);
205
206        // Duration
207        bytes.extend_from_slice(&self.duration_ns.to_le_bytes());
208
209        // Confidence
210        bytes.extend_from_slice(&self.confidence.to_le_bytes());
211
212        // Verdict indicator
213        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    /// Generate a text explanation of the verification.
226    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// =============================================================================
275// Audit Trace Entry
276// =============================================================================
277
278/// A single audit trace entry.
279#[derive(Clone, Debug, Serialize, Deserialize)]
280pub struct AuditTrace {
281    /// Sequence number
282    pub sequence: u64,
283
284    /// Timestamp in nanoseconds since epoch
285    pub timestamp_ns: u64,
286
287    /// The verification path
288    pub path: VerificationPath,
289}
290
291/// Hash chain entry for tamper-evident audit.
292#[derive(Clone, Debug, Serialize, Deserialize)]
293pub struct HashChainEntry {
294    /// Sequence number
295    pub sequence: u64,
296
297    /// SHA-256 hash of previous entry (zeros for genesis)
298    pub prev_hash: [u8; 32],
299
300    /// Hash of this entry
301    pub hash: [u8; 32],
302
303    /// The audit trace
304    pub trace: AuditTrace,
305}
306
307// =============================================================================
308// Audit Collector
309// =============================================================================
310
311/// Collector for verification audit trails.
312#[derive(Debug)]
313pub struct AuditCollector {
314    /// Hash chain entries
315    entries: Vec<HashChainEntry>,
316
317    /// Next sequence number
318    next_sequence: u64,
319
320    /// Suite identifier
321    suite_id: String,
322
323    /// Statistics
324    stats: AuditStats,
325}
326
327/// Statistics for audit trail.
328#[derive(Clone, Debug, Default, Serialize, Deserialize)]
329pub struct AuditStats {
330    /// Total verifications
331    pub total: usize,
332
333    /// Passed verifications
334    pub passed: usize,
335
336    /// Failed verifications
337    pub failed: usize,
338
339    /// Timeout count
340    pub timeouts: usize,
341
342    /// Runtime errors
343    pub errors: usize,
344
345    /// Total duration in nanoseconds
346    pub total_duration_ns: u64,
347}
348
349impl AuditCollector {
350    /// Create a new audit collector for a test suite.
351    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    /// Get the suite identifier.
361    pub fn suite_id(&self) -> &str {
362        &self.suite_id
363    }
364
365    /// Record a verification decision.
366    pub fn record(&mut self, path: VerificationPath) -> &HashChainEntry {
367        // Update statistics
368        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        // Get previous hash
391        let prev_hash = self.entries.last().map_or([0u8; 32], |e| e.hash);
392
393        // Compute hash
394        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        // SAFETY: We just pushed an entry, so the vector is guaranteed to be non-empty.
407        // Using index access to avoid expect() which is denied by clippy configuration.
408        &self.entries[self.entries.len() - 1]
409    }
410
411    /// Compute hash for an entry.
412    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    /// Get all entries.
435    pub fn entries(&self) -> &[HashChainEntry] {
436        &self.entries
437    }
438
439    /// Get entry count.
440    pub fn len(&self) -> usize {
441        self.entries.len()
442    }
443
444    /// Check if empty.
445    pub fn is_empty(&self) -> bool {
446        self.entries.is_empty()
447    }
448
449    /// Get statistics.
450    pub fn stats(&self) -> &AuditStats {
451        &self.stats
452    }
453
454    /// Verify hash chain integrity.
455    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            // Verify prev_hash linkage
461            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            // Verify entry hash
481            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    /// Get recent entries.
501    pub fn recent(&self, n: usize) -> Vec<&HashChainEntry> {
502        self.entries.iter().rev().take(n).collect()
503    }
504
505    /// Get failed verifications.
506    pub fn failures(&self) -> Vec<&HashChainEntry> {
507        self.entries
508            .iter()
509            .filter(|e| !e.trace.path.passed())
510            .collect()
511    }
512
513    /// Export to JSON.
514    ///
515    /// # Errors
516    ///
517    /// Returns an error if JSON serialization fails.
518    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/// Result of hash chain verification.
543#[derive(Clone, Debug, Serialize, Deserialize)]
544pub struct ChainVerification {
545    /// Whether the chain is valid
546    pub valid: bool,
547
548    /// Number of entries verified
549    pub entries_verified: usize,
550
551    /// Index of first broken link (if any)
552    pub first_break: Option<usize>,
553}
554
555// =============================================================================
556// Verification Timer
557// =============================================================================
558
559/// Timer for measuring verification duration.
560#[derive(Debug)]
561pub struct VerificationTimer {
562    start: Instant,
563    test_id: String,
564}
565
566impl VerificationTimer {
567    /// Start timing a verification.
568    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    /// Stop timing and create a verification path.
576    pub fn stop(self) -> VerificationPath {
577        let duration = self.start.elapsed();
578        VerificationPath::new(self.test_id).with_duration(duration)
579    }
580
581    /// Stop timing with a verdict.
582    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
590// =============================================================================
591// Helper Functions
592// =============================================================================
593
594/// Simple string hash for integrity checking.
595fn 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/// Create a new audit collector with generated suite ID.
605#[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// =============================================================================
618// Tests
619// =============================================================================
620
621#[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}