git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
666 lines
20 KiB
Rust
666 lines
20 KiB
Rust
//! Comprehensive tests for deterministic replay
|
|
//!
|
|
//! Tests cover:
|
|
//! - Replay engine creation and configuration
|
|
//! - Checkpoint management
|
|
//! - Decision replay and verification
|
|
//! - Security tests (ensuring determinism)
|
|
|
|
use cognitum_gate_tilezero::replay::{
|
|
ReplayDifference, ReplayEngine, ReplayError, ReplayResult, SequenceVerification,
|
|
StateSnapshot, TileSnapshot,
|
|
};
|
|
use cognitum_gate_tilezero::receipt::{
|
|
EvidentialWitness, PredictiveWitness, StructuralWitness, TimestampProof, WitnessReceipt,
|
|
WitnessSummary,
|
|
};
|
|
use cognitum_gate_tilezero::permit::PermitToken;
|
|
use cognitum_gate_tilezero::GateDecision;
|
|
use std::collections::HashMap;
|
|
|
|
fn create_test_receipt(
|
|
sequence: u64,
|
|
decision: GateDecision,
|
|
witness: WitnessSummary,
|
|
) -> WitnessReceipt {
|
|
WitnessReceipt {
|
|
sequence,
|
|
token: PermitToken {
|
|
decision,
|
|
action_id: format!("action-{}", sequence),
|
|
timestamp: 1000000000 + sequence * 1000,
|
|
ttl_ns: 60_000_000_000,
|
|
witness_hash: [0u8; 32],
|
|
sequence,
|
|
signature: [0u8; 64],
|
|
},
|
|
previous_hash: [0u8; 32],
|
|
witness_summary: witness,
|
|
timestamp_proof: TimestampProof {
|
|
timestamp: 1000000000 + sequence * 1000,
|
|
previous_receipt_hash: [0u8; 32],
|
|
merkle_root: [0u8; 32],
|
|
},
|
|
}
|
|
}
|
|
|
|
fn create_permit_witness() -> WitnessSummary {
|
|
WitnessSummary {
|
|
structural: StructuralWitness {
|
|
cut_value: 10.0,
|
|
partition: "stable".to_string(),
|
|
critical_edges: 2,
|
|
boundary: vec![],
|
|
},
|
|
predictive: PredictiveWitness {
|
|
set_size: 5,
|
|
coverage: 0.9,
|
|
},
|
|
evidential: EvidentialWitness {
|
|
e_value: 150.0,
|
|
verdict: "accept".to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn create_defer_witness() -> WitnessSummary {
|
|
WitnessSummary {
|
|
structural: StructuralWitness {
|
|
cut_value: 10.0,
|
|
partition: "stable".to_string(),
|
|
critical_edges: 5,
|
|
boundary: vec![],
|
|
},
|
|
predictive: PredictiveWitness {
|
|
set_size: 25, // Large set size -> defer
|
|
coverage: 0.9,
|
|
},
|
|
evidential: EvidentialWitness {
|
|
e_value: 50.0,
|
|
verdict: "continue".to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn create_deny_witness() -> WitnessSummary {
|
|
WitnessSummary {
|
|
structural: StructuralWitness {
|
|
cut_value: 2.0,
|
|
partition: "fragile".to_string(), // Fragile -> deny
|
|
critical_edges: 10,
|
|
boundary: vec![],
|
|
},
|
|
predictive: PredictiveWitness {
|
|
set_size: 5,
|
|
coverage: 0.9,
|
|
},
|
|
evidential: EvidentialWitness {
|
|
e_value: 0.001,
|
|
verdict: "reject".to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod engine_creation {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_default_engine() {
|
|
let engine = ReplayEngine::default();
|
|
assert_eq!(engine.checkpoint_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_engine_with_interval() {
|
|
let engine = ReplayEngine::new(50);
|
|
assert_eq!(engine.checkpoint_count(), 0);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod checkpoint_management {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_save_checkpoint() {
|
|
let mut engine = ReplayEngine::new(10);
|
|
|
|
let snapshot = StateSnapshot {
|
|
sequence: 0,
|
|
timestamp: 1000,
|
|
global_min_cut: 10.0,
|
|
aggregate_e_value: 100.0,
|
|
min_coherence: 256,
|
|
tile_states: HashMap::new(),
|
|
};
|
|
|
|
engine.save_checkpoint(0, snapshot);
|
|
assert_eq!(engine.checkpoint_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_checkpoint_at_interval() {
|
|
let mut engine = ReplayEngine::new(10);
|
|
|
|
// Checkpoint at 0, 10, 20 should be saved
|
|
for seq in [0, 5, 10, 15, 20] {
|
|
let snapshot = StateSnapshot {
|
|
sequence: seq,
|
|
timestamp: 1000 + seq,
|
|
global_min_cut: 10.0,
|
|
aggregate_e_value: 100.0,
|
|
min_coherence: 256,
|
|
tile_states: HashMap::new(),
|
|
};
|
|
engine.save_checkpoint(seq, snapshot);
|
|
}
|
|
|
|
// Only 0, 10, 20 should be saved (multiples of 10)
|
|
assert_eq!(engine.checkpoint_count(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_nearest_checkpoint() {
|
|
let mut engine = ReplayEngine::new(10);
|
|
|
|
for seq in [0, 10, 20] {
|
|
let snapshot = StateSnapshot {
|
|
sequence: seq,
|
|
timestamp: 1000 + seq,
|
|
global_min_cut: seq as f64,
|
|
aggregate_e_value: 100.0,
|
|
min_coherence: 256,
|
|
tile_states: HashMap::new(),
|
|
};
|
|
engine.save_checkpoint(seq, snapshot);
|
|
}
|
|
|
|
// Find nearest for 15 -> should be 10
|
|
let (found_seq, snapshot) = engine.find_nearest_checkpoint(15).unwrap();
|
|
assert_eq!(found_seq, 10);
|
|
assert_eq!(snapshot.global_min_cut, 10.0);
|
|
|
|
// Find nearest for 25 -> should be 20
|
|
let (found_seq, _) = engine.find_nearest_checkpoint(25).unwrap();
|
|
assert_eq!(found_seq, 20);
|
|
|
|
// Find nearest for 5 -> should be 0
|
|
let (found_seq, _) = engine.find_nearest_checkpoint(5).unwrap();
|
|
assert_eq!(found_seq, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_checkpoint_found() {
|
|
let engine = ReplayEngine::new(10);
|
|
assert!(engine.find_nearest_checkpoint(5).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_prune_checkpoints() {
|
|
let mut engine = ReplayEngine::new(10);
|
|
|
|
for seq in [0, 10, 20, 30, 40, 50] {
|
|
let snapshot = StateSnapshot {
|
|
sequence: seq,
|
|
timestamp: 1000 + seq,
|
|
global_min_cut: 10.0,
|
|
aggregate_e_value: 100.0,
|
|
min_coherence: 256,
|
|
tile_states: HashMap::new(),
|
|
};
|
|
engine.save_checkpoint(seq, snapshot);
|
|
}
|
|
|
|
assert_eq!(engine.checkpoint_count(), 6);
|
|
|
|
engine.prune_before(30);
|
|
|
|
assert_eq!(engine.checkpoint_count(), 3); // 30, 40, 50 remain
|
|
assert!(engine.find_nearest_checkpoint(20).is_none());
|
|
assert!(engine.find_nearest_checkpoint(30).is_some());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod decision_replay {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_replay_permit() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipt = create_test_receipt(0, GateDecision::Permit, create_permit_witness());
|
|
|
|
let result = engine.replay(&receipt);
|
|
|
|
assert!(result.matched);
|
|
assert_eq!(result.decision, GateDecision::Permit);
|
|
assert_eq!(result.original_decision, GateDecision::Permit);
|
|
assert!(result.differences.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_replay_defer() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipt = create_test_receipt(0, GateDecision::Defer, create_defer_witness());
|
|
|
|
let result = engine.replay(&receipt);
|
|
|
|
assert!(result.matched);
|
|
assert_eq!(result.decision, GateDecision::Defer);
|
|
}
|
|
|
|
#[test]
|
|
fn test_replay_deny() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipt = create_test_receipt(0, GateDecision::Deny, create_deny_witness());
|
|
|
|
let result = engine.replay(&receipt);
|
|
|
|
assert!(result.matched);
|
|
assert_eq!(result.decision, GateDecision::Deny);
|
|
}
|
|
|
|
#[test]
|
|
fn test_replay_mismatch() {
|
|
let engine = ReplayEngine::new(100);
|
|
|
|
// Create a receipt where the decision doesn't match the witness
|
|
// Witness indicates DENY (fragile partition), but token says PERMIT
|
|
let receipt = create_test_receipt(0, GateDecision::Permit, create_deny_witness());
|
|
|
|
let result = engine.replay(&receipt);
|
|
|
|
assert!(!result.matched);
|
|
assert_eq!(result.decision, GateDecision::Deny); // Reconstructed from witness
|
|
assert_eq!(result.original_decision, GateDecision::Permit); // From token
|
|
assert!(!result.differences.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_replay_preserves_snapshot() {
|
|
let engine = ReplayEngine::new(100);
|
|
let witness = create_permit_witness();
|
|
let receipt = create_test_receipt(0, GateDecision::Permit, witness.clone());
|
|
|
|
let result = engine.replay(&receipt);
|
|
|
|
assert_eq!(result.state_snapshot.structural.cut_value, witness.structural.cut_value);
|
|
assert_eq!(result.state_snapshot.evidential.e_value, witness.evidential.e_value);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod sequence_verification {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_verify_empty_sequence() {
|
|
let engine = ReplayEngine::new(100);
|
|
let verification = engine.verify_sequence(&[]);
|
|
|
|
assert_eq!(verification.total_receipts, 0);
|
|
assert!(verification.all_matched);
|
|
assert_eq!(verification.mismatch_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verify_single_receipt() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipts = vec![create_test_receipt(0, GateDecision::Permit, create_permit_witness())];
|
|
|
|
let verification = engine.verify_sequence(&receipts);
|
|
|
|
assert_eq!(verification.total_receipts, 1);
|
|
assert!(verification.all_matched);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verify_multiple_receipts() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipts = vec![
|
|
create_test_receipt(0, GateDecision::Permit, create_permit_witness()),
|
|
create_test_receipt(1, GateDecision::Defer, create_defer_witness()),
|
|
create_test_receipt(2, GateDecision::Deny, create_deny_witness()),
|
|
];
|
|
|
|
let verification = engine.verify_sequence(&receipts);
|
|
|
|
assert_eq!(verification.total_receipts, 3);
|
|
assert!(verification.all_matched);
|
|
assert_eq!(verification.mismatch_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verify_with_mismatches() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipts = vec![
|
|
create_test_receipt(0, GateDecision::Permit, create_permit_witness()),
|
|
create_test_receipt(1, GateDecision::Permit, create_deny_witness()), // Mismatch!
|
|
create_test_receipt(2, GateDecision::Deny, create_deny_witness()),
|
|
];
|
|
|
|
let verification = engine.verify_sequence(&receipts);
|
|
|
|
assert_eq!(verification.total_receipts, 3);
|
|
assert!(!verification.all_matched);
|
|
assert_eq!(verification.mismatch_count(), 1);
|
|
|
|
let mismatches: Vec<_> = verification.mismatches().collect();
|
|
assert_eq!(mismatches.len(), 1);
|
|
assert_eq!(mismatches[0].0, 1); // Sequence 1 mismatched
|
|
}
|
|
|
|
#[test]
|
|
fn test_mismatches_iterator() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipts = vec![
|
|
create_test_receipt(0, GateDecision::Permit, create_deny_witness()), // Mismatch
|
|
create_test_receipt(1, GateDecision::Permit, create_permit_witness()),
|
|
create_test_receipt(2, GateDecision::Defer, create_deny_witness()), // Mismatch
|
|
];
|
|
|
|
let verification = engine.verify_sequence(&receipts);
|
|
let mismatches: Vec<_> = verification.mismatches().collect();
|
|
|
|
assert_eq!(mismatches.len(), 2);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod checkpoint_export_import {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_export_checkpoint() {
|
|
let mut engine = ReplayEngine::new(10);
|
|
|
|
let snapshot = StateSnapshot {
|
|
sequence: 0,
|
|
timestamp: 1000,
|
|
global_min_cut: 15.0,
|
|
aggregate_e_value: 200.0,
|
|
min_coherence: 512,
|
|
tile_states: HashMap::new(),
|
|
};
|
|
|
|
engine.save_checkpoint(0, snapshot);
|
|
|
|
let exported = engine.export_checkpoint(0);
|
|
assert!(exported.is_some());
|
|
|
|
let data = exported.unwrap();
|
|
assert!(!data.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_export_nonexistent() {
|
|
let engine = ReplayEngine::new(10);
|
|
assert!(engine.export_checkpoint(0).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_import_checkpoint() {
|
|
let mut engine1 = ReplayEngine::new(10);
|
|
|
|
let snapshot = StateSnapshot {
|
|
sequence: 0,
|
|
timestamp: 1000,
|
|
global_min_cut: 25.0,
|
|
aggregate_e_value: 300.0,
|
|
min_coherence: 768,
|
|
tile_states: HashMap::new(),
|
|
};
|
|
|
|
engine1.save_checkpoint(0, snapshot);
|
|
let exported = engine1.export_checkpoint(0).unwrap();
|
|
|
|
let mut engine2 = ReplayEngine::new(10);
|
|
assert!(engine2.import_checkpoint(0, &exported).is_ok());
|
|
assert_eq!(engine2.checkpoint_count(), 1);
|
|
|
|
let (_, imported) = engine2.find_nearest_checkpoint(0).unwrap();
|
|
assert_eq!(imported.global_min_cut, 25.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_import_invalid_data() {
|
|
let mut engine = ReplayEngine::new(10);
|
|
let result = engine.import_checkpoint(0, b"invalid json");
|
|
assert!(matches!(result, Err(ReplayError::InvalidCheckpoint)));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tile_snapshot {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_tile_snapshot_in_state() {
|
|
let mut tile_states = HashMap::new();
|
|
tile_states.insert(
|
|
1,
|
|
TileSnapshot {
|
|
tile_id: 1,
|
|
coherence: 256,
|
|
e_value: 10.0,
|
|
boundary_edges: 5,
|
|
},
|
|
);
|
|
tile_states.insert(
|
|
2,
|
|
TileSnapshot {
|
|
tile_id: 2,
|
|
coherence: 512,
|
|
e_value: 20.0,
|
|
boundary_edges: 3,
|
|
},
|
|
);
|
|
|
|
let snapshot = StateSnapshot {
|
|
sequence: 0,
|
|
timestamp: 1000,
|
|
global_min_cut: 10.0,
|
|
aggregate_e_value: 100.0,
|
|
min_coherence: 256,
|
|
tile_states,
|
|
};
|
|
|
|
assert_eq!(snapshot.tile_states.len(), 2);
|
|
assert_eq!(snapshot.tile_states.get(&1).unwrap().coherence, 256);
|
|
assert_eq!(snapshot.tile_states.get(&2).unwrap().e_value, 20.0);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod replay_difference {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_difference_structure() {
|
|
let diff = ReplayDifference {
|
|
field: "decision".to_string(),
|
|
original: "permit".to_string(),
|
|
replayed: "deny".to_string(),
|
|
};
|
|
|
|
assert_eq!(diff.field, "decision");
|
|
assert_eq!(diff.original, "permit");
|
|
assert_eq!(diff.replayed, "deny");
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod determinism {
|
|
use super::*;
|
|
|
|
/// Test that replaying the same receipt always produces the same result
|
|
#[test]
|
|
fn test_replay_deterministic() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipt = create_test_receipt(0, GateDecision::Permit, create_permit_witness());
|
|
|
|
let result1 = engine.replay(&receipt);
|
|
let result2 = engine.replay(&receipt);
|
|
|
|
assert_eq!(result1.decision, result2.decision);
|
|
assert_eq!(result1.matched, result2.matched);
|
|
assert_eq!(result1.differences.len(), result2.differences.len());
|
|
}
|
|
|
|
/// Test that different engines produce same results
|
|
#[test]
|
|
fn test_cross_engine_determinism() {
|
|
let engine1 = ReplayEngine::new(100);
|
|
let engine2 = ReplayEngine::new(50); // Different checkpoint interval
|
|
|
|
let receipt = create_test_receipt(0, GateDecision::Defer, create_defer_witness());
|
|
|
|
let result1 = engine1.replay(&receipt);
|
|
let result2 = engine2.replay(&receipt);
|
|
|
|
assert_eq!(result1.decision, result2.decision);
|
|
assert_eq!(result1.matched, result2.matched);
|
|
}
|
|
|
|
/// Test sequence verification is deterministic
|
|
#[test]
|
|
fn test_sequence_verification_deterministic() {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipts = vec![
|
|
create_test_receipt(0, GateDecision::Permit, create_permit_witness()),
|
|
create_test_receipt(1, GateDecision::Deny, create_deny_witness()),
|
|
];
|
|
|
|
let v1 = engine.verify_sequence(&receipts);
|
|
let v2 = engine.verify_sequence(&receipts);
|
|
|
|
assert_eq!(v1.total_receipts, v2.total_receipts);
|
|
assert_eq!(v1.all_matched, v2.all_matched);
|
|
assert_eq!(v1.mismatch_count(), v2.mismatch_count());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod security_tests {
|
|
use super::*;
|
|
|
|
/// Test that modified witness produces different replay result
|
|
#[test]
|
|
fn test_witness_tampering_detected() {
|
|
let engine = ReplayEngine::new(100);
|
|
|
|
let original = create_test_receipt(0, GateDecision::Permit, create_permit_witness());
|
|
let original_result = engine.replay(&original);
|
|
|
|
// Create tampered receipt with modified witness
|
|
let mut tampered_witness = create_permit_witness();
|
|
tampered_witness.structural.partition = "fragile".to_string();
|
|
let tampered = create_test_receipt(0, GateDecision::Permit, tampered_witness);
|
|
let tampered_result = engine.replay(&tampered);
|
|
|
|
// Tampered one should fail replay
|
|
assert!(original_result.matched);
|
|
assert!(!tampered_result.matched);
|
|
}
|
|
|
|
/// Test audit trail completeness
|
|
#[test]
|
|
fn test_audit_trail() {
|
|
let engine = ReplayEngine::new(100);
|
|
let mut receipts = Vec::new();
|
|
|
|
// Build a sequence of decisions
|
|
for i in 0..10 {
|
|
let witness = if i % 3 == 0 {
|
|
create_permit_witness()
|
|
} else if i % 3 == 1 {
|
|
create_defer_witness()
|
|
} else {
|
|
create_deny_witness()
|
|
};
|
|
|
|
let decision = if i % 3 == 0 {
|
|
GateDecision::Permit
|
|
} else if i % 3 == 1 {
|
|
GateDecision::Defer
|
|
} else {
|
|
GateDecision::Deny
|
|
};
|
|
|
|
receipts.push(create_test_receipt(i, decision, witness));
|
|
}
|
|
|
|
let verification = engine.verify_sequence(&receipts);
|
|
|
|
// All should match since we built them consistently
|
|
assert!(verification.all_matched);
|
|
assert_eq!(verification.total_receipts, 10);
|
|
}
|
|
}
|
|
|
|
// Property-based tests
|
|
#[cfg(test)]
|
|
mod property_tests {
|
|
use super::*;
|
|
use proptest::prelude::*;
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn prop_replay_always_produces_result(sequence in 0u64..1000) {
|
|
let engine = ReplayEngine::new(100);
|
|
let receipt = create_test_receipt(
|
|
sequence,
|
|
GateDecision::Permit,
|
|
create_permit_witness()
|
|
);
|
|
|
|
let result = engine.replay(&receipt);
|
|
// Should always produce a valid result
|
|
assert!(result.decision == GateDecision::Permit ||
|
|
result.decision == GateDecision::Defer ||
|
|
result.decision == GateDecision::Deny);
|
|
}
|
|
|
|
#[test]
|
|
fn prop_checkpoint_interval_works(interval in 1u64..100) {
|
|
let mut engine = ReplayEngine::new(interval);
|
|
|
|
for seq in 0..interval * 3 {
|
|
let snapshot = StateSnapshot {
|
|
sequence: seq,
|
|
timestamp: 1000 + seq,
|
|
global_min_cut: 10.0,
|
|
aggregate_e_value: 100.0,
|
|
min_coherence: 256,
|
|
tile_states: HashMap::new(),
|
|
};
|
|
engine.save_checkpoint(seq, snapshot);
|
|
}
|
|
|
|
// Should have saved at least 3 checkpoints
|
|
assert!(engine.checkpoint_count() >= 3);
|
|
}
|
|
|
|
#[test]
|
|
fn prop_matching_decisions_have_empty_differences(seq in 0u64..100) {
|
|
let engine = ReplayEngine::new(100);
|
|
|
|
// Create receipts where decision matches witness
|
|
let receipts = vec![
|
|
(GateDecision::Permit, create_permit_witness()),
|
|
(GateDecision::Defer, create_defer_witness()),
|
|
(GateDecision::Deny, create_deny_witness()),
|
|
];
|
|
|
|
for (decision, witness) in receipts {
|
|
let receipt = create_test_receipt(seq, decision, witness);
|
|
let result = engine.replay(&receipt);
|
|
if result.matched {
|
|
assert!(result.differences.is_empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|