use proptest::prelude::*;
use crate::agent::{CausalContext, PrincipalId, PrincipalKind, Session, SessionId};
use crate::block::{Block, BlockBuilder, SealerId};
use crate::crypto::{hash, Hash, PublicKey, SecretKey, Sig};
use crate::event::{
ActorId, ActorKind, AuditEvent, EventId, EventType, Outcome, ResourceId, ResourceKind,
ReviewVerdict,
};
fn arb_bytes32() -> impl Strategy<Value = [u8; 32]> {
prop::array::uniform32(any::<u8>())
}
#[allow(dead_code)]
fn arb_bytes64() -> impl Strategy<Value = [u8; 64]> {
prop::array::uniform32(any::<u8>()).prop_flat_map(|first| {
prop::array::uniform32(any::<u8>()).prop_map(move |second| {
let mut arr = [0u8; 64];
arr[..32].copy_from_slice(&first);
arr[32..].copy_from_slice(&second);
arr
})
})
}
#[allow(dead_code)]
fn arb_hash() -> impl Strategy<Value = Hash> {
arb_bytes32().prop_map(Hash::from_bytes)
}
fn arb_secret_key() -> impl Strategy<Value = SecretKey> {
Just(()).prop_map(|_| SecretKey::generate())
}
fn arb_actor_kind() -> impl Strategy<Value = ActorKind> {
prop_oneof![
Just(ActorKind::User),
Just(ActorKind::System),
Just(ActorKind::Agent),
Just(ActorKind::Integration),
]
}
fn arb_resource_kind() -> impl Strategy<Value = ResourceKind> {
prop_oneof![
Just(ResourceKind::Repository),
Just(ResourceKind::Commit),
Just(ResourceKind::Branch),
Just(ResourceKind::Tag),
Just(ResourceKind::PullRequest),
Just(ResourceKind::Issue),
Just(ResourceKind::File),
Just(ResourceKind::User),
Just(ResourceKind::Organization),
Just(ResourceKind::Credential),
Just(ResourceKind::Config),
Just(ResourceKind::Document),
Just(ResourceKind::Other),
]
}
fn arb_review_verdict() -> impl Strategy<Value = ReviewVerdict> {
prop_oneof![
Just(ReviewVerdict::Approved),
Just(ReviewVerdict::ChangesRequested),
Just(ReviewVerdict::Commented),
]
}
fn arb_event_type() -> impl Strategy<Value = EventType> {
prop_oneof![
Just(EventType::RepoCreated),
Just(EventType::RepoDeleted),
Just(EventType::RepoTransferred),
Just(EventType::RepoVisibilityChanged),
(any::<bool>(), 0u32..1000u32)
.prop_map(|(force, commits)| EventType::Push { force, commits }),
Just(EventType::BranchCreated),
Just(EventType::BranchDeleted),
Just(EventType::BranchProtectionChanged),
Just(EventType::TagCreated),
Just(EventType::TagDeleted),
Just(EventType::PullRequestOpened),
Just(EventType::PullRequestMerged),
Just(EventType::PullRequestClosed),
arb_review_verdict().prop_map(|verdict| EventType::ReviewSubmitted { verdict }),
Just(EventType::IssueOpened),
Just(EventType::IssueClosed),
"[a-z]{3,10}".prop_map(|permission| EventType::AccessGranted { permission }),
Just(EventType::AccessRevoked),
"[a-z]{3,10}".prop_map(|method| EventType::Login { method }),
Just(EventType::Logout),
"[a-z]{5,20}".prop_map(|reason| EventType::LoginFailed { reason }),
Just(EventType::MfaConfigured),
("[a-z]{3,15}", prop::option::of("[a-z ]{10,50}"))
.prop_map(|(action, reasoning)| EventType::AgentAction { action, reasoning }),
prop::collection::vec("[a-z]{3,10}", 1..5)
.prop_map(|scope| EventType::AgentAuthorized { scope }),
Just(EventType::AgentRevoked),
Just(EventType::DataExportRequested),
Just(EventType::DataExportCompleted),
Just(EventType::DataDeletionRequested),
Just(EventType::DataDeletionCompleted),
"[a-z]{5,15}".prop_map(|purpose| EventType::ConsentGiven { purpose }),
"[a-z]{5,15}".prop_map(|purpose| EventType::ConsentRevoked { purpose }),
"[a-z._]{3,20}".prop_map(|key| EventType::ConfigChanged { key }),
"[0-9]+\\.[0-9]+\\.[0-9]+".prop_map(|version| EventType::ReleasePublished { version }),
Just(EventType::BackupCreated),
(0u32..100u32).prop_map(|findings| EventType::SecurityScan { findings }),
"[a-z_]{5,20}".prop_map(|name| EventType::Custom { name }),
]
}
fn arb_outcome() -> impl Strategy<Value = Outcome> {
prop_oneof![
Just(Outcome::Success),
"[a-z ]{5,30}".prop_map(|reason| Outcome::Failure { reason }),
"[a-z ]{5,30}".prop_map(|reason| Outcome::Denied { reason }),
Just(Outcome::Pending),
]
}
fn arb_resource_id() -> impl Strategy<Value = ResourceId> {
(arb_resource_kind(), "[a-z0-9-]{3,20}").prop_map(|(kind, id)| ResourceId::new(kind, id))
}
fn arb_actor_id() -> impl Strategy<Value = (SecretKey, ActorId)> {
(
arb_secret_key(),
arb_actor_kind(),
prop::option::of("[a-z]{3,15}"),
)
.prop_map(|(key, kind, name)| {
let actor = ActorId::new(key.public_key(), kind);
let actor = match name {
Some(n) => actor.with_name(n),
None => actor,
};
(key, actor)
})
}
fn arb_audit_event() -> impl Strategy<Value = AuditEvent> {
(
arb_actor_id(),
arb_event_type(),
arb_resource_id(),
arb_outcome(),
prop::collection::vec(any::<u8>(), 0..100),
)
.prop_map(|((key, actor), event_type, resource, outcome, metadata)| {
AuditEvent::builder()
.now()
.event_type(event_type)
.actor(actor)
.resource(resource)
.outcome(outcome)
.metadata_bytes(metadata)
.sign(&key)
.expect("signing should succeed")
})
}
fn arb_block(event_count: usize) -> impl Strategy<Value = Block> {
(
arb_secret_key(),
prop::collection::vec(arb_audit_event(), event_count),
)
.prop_map(|(sealer_key, events)| {
let sealer = SealerId::new(sealer_key.public_key());
BlockBuilder::new(sealer).events(events).seal(&sealer_key)
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn prop_hash_bytes_roundtrip(bytes in arb_bytes32()) {
let h = Hash::from_bytes(bytes);
prop_assert_eq!(h.as_bytes(), &bytes);
}
#[test]
fn prop_hash_hex_roundtrip(bytes in arb_bytes32()) {
let h = Hash::from_bytes(bytes);
let hex_str = h.to_hex();
let restored = Hash::from_hex(&hex_str).expect("hex roundtrip should succeed");
prop_assert_eq!(h, restored);
}
#[test]
fn prop_hash_bincode_roundtrip(bytes in arb_bytes32()) {
let h = Hash::from_bytes(bytes);
let encoded = bincode::serialize(&h).expect("serialize should succeed");
let decoded: Hash = bincode::deserialize(&encoded).expect("deserialize should succeed");
prop_assert_eq!(h, decoded);
}
#[test]
fn prop_hash_json_roundtrip(bytes in arb_bytes32()) {
let h = Hash::from_bytes(bytes);
let json = serde_json::to_string(&h).expect("json serialize should succeed");
let decoded: Hash = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(h, decoded);
}
#[test]
fn prop_hash_deterministic(data in prop::collection::vec(any::<u8>(), 0..1000)) {
let h1 = hash(&data);
let h2 = hash(&data);
prop_assert_eq!(h1, h2);
}
#[test]
fn prop_hash_avalanche(data in prop::collection::vec(any::<u8>(), 1..100)) {
let h1 = hash(&data);
let mut modified = data.clone();
modified[0] = modified[0].wrapping_add(1);
let h2 = hash(&modified);
prop_assert_ne!(h1, h2);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_sig_bincode_roundtrip(_seed in any::<u64>()) {
let key = SecretKey::generate();
let sig = key.sign(b"test message");
let encoded = bincode::serialize(&sig).expect("serialize should succeed");
let decoded: Sig = bincode::deserialize(&encoded).expect("deserialize should succeed");
prop_assert_eq!(sig.to_bytes(), decoded.to_bytes());
}
#[test]
fn prop_sig_json_roundtrip(_seed in any::<u64>()) {
let key = SecretKey::generate();
let sig = key.sign(b"test message");
let json = serde_json::to_string(&sig).expect("json serialize should succeed");
let decoded: Sig = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(sig.to_bytes(), decoded.to_bytes());
}
#[test]
fn prop_sig_verify_roundtrip(message in prop::collection::vec(any::<u8>(), 0..1000)) {
let key = SecretKey::generate();
let pk = key.public_key();
let sig = key.sign(&message);
prop_assert!(pk.verify(&message, &sig).is_ok());
}
#[test]
fn prop_sig_different_messages(
msg1 in prop::collection::vec(any::<u8>(), 1..100),
msg2 in prop::collection::vec(any::<u8>(), 1..100)
) {
prop_assume!(msg1 != msg2);
let key = SecretKey::generate();
let sig1 = key.sign(&msg1);
let sig2 = key.sign(&msg2);
prop_assert_ne!(sig1.to_bytes(), sig2.to_bytes());
}
#[test]
fn prop_sig_wrong_key_fails(message in prop::collection::vec(any::<u8>(), 1..100)) {
let key1 = SecretKey::generate();
let key2 = SecretKey::generate();
let sig = key1.sign(&message);
prop_assert!(key2.public_key().verify(&message, &sig).is_err());
}
#[test]
fn prop_sig_wrong_message_fails(
msg1 in prop::collection::vec(any::<u8>(), 1..100),
msg2 in prop::collection::vec(any::<u8>(), 1..100)
) {
prop_assume!(msg1 != msg2);
let key = SecretKey::generate();
let sig = key.sign(&msg1);
prop_assert!(key.public_key().verify(&msg2, &sig).is_err());
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_pubkey_bincode_roundtrip(_seed in any::<u64>()) {
let key = SecretKey::generate();
let pk = key.public_key();
let encoded = bincode::serialize(&pk).expect("serialize should succeed");
let decoded: PublicKey = bincode::deserialize(&encoded).expect("deserialize should succeed");
prop_assert_eq!(pk.as_bytes(), decoded.as_bytes());
}
#[test]
fn prop_pubkey_json_roundtrip(_seed in any::<u64>()) {
let key = SecretKey::generate();
let pk = key.public_key();
let json = serde_json::to_string(&pk).expect("json serialize should succeed");
let decoded: PublicKey = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(pk.as_bytes(), decoded.as_bytes());
}
#[test]
fn prop_pubkey_bytes_roundtrip(_seed in any::<u64>()) {
let key = SecretKey::generate();
let pk = key.public_key();
let bytes = pk.as_bytes();
let restored = PublicKey::from_bytes(&bytes).expect("bytes roundtrip should succeed");
prop_assert_eq!(pk.as_bytes(), restored.as_bytes());
}
#[test]
fn prop_pubkey_id_deterministic(_seed in any::<u64>()) {
let key = SecretKey::generate();
let pk = key.public_key();
let id1 = pk.id();
let id2 = pk.id();
prop_assert_eq!(id1, id2);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_secret_key_bytes_roundtrip(_seed in any::<u64>()) {
let key = SecretKey::generate();
let bytes = key.as_bytes();
let restored = SecretKey::from_bytes(&bytes).expect("bytes roundtrip should succeed");
prop_assert_eq!(key.public_key().as_bytes(), restored.public_key().as_bytes());
}
#[test]
fn prop_different_keys_different_pubkeys(_seed1 in any::<u64>(), _seed2 in any::<u64>()) {
let key1 = SecretKey::generate();
let key2 = SecretKey::generate();
prop_assert_ne!(key1.public_key().as_bytes(), key2.public_key().as_bytes());
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn prop_event_bincode_roundtrip(event in arb_audit_event()) {
let encoded = bincode::serialize(&event).expect("serialize should succeed");
let decoded: AuditEvent = bincode::deserialize(&encoded).expect("deserialize should succeed");
prop_assert_eq!(event.id(), decoded.id());
}
#[test]
fn prop_event_json_roundtrip(event in arb_audit_event()) {
let json = serde_json::to_string(&event).expect("json serialize should succeed");
let decoded: AuditEvent = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(event.id(), decoded.id());
}
#[test]
fn prop_event_validates(event in arb_audit_event()) {
prop_assert!(event.validate().is_ok());
}
#[test]
fn prop_event_id_deterministic(event in arb_audit_event()) {
let id1 = event.id();
let id2 = event.id();
prop_assert_eq!(id1, id2);
}
#[test]
fn prop_event_tamper_detected(event in arb_audit_event()) {
let mut tampered = event.clone();
tampered.outcome = Outcome::Failure { reason: "tampered".into() };
prop_assert!(tampered.validate().is_err());
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(20))]
#[test]
fn prop_block_bincode_roundtrip_small(block in arb_block(3)) {
let encoded = bincode::serialize(&block).expect("serialize should succeed");
let decoded: Block = bincode::deserialize(&encoded).expect("deserialize should succeed");
prop_assert_eq!(block.hash(), decoded.hash());
}
#[test]
fn prop_block_json_roundtrip_small(block in arb_block(3)) {
let json = serde_json::to_string(&block).expect("json serialize should succeed");
let decoded: Block = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(block.hash(), decoded.hash());
}
#[test]
fn prop_block_validates(block in arb_block(5)) {
prop_assert!(block.validate(None).is_ok());
}
#[test]
fn prop_block_hash_deterministic(block in arb_block(3)) {
let h1 = block.hash();
let h2 = block.hash();
prop_assert_eq!(h1, h2);
}
#[test]
fn prop_block_event_count(event_count in 0usize..10usize) {
let key = SecretKey::generate();
let sealer = SealerId::new(key.public_key());
let events: Vec<_> = (0..event_count)
.map(|_| {
let actor = ActorId::new(key.public_key(), ActorKind::User);
let resource = ResourceId::new(ResourceKind::Repository, "test");
AuditEvent::builder()
.now()
.event_type(EventType::RepoCreated)
.actor(actor)
.resource(resource)
.sign(&key)
.unwrap()
})
.collect();
let block = BlockBuilder::new(sealer).events(events).seal(&key);
prop_assert_eq!(block.header.events_count as usize, event_count);
prop_assert_eq!(block.events.len(), event_count);
}
#[test]
fn prop_block_height_tamper_detected(block in arb_block(2)) {
let mut tampered = block.clone();
tampered.header.height = 999;
prop_assert!(tampered.validate(None).is_err());
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn prop_event_type_bincode_roundtrip(et in arb_event_type()) {
let encoded = bincode::serialize(&et).expect("serialize should succeed");
let decoded: EventType = bincode::deserialize(&encoded).expect("deserialize should succeed");
prop_assert_eq!(et, decoded);
}
#[test]
fn prop_event_type_json_roundtrip(et in arb_event_type()) {
let json = serde_json::to_string(&et).expect("json serialize should succeed");
let decoded: EventType = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(et, decoded);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn prop_resource_id_bincode_roundtrip(r in arb_resource_id()) {
let encoded = bincode::serialize(&r).expect("serialize should succeed");
let decoded: ResourceId = bincode::deserialize(&encoded).expect("deserialize should succeed");
prop_assert_eq!(r, decoded);
}
#[test]
fn prop_resource_id_json_roundtrip(r in arb_resource_id()) {
let json = serde_json::to_string(&r).expect("json serialize should succeed");
let decoded: ResourceId = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(r, decoded);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn prop_outcome_bincode_roundtrip(o in arb_outcome()) {
let encoded = bincode::serialize(&o).expect("serialize should succeed");
let decoded: Outcome = bincode::deserialize(&encoded).expect("deserialize should succeed");
prop_assert_eq!(o, decoded);
}
#[test]
fn prop_outcome_json_roundtrip(o in arb_outcome()) {
let json = serde_json::to_string(&o).expect("json serialize should succeed");
let decoded: Outcome = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(o, decoded);
}
}
fn arb_principal_kind() -> impl Strategy<Value = PrincipalKind> {
prop_oneof![Just(PrincipalKind::User), Just(PrincipalKind::Organization),]
}
fn arb_principal_id() -> impl Strategy<Value = PrincipalId> {
(arb_principal_kind(), "[a-z0-9_]{3,20}").prop_map(|(kind, id)| {
PrincipalId::new(id, kind).expect("principal creation should succeed")
})
}
fn arb_session_id() -> impl Strategy<Value = SessionId> {
prop::array::uniform16(any::<u8>()).prop_map(SessionId::from_bytes)
}
fn arb_event_id() -> impl Strategy<Value = EventId> {
arb_bytes32().prop_map(|bytes| EventId(Hash::from_bytes(bytes)))
}
#[allow(dead_code)]
fn arb_causal_params() -> impl Strategy<Value = (u32, u64)> {
(0u32..10u32).prop_flat_map(|depth| {
let min_seq = depth as u64;
(Just(depth), min_seq..min_seq + 100)
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn prop_principal_json_roundtrip(p in arb_principal_id()) {
let json = serde_json::to_string(&p).expect("json serialize should succeed");
let decoded: PrincipalId = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(&p, &decoded);
}
#[test]
fn prop_principal_hash_deterministic(p in arb_principal_id()) {
let h1 = p.hash();
let h2 = p.hash();
prop_assert_eq!(h1, h2);
}
#[test]
fn prop_principal_hash_collision_resistant(
id1 in "[a-z0-9]{3,10}",
id2 in "[a-z0-9]{3,10}"
) {
prop_assume!(id1 != id2);
let p1 = PrincipalId::user(&id1).expect("principal creation should succeed");
let p2 = PrincipalId::user(&id2).expect("principal creation should succeed");
prop_assert_ne!(p1.hash(), p2.hash());
}
#[test]
fn prop_principal_root_owner_not_service(p in arb_principal_id()) {
let root = p.root_owner();
prop_assert!(!root.is_service_account());
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn prop_session_id_hex_roundtrip(s in arb_session_id()) {
let hex = s.to_hex();
let restored = SessionId::from_hex(&hex).expect("hex roundtrip should succeed");
prop_assert_eq!(s, restored);
}
#[test]
fn prop_session_id_bytes_roundtrip(bytes in prop::array::uniform16(any::<u8>())) {
let s = SessionId::from_bytes(bytes);
prop_assert_eq!(s.as_bytes(), &bytes);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_session_expiry_consistent(
p in arb_principal_id(),
duration_secs in 1u64..3600u64,
elapsed_secs in 0u64..7200u64
) {
use std::time::Duration;
let start = 1000000i64; let duration = Duration::from_secs(duration_secs);
let current = start + (elapsed_secs as i64 * 1000);
let session = Session::builder()
.principal(p)
.started_at(start)
.max_duration(duration)
.build()
.expect("session creation should succeed");
let is_expired = session.is_expired(current);
let remaining = session.remaining_duration(current);
if is_expired {
prop_assert!(remaining.is_none(), "expired session should have no remaining duration");
}
if remaining.is_some() {
prop_assert!(!is_expired, "session with remaining time should not be expired");
}
}
#[test]
fn prop_session_end_idempotent(
p in arb_principal_id()
) {
use crate::agent::SessionEndReason;
let mut session = Session::builder()
.principal(p)
.build()
.expect("session creation should succeed");
let now = chrono::Utc::now().timestamp_millis();
let result1 = session.end(now, SessionEndReason::Completed);
prop_assert!(result1.is_ok());
let result2 = session.end(now + 1000, SessionEndReason::Completed);
prop_assert!(result2.is_err());
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn prop_causal_inv1_child_sequence_exceeds_parent(
p in arb_principal_id(),
session_id in arb_session_id(),
root_event_id in arb_event_id(),
parent_event_id in arb_event_id(),
child_seq in 1u64..1000u64
) {
let root = CausalContext::root(root_event_id, session_id, p);
prop_assert_eq!(root.sequence(), 0);
let child = root.child(parent_event_id, child_seq);
prop_assert!(child.is_ok());
let child = child.unwrap();
prop_assert!(child.sequence() > root.sequence(), "child sequence must exceed parent");
}
#[test]
fn prop_causal_inv1_rejects_invalid_sequence(
p in arb_principal_id(),
session_id in arb_session_id(),
root_event_id in arb_event_id(),
parent_event_id in arb_event_id()
) {
let root = CausalContext::root(root_event_id, session_id, p);
let child = root.child(parent_event_id, 0);
prop_assert!(child.is_err(), "child with same sequence should fail");
}
#[test]
fn prop_causal_inv2_root_depth_zero(
p in arb_principal_id(),
session_id in arb_session_id(),
event_id in arb_event_id()
) {
let root = CausalContext::root(event_id, session_id, p);
prop_assert_eq!(root.depth(), 0);
prop_assert!(root.is_root());
prop_assert!(root.parent_event_id().is_none());
}
#[test]
fn prop_causal_inv3_depth_bounded(
p in arb_principal_id(),
session_id in arb_session_id(),
event_id in arb_event_id(),
parent_id in arb_event_id(),
depth in 1u32..20u32,
max_depth in 0u32..10u32
) {
use crate::agent::CausalContextBuilder;
let ctx = CausalContextBuilder::new()
.root_event_id(event_id)
.session_id(session_id)
.principal(p)
.depth(depth)
.sequence(depth as u64)
.parent_event_id(parent_id)
.build()
.expect("context creation should succeed");
let result = ctx.validate(max_depth);
if depth > max_depth {
prop_assert!(result.is_err(), "depth {} should exceed max {}", depth, max_depth);
} else {
prop_assert!(result.is_ok(), "depth {} should be within max {}", depth, max_depth);
}
}
#[test]
fn prop_causal_child_depth_increments(
p in arb_principal_id(),
session_id in arb_session_id(),
root_event_id in arb_event_id(),
parent_event_id in arb_event_id()
) {
let root = CausalContext::root(root_event_id, session_id, p);
let child = root.child(parent_event_id, 1).unwrap();
prop_assert_eq!(child.depth(), root.depth() + 1);
let child2 = child.child(parent_event_id, 2).unwrap();
prop_assert_eq!(child2.depth(), child.depth() + 1);
}
#[test]
fn prop_causal_root_preserved(
p in arb_principal_id(),
session_id in arb_session_id(),
root_event_id in arb_event_id(),
parent_event_id in arb_event_id()
) {
let root = CausalContext::root(root_event_id, session_id, p);
let child1 = root.child(parent_event_id, 1).unwrap();
let child2 = child1.child(parent_event_id, 2).unwrap();
prop_assert_eq!(root.root_event_id(), child1.root_event_id());
prop_assert_eq!(root.root_event_id(), child2.root_event_id());
}
#[test]
fn prop_causal_principal_preserved(
p in arb_principal_id(),
session_id in arb_session_id(),
root_event_id in arb_event_id(),
parent_event_id in arb_event_id()
) {
let root = CausalContext::root(root_event_id, session_id, p.clone());
let child1 = root.child(parent_event_id, 1).unwrap();
let child2 = child1.child(parent_event_id, 2).unwrap();
prop_assert_eq!(root.principal(), &p);
prop_assert_eq!(child1.principal(), &p);
prop_assert_eq!(child2.principal(), &p);
}
#[test]
fn prop_causal_session_preserved(
p in arb_principal_id(),
session_id in arb_session_id(),
root_event_id in arb_event_id(),
parent_event_id in arb_event_id()
) {
let root = CausalContext::root(root_event_id, session_id, p);
let child1 = root.child(parent_event_id, 1).unwrap();
let child2 = child1.child(parent_event_id, 2).unwrap();
prop_assert_eq!(root.session_id(), session_id);
prop_assert_eq!(child1.session_id(), session_id);
prop_assert_eq!(child2.session_id(), session_id);
}
#[test]
fn prop_causal_validate_root_mismatch(
p in arb_principal_id(),
session_id in arb_session_id(),
root_event_id1 in arb_event_id(),
root_event_id2 in arb_event_id(),
parent_event_id in arb_event_id()
) {
use crate::agent::CausalContextBuilder;
prop_assume!(root_event_id1 != root_event_id2);
let parent = CausalContext::root(root_event_id1, session_id, p.clone());
let child = CausalContextBuilder::new()
.root_event_id(root_event_id2) .session_id(session_id)
.principal(p)
.depth(1)
.sequence(1)
.parent_event_id(parent_event_id)
.build()
.expect("context creation should succeed");
let result = child.validate_against_parent(&parent);
prop_assert!(result.is_err(), "mismatched root event should fail validation");
}
#[test]
fn prop_causal_context_json_roundtrip(
p in arb_principal_id(),
session_id in arb_session_id(),
event_id in arb_event_id()
) {
let ctx = CausalContext::root(event_id, session_id, p);
let json = serde_json::to_string(&ctx).expect("json serialize should succeed");
let decoded: CausalContext = serde_json::from_str(&json).expect("json deserialize should succeed");
prop_assert_eq!(&ctx, &decoded);
}
}