#![cfg(test)]
use crate::error::RetrievalError;
use crate::event_dag::EventLayer;
use crate::property::backend::lww::LWWBackend;
use crate::property::backend::PropertyBackend;
use crate::retrieval::GetEvents;
use crate::value::Value;
use super::comparison::compare;
use super::relation::AbstractCausalRelation;
use ankurah_proto::{Clock, EntityId, Event, EventId, OperationSet};
use async_trait::async_trait;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
#[derive(Clone)]
struct MockRetriever {
events: HashMap<EventId, Event>,
}
impl MockRetriever {
fn new() -> Self { Self { events: HashMap::new() } }
fn add_event(&mut self, event: Event) { self.events.insert(event.id(), event); }
}
#[async_trait]
impl GetEvents for MockRetriever {
async fn get_event(&self, event_id: &EventId) -> Result<Event, RetrievalError> {
self.events.get(event_id).cloned().ok_or_else(|| RetrievalError::EventNotFound(event_id.clone()))
}
async fn event_stored(&self, _event_id: &EventId) -> Result<bool, RetrievalError> { Ok(false) }
}
fn make_test_event(seed: u8, parent_ids: &[EventId]) -> Event {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = seed;
let entity_id = EntityId::from_bytes(entity_id_bytes);
Event { entity_id, collection: "test".into(), parent: Clock::from(parent_ids.to_vec()), operations: OperationSet(BTreeMap::new()) }
}
fn make_test_event_u16(seed: u16, parent_ids: &[EventId]) -> Event {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0..2].copy_from_slice(&seed.to_be_bytes());
let entity_id = EntityId::from_bytes(entity_id_bytes);
Event { entity_id, collection: "test".into(), parent: Clock::from(parent_ids.to_vec()), operations: OperationSet(BTreeMap::new()) }
}
macro_rules! clock {
($($id:expr),* $(,)?) => {
Clock::from(vec![$($id.clone()),*])
};
}
fn make_lww_event(seed: u8, properties: Vec<(&str, &str)>) -> Event {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = seed;
let entity_id = EntityId::from_bytes(entity_id_bytes);
let backend = LWWBackend::new();
for (name, value) in properties {
backend.set(name.into(), Some(Value::String(value.into())));
}
let ops = backend.to_operations().unwrap().unwrap();
Event {
entity_id,
collection: "test".into(),
parent: Clock::default(),
operations: OperationSet(BTreeMap::from([("lww".to_string(), ops)])),
}
}
fn make_lww_event_with_parent(seed: u8, properties: Vec<(&str, &str)>, parent_ids: &[EventId]) -> Event {
let mut event = make_lww_event(seed, properties);
event.parent = Clock::from(parent_ids.to_vec());
event
}
fn layer_from_refs_with_context(already_applied: &[&Event], to_apply: &[&Event], context_events: &[&Event]) -> EventLayer {
let mut dag = BTreeMap::new();
for event in already_applied.iter().chain(to_apply.iter()).chain(context_events.iter()) {
dag.insert(event.id(), event.parent.as_slice().to_vec());
}
EventLayer::new(already_applied.iter().map(|e| (*e).clone()).collect(), to_apply.iter().map(|e| (*e).clone()).collect(), Arc::new(dag))
}
fn layer_from_refs(already_applied: &[&Event], to_apply: &[&Event]) -> EventLayer {
layer_from_refs_with_context(already_applied, to_apply, &[])
}
#[tokio::test]
async fn test_linear_history() {
let mut retriever = MockRetriever::new();
let ev1 = make_test_event(1, &[]);
let id1 = ev1.id();
retriever.add_event(ev1);
let ev2 = make_test_event(2, &[id1.clone()]);
let id2 = ev2.id();
retriever.add_event(ev2);
let ev3 = make_test_event(3, &[id2]);
let id3 = ev3.id();
retriever.add_event(ev3);
let ancestor = clock!(id1);
let descendant = clock!(id3);
let result = compare(retriever.clone(), &descendant, &ancestor, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let result = compare(retriever.clone(), &ancestor, &descendant, 100).await.unwrap();
assert_eq!(result.relation, AbstractCausalRelation::StrictAscends);
}
#[tokio::test]
async fn test_concurrent_history() {
let mut retriever = MockRetriever::new();
let ev1 = make_test_event(1, &[]);
let id1 = ev1.id();
retriever.add_event(ev1);
let ev2 = make_test_event(2, &[id1.clone()]);
let id2 = ev2.id();
retriever.add_event(ev2);
let ev3 = make_test_event(3, &[id1.clone()]);
let id3 = ev3.id();
retriever.add_event(ev3);
let ev4 = make_test_event(4, &[id1.clone()]);
let id4 = ev4.id();
retriever.add_event(ev4);
let ev5 = make_test_event(5, &[id2.clone(), id3.clone()]);
let id5 = ev5.id();
retriever.add_event(ev5);
let ev6 = make_test_event(6, &[id3.clone(), id4]);
let id6 = ev6.id();
retriever.add_event(ev6);
let ev7 = make_test_event(7, &[id5.clone(), id6.clone()]);
let _id7 = ev7.id();
retriever.add_event(ev7);
{
let ancestor = clock!(id1);
let descendant = clock!(id5);
let result = compare(retriever.clone(), &descendant, &ancestor, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let result = compare(retriever.clone(), &ancestor, &descendant, 100).await.unwrap();
assert_eq!(result.relation, AbstractCausalRelation::StrictAscends);
}
{
let ancestor = clock!(id2, id3);
let descendant = clock!(id5);
let result = compare(retriever.clone(), &descendant, &ancestor, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let result = compare(retriever.clone(), &ancestor, &descendant, 100).await.unwrap();
assert_eq!(result.relation, AbstractCausalRelation::StrictAscends);
}
{
let a = clock!(id2);
let b = clock!(id3);
let result = compare(retriever.clone(), &a, &b, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::DivergedSince { ref meet, .. } if meet == &vec![id1.clone()]
));
let result = compare(retriever.clone(), &b, &a, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::DivergedSince { ref meet, .. } if meet == &vec![id1.clone()]
));
}
{
let a = clock!(id6);
let b = clock!(id2, id3);
let result = compare(retriever.clone(), &a, &b, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::DivergedSince { ref meet, .. } if meet == &vec![id3.clone()]
));
}
}
#[tokio::test]
async fn test_incomparable() {
let mut retriever = MockRetriever::new();
let ev1 = make_test_event(1, &[]);
let id1 = ev1.id();
retriever.add_event(ev1);
let ev2 = make_test_event(2, &[id1.clone()]);
let id2 = ev2.id();
retriever.add_event(ev2);
let ev3 = make_test_event(3, &[id2.clone()]);
let id3 = ev3.id();
retriever.add_event(ev3);
let ev4 = make_test_event(4, &[id1.clone()]);
let _id4 = ev4.id();
retriever.add_event(ev4);
let ev5 = make_test_event(5, &[_id4]);
let id5 = ev5.id();
retriever.add_event(ev5);
let ev6 = make_test_event(6, &[]);
let id6 = ev6.id();
retriever.add_event(ev6);
let ev7 = make_test_event(7, &[id6.clone()]);
let id7 = ev7.id();
retriever.add_event(ev7);
let ev8 = make_test_event(8, &[id7]);
let id8 = ev8.id();
retriever.add_event(ev8);
{
let a = clock!(id3);
let b = clock!(id8);
let result = compare(retriever.clone(), &a, &b, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::Disjoint { ref subject_root, ref other_root, .. }
if *subject_root == id1 && *other_root == id6
));
}
{
let a = clock!(id2);
let b = clock!(id8);
let result = compare(retriever.clone(), &a, &b, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::Disjoint { ref subject_root, ref other_root, .. }
if *subject_root == id1 && *other_root == id6
));
}
{
let a = clock!(id3);
let b = clock!(id5, id8);
let result = compare(retriever.clone(), &a, &b, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::DivergedSince { ref meet, .. } if meet.is_empty()
));
}
}
#[tokio::test]
async fn test_empty_clocks() {
let mut retriever = MockRetriever::new();
let ev1 = make_test_event(1, &[]);
let id1 = ev1.id();
retriever.add_event(ev1);
let empty = Clock::default();
let non_empty = clock!(id1);
let result = compare(retriever.clone(), &empty, &empty, 100).await.unwrap();
assert_eq!(result.relation, AbstractCausalRelation::Equal);
let expected =
AbstractCausalRelation::DivergedSince { meet: vec![], subject: vec![], other: vec![], subject_chain: vec![], other_chain: vec![] };
let result = compare(retriever.clone(), &non_empty, &empty, 100).await.unwrap();
assert_eq!(result.relation, expected);
let result = compare(retriever.clone(), &empty, &non_empty, 100).await.unwrap();
assert_eq!(result.relation, expected);
}
#[tokio::test]
async fn test_budget_exceeded() {
let mut retriever = MockRetriever::new();
let mut ids: Vec<EventId> = Vec::new();
for i in 0..20u8 {
let parents = if i == 0 { vec![] } else { vec![ids[i as usize - 1].clone()] };
let ev = make_test_event(i + 1, &parents);
ids.push(ev.id());
retriever.add_event(ev);
}
let ancestor = clock!(ids[0].clone());
let descendant = clock!(ids[19].clone());
let result = compare(retriever.clone(), &descendant, &ancestor, 1).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::BudgetExceeded { .. }));
let result = compare(retriever.clone(), &descendant, &ancestor, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let result = compare(retriever.clone(), &ancestor, &descendant, 100).await.unwrap();
assert_eq!(result.relation, AbstractCausalRelation::StrictAscends);
}
#[tokio::test]
async fn test_self_comparison() {
let mut retriever = MockRetriever::new();
let ev1 = make_test_event(1, &[]);
let id1 = ev1.id();
retriever.add_event(ev1);
let clock = clock!(id1);
let result = compare(retriever.clone(), &clock, &clock, 100).await.unwrap();
assert_eq!(result.relation, AbstractCausalRelation::Equal);
}
#[tokio::test]
async fn multiple_roots() {
let mut retriever = MockRetriever::new();
let mut root_ids = Vec::new();
for i in 1..=6u8 {
let ev = make_test_event(i, &[]);
root_ids.push(ev.id());
retriever.add_event(ev);
}
let ev7 = make_test_event(7, &root_ids);
let id7 = ev7.id();
retriever.add_event(ev7);
let ev8 = make_test_event(8, &[id7]);
let id8 = ev8.id();
retriever.add_event(ev8);
let subject = clock!(id8);
let big_other = Clock::from(root_ids.clone());
let result = compare(retriever.clone(), &subject, &big_other, 1_000).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let result = compare(retriever.clone(), &big_other, &subject, 1_000).await.unwrap();
assert_eq!(result.relation, AbstractCausalRelation::StrictAscends);
}
#[tokio::test]
async fn test_compare_event_unstored() {
let mut retriever = MockRetriever::new();
let ev1 = make_test_event(1, &[]);
let id1 = ev1.id();
retriever.add_event(ev1);
let ev2 = make_test_event(2, &[id1.clone()]);
let id2 = ev2.id();
retriever.add_event(ev2);
let ev3 = make_test_event(3, &[id2.clone()]);
let id3 = ev3.id();
retriever.add_event(ev3);
let unstored_event = make_test_event(4, &[id3.clone()]);
let clock_1 = clock!(id1);
let clock_2 = clock!(id2);
let clock_3 = clock!(id3);
retriever.add_event(unstored_event.clone());
let result = compare(retriever.clone(), &Clock::from(vec![unstored_event.id()]), &clock_1, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let result = compare(retriever.clone(), &Clock::from(vec![unstored_event.id()]), &clock_2, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let result = compare(retriever.clone(), &Clock::from(vec![unstored_event.id()]), &clock_3, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let unstored_merge_event = make_test_event(5, &[id2.clone(), id3.clone()]);
retriever.add_event(unstored_merge_event.clone());
let result = compare(retriever.clone(), &Clock::from(vec![unstored_merge_event.id()]), &clock_1, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let ev10 = make_test_event(10, &[]); let id10 = ev10.id();
retriever.add_event(ev10);
let incomparable_clock = clock!(id10);
let result = compare(retriever.clone(), &Clock::from(vec![unstored_event.id()]), &incomparable_clock, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::Disjoint { ref subject_root, ref other_root, .. }
if *subject_root == id1 && *other_root == id10
));
let root_event = make_test_event(11, &[]);
retriever.add_event(root_event.clone());
let empty_clock = Clock::default();
let result = compare(retriever.clone(), &Clock::from(vec![root_event.id()]), &empty_clock, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::DivergedSince { ref meet, .. } if meet.is_empty()
));
let result = compare(retriever.clone(), &Clock::from(vec![root_event.id()]), &clock_1, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::Disjoint { .. }));
let empty_clock = Clock::default();
let result = compare(retriever.clone(), &Clock::from(vec![unstored_event.id()]), &empty_clock, 100).await.unwrap();
assert!(matches!(
result.relation,
AbstractCausalRelation::DivergedSince { ref meet, .. } if meet.is_empty()
));
}
#[tokio::test]
async fn test_compare_event_redundant_delivery() {
let mut retriever = MockRetriever::new();
let ev1 = make_test_event(1, &[]);
let id1 = ev1.id();
retriever.add_event(ev1);
let ev2 = make_test_event(2, &[id1]);
let id2 = ev2.id();
retriever.add_event(ev2);
let ev3 = make_test_event(3, &[id2]);
let id3 = ev3.id();
retriever.add_event(ev3);
let unstored_event = make_test_event(4, &[id3.clone()]);
let id4 = unstored_event.id();
retriever.add_event(unstored_event.clone());
let clock_3 = clock!(id3);
let result = compare(retriever.clone(), &Clock::from(vec![unstored_event.id()]), &clock_3, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }));
let ev4_stored = make_test_event(4, &[id3.clone()]);
retriever.add_event(ev4_stored);
let clock_with_event = clock!(id4);
let result = compare(retriever.clone(), &Clock::from(vec![unstored_event.id()]), &clock_with_event, 100).await.unwrap();
assert_eq!(result.relation, AbstractCausalRelation::Equal);
let clock_with_multiple = clock!(id3, id4);
let result = compare(retriever.clone(), &Clock::from(vec![unstored_event.id()]), &clock_with_multiple, 100).await.unwrap();
assert!(
matches!(result.relation, AbstractCausalRelation::Equal | AbstractCausalRelation::StrictDescends { .. }),
"Redundant delivery with multiple heads should return Equal or StrictDescends, got {:?}",
result.relation
);
}
#[tokio::test]
async fn test_missing_event_busyloop() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
let ev_c = make_test_event(3, &[id_b.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let result = compare(retriever, &clock!(id_c), &clock!(id_a), 100).await;
let err = result.err().expect("Expected EventNotFound error for missing event B, but got Ok");
assert!(
matches!(err, RetrievalError::EventNotFound(ref id) if *id == id_b),
"Error should be EventNotFound for event B, got {:?}",
err
);
}
#[tokio::test]
async fn test_both_frontiers_unfetchable_meet_point() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_a.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let result = compare(retriever.clone(), &clock!(id_b), &clock!(id_c), 100).await;
let result = result.expect("Should succeed because A is on both frontiers and processed as common ancestor");
assert!(
matches!(result.relation, AbstractCausalRelation::DivergedSince { ref meet, .. } if meet == &vec![id_a.clone()]),
"Should find meet at A (the unfetchable common ancestor), got {:?}",
result.relation
);
}
#[tokio::test]
async fn test_multihead_event_extends_one_tip() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_a]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let event_d = make_test_event(4, &[id_c.clone()]);
let entity_head = clock!(id_b, id_c);
retriever.add_event(event_d.clone());
let result = compare(retriever.clone(), &Clock::from(vec![event_d.id()]), &entity_head, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::DivergedSince { .. }), "Expected DivergedSince, got {:?}", result.relation);
if let AbstractCausalRelation::DivergedSince { meet, .. } = &result.relation {
assert_eq!(meet, &vec![id_c], "Meet should be [C]");
}
}
#[tokio::test]
async fn test_multihead_event_extends_multiple_tips() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_a]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let event_d = make_test_event(4, &[id_b.clone(), id_c.clone()]);
let entity_head = clock!(id_b, id_c);
retriever.add_event(event_d.clone());
let result = compare(retriever.clone(), &Clock::from(vec![event_d.id()]), &entity_head, 100).await.unwrap();
assert!(
matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }),
"Expected StrictDescends when event merges all tips, got {:?}",
result.relation
);
}
#[tokio::test]
async fn test_multihead_three_way_concurrency() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_a.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_d = make_test_event(4, &[id_a]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let event_e = make_test_event(5, &[id_b.clone()]);
let entity_head = clock!(id_b, id_c, id_d);
retriever.add_event(event_e.clone());
let result = compare(retriever.clone(), &Clock::from(vec![event_e.id()]), &entity_head, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::DivergedSince { .. }), "Expected DivergedSince, got {:?}", result.relation);
if let AbstractCausalRelation::DivergedSince { meet, .. } = &result.relation {
assert_eq!(meet, &vec![id_b], "Meet should be [B]");
}
}
#[tokio::test]
async fn test_deep_diamond_asymmetric_branches() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_a.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_d = make_test_event(4, &[id_b]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_e = make_test_event(5, &[id_c]);
let id_e = ev_e.id();
retriever.add_event(ev_e);
let ev_f = make_test_event(6, &[id_d]);
let id_f = ev_f.id();
retriever.add_event(ev_f);
let ev_g = make_test_event(7, &[id_e]);
let id_g = ev_g.id();
retriever.add_event(ev_g);
let ev_h = make_test_event(8, &[id_f]);
let id_h = ev_h.id();
retriever.add_event(ev_h);
let ev_i = make_test_event(9, &[id_g]);
let id_i = ev_i.id();
retriever.add_event(ev_i);
let clock_h = clock!(id_h);
let clock_i = clock!(id_i);
let result = compare(retriever.clone(), &clock_h, &clock_i, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::DivergedSince { .. }), "Expected DivergedSince, got {:?}", result.relation);
if let AbstractCausalRelation::DivergedSince { meet, subject_chain, other_chain, .. } = &result.relation {
assert_eq!(meet, &vec![id_a], "Meet should be A");
assert_eq!(subject_chain.len(), 4, "Subject chain should have 4 events");
assert_eq!(other_chain.len(), 4, "Other chain should have 4 events");
}
}
#[tokio::test]
async fn test_short_branch_from_deep_point() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_b]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_d = make_test_event(4, &[id_c]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_e = make_test_event(5, &[id_d.clone()]);
let id_e = ev_e.id();
retriever.add_event(ev_e);
let ev_f = make_test_event(6, &[id_e]);
let id_f = ev_f.id();
retriever.add_event(ev_f);
let ev_g = make_test_event(7, &[id_f]);
let id_g = ev_g.id();
retriever.add_event(ev_g);
let ev_h = make_test_event(8, &[id_g]);
let id_h = ev_h.id();
retriever.add_event(ev_h);
let ev_x = make_test_event(9, &[id_d]); let id_x = ev_x.id();
retriever.add_event(ev_x);
let ev_y = make_test_event(10, &[id_x]); let _id_y = ev_y.id();
let clock_h = clock!(id_h);
retriever.add_event(ev_y.clone());
let result = compare(retriever.clone(), &Clock::from(vec![ev_y.id()]), &clock_h, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::DivergedSince { .. }), "Expected DivergedSince, got {:?}", result.relation);
if let AbstractCausalRelation::DivergedSince { meet, subject_chain, other_chain, .. } = &result.relation {
assert!(!meet.is_empty(), "Meet should not be empty");
assert!(!subject_chain.is_empty(), "Subject chain should not be empty");
assert!(!other_chain.is_empty(), "Other chain should not be empty");
}
}
#[tokio::test]
async fn test_late_arrival_long_branch_from_genesis() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_b]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_d = make_test_event(4, &[id_c]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_x = make_test_event(5, &[id_a.clone()]);
let id_x = ev_x.id();
retriever.add_event(ev_x);
let ev_y = make_test_event(6, &[id_x]);
let id_y = ev_y.id();
retriever.add_event(ev_y);
let ev_z = make_test_event(7, &[id_y]);
let id_z = ev_z.id();
retriever.add_event(ev_z);
let clock_d = clock!(id_d);
let clock_z = clock!(id_z);
let result = compare(retriever.clone(), &clock_d, &clock_z, 100).await.unwrap();
assert!(matches!(result.relation, AbstractCausalRelation::DivergedSince { .. }), "Expected DivergedSince, got {:?}", result.relation);
if let AbstractCausalRelation::DivergedSince { meet, subject_chain, other_chain, .. } = &result.relation {
assert_eq!(meet, &vec![id_a], "Meet should be A (genesis)");
assert_eq!(subject_chain.len(), 3, "Subject chain B, C, D should have 3 events");
assert_eq!(other_chain.len(), 3, "Other chain X, Y, Z should have 3 events");
}
}
#[tokio::test]
async fn test_forward_chain_ordering() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_b.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_d = make_test_event(4, &[id_c.clone()]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_e = make_test_event(5, &[id_d.clone()]);
let id_e = ev_e.id();
retriever.add_event(ev_e);
let clock_a = clock!(id_a);
let clock_e = clock!(id_e);
let result = compare(retriever.clone(), &clock_e, &clock_a, 100).await.unwrap();
if let AbstractCausalRelation::StrictDescends { chain } = &result.relation {
assert_eq!(chain.len(), 4, "Chain should have 4 events");
assert_eq!(chain, &vec![id_b, id_c, id_d, id_e], "Chain should be [B, C, D, E] in causal order");
} else {
panic!("Expected StrictDescends, got {:?}", result.relation);
}
}
#[tokio::test]
async fn test_diverged_chains_ordering() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_a.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_d = make_test_event(4, &[id_b.clone()]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_e = make_test_event(5, &[id_c.clone()]);
let id_e = ev_e.id();
retriever.add_event(ev_e);
let clock_d = clock!(id_d);
let clock_e = clock!(id_e);
let result = compare(retriever.clone(), &clock_d, &clock_e, 100).await.unwrap();
if let AbstractCausalRelation::DivergedSince { meet, subject_chain, other_chain, .. } = &result.relation {
assert_eq!(meet, &vec![id_a], "Meet should be A");
assert_eq!(subject_chain, &vec![id_b, id_d], "Subject chain should be [B, D]");
assert_eq!(other_chain, &vec![id_c, id_e], "Other chain should be [C, E]");
} else {
panic!("Expected DivergedSince, got {:?}", result.relation);
}
}
#[cfg(test)]
mod lww_layer_tests {
use super::*;
#[test]
fn test_apply_layer_higher_event_id_wins() {
let backend = LWWBackend::new();
let event_a = make_lww_event(1, vec![("x", "value_a")]);
let event_b = make_lww_event(2, vec![("x", "value_b")]);
let (winner_value, _loser_value) = if event_a.id() > event_b.id() { ("value_a", "value_b") } else { ("value_b", "value_a") };
let already_applied: Vec<&Event> = vec![];
let to_apply: Vec<&Event> = vec![&event_a, &event_b];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
assert_eq!(backend.get(&"x".into()), Some(Value::String(winner_value.into())));
}
#[test]
fn test_apply_layer_already_applied_wins() {
let backend = LWWBackend::new();
let event_a = make_lww_event(1, vec![("x", "value_a")]);
let event_b = make_lww_event(2, vec![("x", "value_b")]);
let (winner_event, loser_event, _winner_value) =
if event_a.id() > event_b.id() { (&event_a, &event_b, "value_a") } else { (&event_b, &event_a, "value_b") };
let already_applied: Vec<&Event> = vec![winner_event];
let to_apply: Vec<&Event> = vec![loser_event];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
assert_eq!(backend.get(&"x".into()), None);
}
#[test]
fn test_apply_layer_to_apply_overwrites_already_applied() {
let backend = LWWBackend::new();
let event_a = make_lww_event(1, vec![("x", "value_a")]);
let event_b = make_lww_event(2, vec![("x", "value_b")]);
let (winner_event, loser_event, winner_value) =
if event_a.id() > event_b.id() { (&event_a, &event_b, "value_a") } else { (&event_b, &event_a, "value_b") };
let already_applied: Vec<&Event> = vec![loser_event];
let to_apply: Vec<&Event> = vec![winner_event];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
assert_eq!(backend.get(&"x".into()), Some(Value::String(winner_value.into())));
}
#[test]
fn test_apply_layer_multiple_properties() {
let backend = LWWBackend::new();
let event_a = make_lww_event(1, vec![("x", "x_from_a")]);
let event_b = make_lww_event(2, vec![("x", "x_from_b"), ("y", "y_from_b")]);
let already_applied: Vec<&Event> = vec![];
let to_apply: Vec<&Event> = vec![&event_a, &event_b];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
let expected_x = if event_a.id() > event_b.id() { "x_from_a" } else { "x_from_b" };
assert_eq!(backend.get(&"x".into()), Some(Value::String(expected_x.into())));
assert_eq!(backend.get(&"y".into()), Some(Value::String("y_from_b".into())));
}
#[test]
fn test_apply_layer_three_way_concurrency() {
let backend = LWWBackend::new();
let event_a = make_lww_event(1, vec![("x", "value_a")]);
let event_b = make_lww_event(2, vec![("x", "value_b")]);
let event_c = make_lww_event(3, vec![("x", "value_c")]);
let events = vec![(&event_a, "value_a"), (&event_b, "value_b"), (&event_c, "value_c")];
let (winner_event, winner_value) = events.iter().max_by_key(|(e, _)| e.id()).unwrap();
let already_applied: Vec<&Event> = vec![&event_a];
let to_apply: Vec<&Event> = vec![&event_b, &event_c];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
if winner_event.id() == event_a.id() {
let _to_apply_winner_value = if event_b.id() > event_c.id() { "value_b" } else { "value_c" };
assert_eq!(backend.get(&"x".into()), None);
} else {
assert_eq!(backend.get(&"x".into()), Some(Value::String((*winner_value).into())));
}
}
#[test]
fn test_cross_layer_already_applied_winner_persistence() {
let event_z = make_lww_event(9, vec![("x", "value_from_Z")]);
let meet = make_test_event(50, &[]);
let remote_mid = make_test_event(51, &[meet.id()]); let event_b = make_lww_event_with_parent(2, vec![("x", "value_from_B")], &[remote_mid.id()]);
let event_a = (0u8..=255)
.map(|seed| make_lww_event_with_parent(seed, vec![("x", "value_from_A")], &[meet.id()]))
.find(|a| a.id() > event_b.id())
.expect("some seed yields A.id > B.id");
let lww_key = "lww".to_string();
let a_ops = event_a.operations.get(&lww_key).unwrap();
let z_ops = event_z.operations.get(&lww_key).unwrap();
let layer1 = || layer_from_refs_with_context(&[&event_a], &[&remote_mid], &[&meet, &event_b]);
let layer1_a_fresh = || layer_from_refs_with_context(&[], &[&event_a, &remote_mid], &[&meet, &event_b]);
let layer2 = || layer_from_refs_with_context(&[], &[&event_b], &[&meet, &event_a, &remote_mid]);
let backend_local = LWWBackend::new();
backend_local.apply_operations_with_event(a_ops, event_a.id()).unwrap();
backend_local.apply_layer(&layer1()).unwrap();
backend_local.apply_layer(&layer2()).unwrap();
assert_eq!(
backend_local.get(&"x".into()),
Some(Value::String("value_from_A".into())),
"reachable state: stored entry reflects already_applied A, so A must survive B"
);
let backend_remote = LWWBackend::new();
backend_remote.apply_operations_with_event(z_ops, event_z.id()).unwrap();
backend_remote.apply_layer(&layer1_a_fresh()).unwrap();
backend_remote.apply_layer(&layer2()).unwrap();
assert_eq!(
backend_remote.get(&"x".into()),
Some(Value::String("value_from_A".into())),
"replica receiving A via to_apply must converge to A's value"
);
let backend_stale = LWWBackend::new();
backend_stale.apply_operations_with_event(z_ops, event_z.id()).unwrap();
backend_stale.apply_layer(&layer1()).unwrap();
assert_eq!(
backend_stale.get_event_id(&"x".into()),
Some(event_z.id()),
"already_applied winner A is not written back; stored entry still stamped with Z"
);
backend_stale.apply_layer(&layer2()).unwrap();
assert_eq!(
backend_stale.get(&"x".into()),
Some(Value::String("value_from_B".into())),
"stale-seed mechanics: with an (entity-unreachable) below-meet stored entry, B beats A"
);
assert_eq!(backend_stale.get_event_id(&"x".into()), Some(event_b.id()));
}
}
#[cfg(test)]
mod yrs_layer_tests {
use super::*;
use crate::property::backend::yrs::YrsBackend;
fn make_yrs_event(seed: u8, text_field: &str, insert_text: &str) -> Event {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = seed;
let entity_id = EntityId::from_bytes(entity_id_bytes);
let backend = YrsBackend::new();
backend.insert(text_field, 0, insert_text).unwrap();
let ops = backend.to_operations().unwrap().unwrap();
Event {
entity_id,
collection: "test".into(),
parent: Clock::default(),
operations: OperationSet(BTreeMap::from([("yrs".to_string(), ops)])),
}
}
#[test]
fn test_yrs_apply_layer_concurrent_inserts() {
let backend = YrsBackend::new();
let event_a = make_yrs_event(1, "text", "hello");
let event_b = make_yrs_event(2, "text", "world");
let already_applied: Vec<&Event> = vec![];
let to_apply: Vec<&Event> = vec![&event_a, &event_b];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
let result = backend.get_string("text").unwrap();
assert!(result.contains("hello") || result.contains("world"), "Expected at least one insert to be present, got: {}", result);
}
#[test]
fn test_yrs_apply_layer_ignores_already_applied() {
let backend = YrsBackend::new();
let event_a = make_yrs_event(1, "text", "hello");
let already_applied: Vec<&Event> = vec![];
let to_apply: Vec<&Event> = vec![&event_a];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
let initial_text = backend.get_string("text").unwrap();
assert_eq!(initial_text, "hello");
let event_b = make_yrs_event(2, "text", "world");
let already_applied: Vec<&Event> = vec![&event_a];
let to_apply: Vec<&Event> = vec![&event_b];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
let final_text = backend.get_string("text").unwrap();
assert!(final_text.contains("world"), "Expected 'world' to be in text, got: {}", final_text);
}
#[test]
fn test_yrs_apply_layer_order_independent() {
let event_a = make_yrs_event(1, "text", "A");
let event_b = make_yrs_event(2, "text", "B");
let event_c = make_yrs_event(3, "text", "C");
let backend1 = YrsBackend::new();
backend1.apply_layer(&layer_from_refs(&[], &[&event_a])).unwrap();
backend1.apply_layer(&layer_from_refs(&[], &[&event_b])).unwrap();
backend1.apply_layer(&layer_from_refs(&[], &[&event_c])).unwrap();
let result1 = backend1.get_string("text").unwrap();
let backend2 = YrsBackend::new();
backend2.apply_layer(&layer_from_refs(&[], &[&event_c])).unwrap();
backend2.apply_layer(&layer_from_refs(&[], &[&event_a])).unwrap();
backend2.apply_layer(&layer_from_refs(&[], &[&event_b])).unwrap();
let result2 = backend2.get_string("text").unwrap();
let backend3 = YrsBackend::new();
backend3.apply_layer(&layer_from_refs(&[], &[&event_b])).unwrap();
backend3.apply_layer(&layer_from_refs(&[], &[&event_c])).unwrap();
backend3.apply_layer(&layer_from_refs(&[], &[&event_a])).unwrap();
let result3 = backend3.get_string("text").unwrap();
assert_eq!(result1, result2, "Order 1 vs Order 2 should produce same result");
assert_eq!(result2, result3, "Order 2 vs Order 3 should produce same result");
}
#[test]
fn test_yrs_apply_layer_empty_to_apply() {
let backend = YrsBackend::new();
backend.insert("text", 0, "initial").unwrap();
let event_a = make_yrs_event(1, "text", "hello");
let already_applied: Vec<&Event> = vec![&event_a];
let to_apply: Vec<&Event> = vec![];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
let result = backend.get_string("text").unwrap();
assert_eq!(result, "initial");
}
}
#[cfg(test)]
mod determinism_tests {
use super::*;
#[test]
fn test_two_event_determinism() {
let event_a = make_lww_event(1, vec![("x", "value_a")]);
let event_b = make_lww_event(2, vec![("x", "value_b")]);
let backend1 = LWWBackend::new();
backend1.apply_layer(&layer_from_refs(&[], &[&event_a, &event_b])).unwrap();
let result1 = backend1.get(&"x".into());
let backend2 = LWWBackend::new();
backend2.apply_layer(&layer_from_refs(&[], &[&event_b, &event_a])).unwrap();
let result2 = backend2.get(&"x".into());
assert_eq!(result1, result2, "Different orderings should produce same result");
}
#[test]
fn test_three_event_determinism() {
let event_a = make_lww_event(1, vec![("x", "A")]);
let event_b = make_lww_event(2, vec![("x", "B")]);
let event_c = make_lww_event(3, vec![("x", "C")]);
let permutations: Vec<Vec<&Event>> = vec![
vec![&event_a, &event_b, &event_c],
vec![&event_a, &event_c, &event_b],
vec![&event_b, &event_a, &event_c],
vec![&event_b, &event_c, &event_a],
vec![&event_c, &event_a, &event_b],
vec![&event_c, &event_b, &event_a],
];
let mut results = Vec::new();
for perm in &permutations {
let backend = LWWBackend::new();
backend.apply_layer(&layer_from_refs(&[], perm)).unwrap();
results.push(backend.get(&"x".into()));
}
for (i, result) in results.iter().enumerate() {
assert_eq!(result, &results[0], "Permutation {} produced different result than permutation 0", i);
}
}
#[test]
fn test_multi_property_determinism() {
let event_a = make_lww_event(1, vec![("x", "x_from_a"), ("y", "y_from_a")]);
let event_b = make_lww_event(2, vec![("x", "x_from_b"), ("z", "z_from_b")]);
let event_c = make_lww_event(3, vec![("y", "y_from_c"), ("z", "z_from_c")]);
let backend1 = LWWBackend::new();
backend1.apply_layer(&layer_from_refs(&[], &[&event_a, &event_b, &event_c])).unwrap();
let backend2 = LWWBackend::new();
backend2.apply_layer(&layer_from_refs(&[], &[&event_c, &event_b, &event_a])).unwrap();
assert_eq!(backend1.get(&"x".into()), backend2.get(&"x".into()));
assert_eq!(backend1.get(&"y".into()), backend2.get(&"y".into()));
assert_eq!(backend1.get(&"z".into()), backend2.get(&"z".into()));
}
#[test]
fn test_sequential_layer_determinism() {
let event_a = make_lww_event(1, vec![("x", "layer1_a")]);
let event_b = make_lww_event(2, vec![("x", "layer1_b")]);
let event_c = make_lww_event(3, vec![("x", "layer2_c")]);
let event_d = make_lww_event(4, vec![("x", "layer2_d")]);
let backend1 = LWWBackend::new();
backend1.apply_layer(&layer_from_refs(&[], &[&event_a, &event_b])).unwrap();
backend1.apply_layer(&layer_from_refs(&[&event_a, &event_b], &[&event_c, &event_d])).unwrap();
let backend2 = LWWBackend::new();
backend2.apply_layer(&layer_from_refs(&[], &[&event_b, &event_a])).unwrap();
backend2.apply_layer(&layer_from_refs(&[&event_b, &event_a], &[&event_d, &event_c])).unwrap();
assert_eq!(backend1.get(&"x".into()), backend2.get(&"x".into()));
}
}
#[cfg(test)]
mod edge_case_tests {
use super::*;
fn empty_layer() -> EventLayer { EventLayer::new(Vec::new(), Vec::new(), Arc::new(BTreeMap::new())) }
#[test]
fn test_empty_layer_application() {
let backend = LWWBackend::new();
let init_event = make_lww_event(1, vec![("x", "initial")]);
backend.apply_layer(&layer_from_refs(&[], &[&init_event])).unwrap();
backend.apply_layer(&empty_layer()).unwrap();
assert_eq!(backend.get(&"x".into()), Some(Value::String("initial".into())));
}
#[test]
fn test_event_with_no_lww_operations() {
let backend = LWWBackend::new();
let init_event = make_lww_event(1, vec![("x", "initial")]);
backend.apply_layer(&layer_from_refs(&[], &[&init_event])).unwrap();
let entity_id = EntityId::from_bytes([99u8; 16]);
let empty_event = Event {
entity_id,
collection: "test".into(),
parent: Clock::default(),
operations: OperationSet(BTreeMap::new()), };
let already_applied: Vec<&Event> = vec![];
let to_apply: Vec<&Event> = vec![&empty_event];
backend.apply_layer(&layer_from_refs_with_context(&already_applied, &to_apply, &[&init_event])).unwrap();
assert_eq!(backend.get(&"x".into()), Some(Value::String("initial".into())));
}
#[test]
fn test_same_event_in_both_lists() {
let backend = LWWBackend::new();
let event_a = make_lww_event(1, vec![("x", "value_a")]);
let already_applied: Vec<&Event> = vec![&event_a];
let to_apply: Vec<&Event> = vec![&event_a];
backend.apply_layer(&layer_from_refs(&already_applied, &to_apply)).unwrap();
assert_eq!(backend.get(&"x".into()), Some(Value::String("value_a".into())));
}
#[test]
fn test_many_concurrent_events() {
let backend = LWWBackend::new();
let events: Vec<Event> = (0..10).map(|i| make_lww_event(i as u8, vec![("x", &format!("value_{}", i))])).collect();
let event_refs: Vec<&Event> = events.iter().collect();
backend.apply_layer(&layer_from_refs(&[], &event_refs)).unwrap();
let winner_idx = events.iter().enumerate().max_by_key(|(_, e)| e.id()).map(|(i, _)| i).unwrap();
let expected = format!("value_{}", winner_idx);
assert_eq!(backend.get(&"x".into()), Some(Value::String(expected)));
}
#[test]
fn test_property_deletion() {
let backend = LWWBackend::new();
let init_event = make_lww_event(1, vec![("x", "initial")]);
backend.apply_layer(&layer_from_refs(&[], &[&init_event])).unwrap();
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = 1;
let entity_id = EntityId::from_bytes(entity_id_bytes);
let delete_backend = LWWBackend::new();
delete_backend.set("x".into(), None); let ops = delete_backend.to_operations().unwrap().unwrap();
let delete_event = Event {
entity_id,
collection: "test".into(),
parent: Clock::default(),
operations: OperationSet(BTreeMap::from([("lww".to_string(), ops)])),
};
backend.apply_layer(&layer_from_refs_with_context(&[], &[&delete_event], &[&init_event])).unwrap();
let expected = if delete_event.id() > init_event.id() { None } else { Some(Value::String("initial".into())) };
assert_eq!(backend.get(&"x".into()), expected);
}
#[test]
fn test_stored_last_write_competes() {
let backend = LWWBackend::new();
let init_event = make_lww_event(1, vec![("x", "initial")]);
backend.apply_layer(&layer_from_refs(&[], &[&init_event])).unwrap();
let challenger_event = make_lww_event(2, vec![("x", "challenger")]);
backend.apply_layer(&layer_from_refs_with_context(&[], &[&challenger_event], &[&init_event])).unwrap();
let expected = if challenger_event.id() > init_event.id() {
Some(Value::String("challenger".into()))
} else {
Some(Value::String("initial".into()))
};
assert_eq!(backend.get(&"x".into()), expected);
}
#[test]
fn test_event_id_tracking_after_layer() {
let backend = LWWBackend::new();
let event_a = make_lww_event(1, vec![("x", "value_a")]);
backend.apply_layer(&layer_from_refs(&[], &[&event_a])).unwrap();
let tracked_id = backend.get_event_id(&"x".into());
assert!(tracked_id.is_some());
assert_eq!(tracked_id.unwrap(), event_a.id());
}
}
#[cfg(test)]
mod phase4_stored_below_meet {
use super::*;
#[test]
fn test_stored_event_id_below_meet_loses_to_layer_candidate() {
let backend = LWWBackend::new();
let old_event = make_lww_event(1, vec![("x", "old_value")]);
let old_event_id = old_event.id();
{
let dag = BTreeMap::from([(old_event_id.clone(), vec![])]);
let layer = EventLayer::new(vec![], vec![old_event.clone()], Arc::new(dag));
backend.apply_layer(&layer).unwrap();
}
assert_eq!(backend.get(&"x".into()), Some(Value::String("old_value".into())));
assert_eq!(backend.get_event_id(&"x".into()), Some(old_event_id.clone()));
let new_event = make_lww_event(2, vec![("x", "new_value")]);
let new_event_id = new_event.id();
let dag = BTreeMap::from([(new_event_id.clone(), vec![])]);
let layer = EventLayer::new(vec![], vec![new_event.clone()], Arc::new(dag));
backend.apply_layer(&layer).unwrap();
assert_eq!(
backend.get(&"x".into()),
Some(Value::String("new_value".into())),
"Layer candidate must beat stored value whose event_id is below the meet"
);
assert_eq!(backend.get_event_id(&"x".into()), Some(new_event_id), "Winning event_id must be the new event");
}
}
#[cfg(test)]
mod phase4_idempotency {
use super::*;
#[tokio::test]
async fn test_redelivery_of_historical_event_is_noop() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_b.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let entity_head = clock!(id_c);
let event_b_again = make_test_event(2, &[id_a]);
let result = compare(retriever.clone(), &Clock::from(vec![event_b_again.id()]), &entity_head, 100).await.unwrap();
let event_c_again = make_test_event(3, &[id_b]);
let result_c = compare(retriever.clone(), &Clock::from(vec![event_c_again.id()]), &entity_head, 100).await.unwrap();
assert_eq!(result_c.relation, AbstractCausalRelation::Equal, "Re-delivery of head event must return Equal");
assert!(
matches!(result.relation, AbstractCausalRelation::StrictAscends),
"Historical non-head event re-delivery should return StrictAscends, got {:?}",
result.relation
);
}
}
#[cfg(test)]
mod phase4_duplicate_creation {
use super::*;
use crate::entity::Entity;
use crate::error::MutationError;
fn make_creation_event(seed: u8) -> Event {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = 42; let entity_id = EntityId::from_bytes(entity_id_bytes);
let backend = LWWBackend::new();
backend.set("x".into(), Some(Value::String(format!("value_{}", seed))));
let ops = backend.to_operations().unwrap().unwrap();
Event {
entity_id,
collection: "test".into(),
parent: Clock::default(), operations: OperationSet(BTreeMap::from([("lww".to_string(), ops)])),
}
}
#[tokio::test]
async fn test_second_creation_event_rejected() {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = 42;
let entity_id = EntityId::from_bytes(entity_id_bytes);
let entity = Entity::create(entity_id, "test".into());
let mut retriever = MockRetriever::new();
let creation_event_1 = make_creation_event(1);
retriever.add_event(creation_event_1.clone());
let result = entity.apply_event(&retriever, &creation_event_1).await;
assert!(result.is_ok(), "First creation event should succeed");
assert!(result.unwrap(), "First creation event should return true (applied)");
assert!(!entity.head().is_empty(), "Entity head should be non-empty after creation");
let creation_event_2 = make_creation_event(2);
retriever.add_event(creation_event_2.clone());
let result = entity.apply_event(&retriever, &creation_event_2).await;
assert!(result.is_err(), "Second creation event should fail");
let err = result.unwrap_err();
assert!(matches!(err, MutationError::LineageError(crate::error::LineageError::Disjoint)), "Error should be Disjoint, got: {err:?}");
}
#[tokio::test]
async fn test_redelivery_of_same_creation_event_is_noop() {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = 42;
let entity_id = EntityId::from_bytes(entity_id_bytes);
let entity = Entity::create(entity_id, "test".into());
let mut retriever = MockRetriever::new();
let creation_event = make_creation_event(1);
retriever.add_event(creation_event.clone());
let result = entity.apply_event(&retriever, &creation_event).await;
assert!(result.is_ok() && result.unwrap(), "First apply should succeed");
let result = entity.apply_event(&retriever, &creation_event).await;
assert!(result.is_ok(), "Re-delivery of same creation event should not error");
assert!(!result.unwrap(), "Re-delivery should return false (no-op)");
}
}
#[cfg(test)]
mod phase4_budget_escalation {
use super::*;
#[tokio::test]
async fn test_budget_escalation_succeeds() {
let mut retriever = MockRetriever::new();
let mut ids: Vec<EventId> = Vec::new();
for i in 0..6u8 {
let parents = if i == 0 { vec![] } else { vec![ids[i as usize - 1].clone()] };
let ev = make_test_event(i + 1, &parents);
ids.push(ev.id());
retriever.add_event(ev);
}
let ancestor = clock!(ids[0]);
let descendant = clock!(ids[5]);
let result = compare(retriever.clone(), &descendant, &ancestor, 2).await.unwrap();
assert!(
matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }),
"Budget escalation (2 -> 8) should succeed for a 6-deep chain, got {:?}",
result.relation
);
let result = compare(retriever.clone(), &descendant, &ancestor, 1).await.unwrap();
assert!(
matches!(result.relation, AbstractCausalRelation::BudgetExceeded { .. }),
"Budget=1 (max 4) should fail for a 6-deep chain, got {:?}",
result.relation
);
}
}
#[cfg(test)]
mod phase4_toctou_retry {
#[test]
#[ignore = "Testing TOCTOU retry exhaustion requires a mock that modifies the entity \
head between comparison and the CAS attempt inside try_mutate. This requires: \
(1) a custom Retrieve that triggers a side-effect during get_event to modify \
entity state, (2) precise timing control over the entity's RwLock. The expected \
behavior is: after MAX_RETRIES (5) attempts where the head moves each time, \
apply_event returns Err(MutationError::TOCTOUAttemptsExhausted). This error \
variant exists and is returned at the end of the retry loop."]
fn test_toctou_retry_exhaustion_produces_clean_error() {
}
}
#[cfg(test)]
mod phase4_mixed_parent_merge {
use super::*;
use crate::event_dag::accumulator::EventAccumulator;
#[tokio::test]
async fn test_merge_event_with_parent_from_non_meet_branch() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_g = make_test_event(3, &[id_a.clone()]);
let id_g = ev_g.id();
retriever.add_event(ev_g);
let ev_c = make_test_event(4, &[id_b.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_e = make_test_event(5, &[id_c.clone(), id_g.clone()]);
let id_e = ev_e.id();
retriever.add_event(ev_e);
let mut accumulator = EventAccumulator::new(retriever.clone());
for id in [&id_b, &id_g, &id_c, &id_e] {
let event = retriever.get_event(id).await.unwrap();
accumulator.accumulate(&event);
}
let mut layers = accumulator.into_layers(vec![id_a.clone()], vec![id_b.clone()]);
let mut all_layers = Vec::new();
while let Some(layer) = layers.next().await.unwrap() {
all_layers.push(layer);
}
assert!(all_layers.len() >= 2, "Expected at least 2 layers for mixed-parent merge, got {}", all_layers.len());
let mut all_event_ids: Vec<EventId> = Vec::new();
for layer in &all_layers {
for e in &layer.already_applied {
all_event_ids.push(e.id());
}
for e in &layer.to_apply {
all_event_ids.push(e.id());
}
}
assert!(all_event_ids.contains(&id_b), "B should be in layers");
assert!(all_event_ids.contains(&id_g), "G should be in layers");
assert!(all_event_ids.contains(&id_c), "C should be in layers");
assert!(all_event_ids.contains(&id_e), "E (merge event) should be in layers");
let e_layer_idx = all_layers
.iter()
.position(|l| l.to_apply.iter().any(|ev| ev.id() == id_e) || l.already_applied.iter().any(|ev| ev.id() == id_e))
.expect("E must appear in some layer");
let g_layer_idx = all_layers
.iter()
.position(|l| l.to_apply.iter().any(|ev| ev.id() == id_g) || l.already_applied.iter().any(|ev| ev.id() == id_g))
.expect("G must appear in some layer");
let c_layer_idx = all_layers
.iter()
.position(|l| l.to_apply.iter().any(|ev| ev.id() == id_c) || l.already_applied.iter().any(|ev| ev.id() == id_c))
.expect("C must appear in some layer");
assert!(e_layer_idx > g_layer_idx, "Merge event E (layer {}) must be after G (layer {})", e_layer_idx, g_layer_idx);
assert!(e_layer_idx > c_layer_idx, "Merge event E (layer {}) must be after C (layer {})", e_layer_idx, c_layer_idx);
let b_in_already = all_layers.iter().any(|l| l.already_applied.iter().any(|ev| ev.id() == id_b));
assert!(b_in_already, "B should be in already_applied (current head ancestry)");
let g_in_to_apply = all_layers.iter().any(|l| l.to_apply.iter().any(|ev| ev.id() == id_g));
assert!(g_in_to_apply, "G should be in to_apply (not in current head ancestry)");
}
}
#[cfg(test)]
mod phase4_eager_storage_bfs {
use super::*;
#[tokio::test]
async fn test_eagerly_stored_events_discoverable_by_bfs() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_c = make_test_event(3, &[id_a.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_d = make_test_event(4, &[id_b.clone()]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_e = make_test_event(5, &[id_c.clone()]);
let id_e = ev_e.id();
retriever.add_event(ev_e);
let clock_d = clock!(id_d);
let clock_e = clock!(id_e);
let result = compare(retriever.clone(), &clock_d, &clock_e, 100).await.unwrap();
assert!(
matches!(result.relation, AbstractCausalRelation::DivergedSince { ref meet, .. } if meet == &vec![id_a.clone()]),
"BFS should discover meet at A via eagerly stored events, got {:?}",
result.relation
);
let accumulator = result.accumulator();
let dag = accumulator.dag();
assert!(dag.contains_key(&id_b), "BFS should have accumulated event B");
assert!(dag.contains_key(&id_c), "BFS should have accumulated event C");
assert!(dag.contains_key(&id_d), "BFS should have accumulated event D");
assert!(dag.contains_key(&id_e), "BFS should have accumulated event E");
let mut layers = result.into_layers(vec![id_d.clone()]).unwrap();
let mut layer_count = 0;
while let Some(layer) = layers.next().await.unwrap() {
layer_count += 1;
assert!(!layer.to_apply.is_empty() || !layer.already_applied.is_empty(), "Layer should not be empty");
}
assert!(layer_count > 0, "Should produce at least one layer from eagerly stored events");
}
}
#[cfg(test)]
mod bfs_revisit_bugs {
use super::*;
#[tokio::test]
async fn double_decrement_falsely_reports_strict_descends() {
let mut retriever = MockRetriever::new();
let ev_r = make_test_event(1, &[]);
let id_r = ev_r.id();
retriever.add_event(ev_r);
let ev_n = make_test_event(2, &[id_r.clone()]);
let id_n = ev_n.id();
retriever.add_event(ev_n);
let ev_d = make_test_event(3, &[id_r.clone()]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_a = make_test_event(4, &[id_n.clone()]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_c = (10u8..=255)
.map(|seed| make_test_event(seed, &[id_n.clone()]))
.find(|ev| ev.id() > id_n)
.expect("some seed must yield id(C) > id(N)");
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_b = make_test_event(5, &[id_c.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_s = make_test_event(6, &[id_a.clone(), id_b.clone()]);
let id_s = ev_s.id();
retriever.add_event(ev_s);
let subject = clock!(id_s);
let comparison = clock!(id_n, id_d);
let result = compare(retriever, &subject, &comparison, 100).await.unwrap();
match &result.relation {
AbstractCausalRelation::DivergedSince { meet, .. } => {
assert_eq!(meet, &vec![id_n.clone()], "meet should be exactly [N]");
}
other => panic!("S does NOT descend from concurrent tip D; expected DivergedSince {{ meet: [N] }} but got {:?}", other),
}
}
#[tokio::test]
async fn double_decrement_falsely_reports_strict_ascends() {
let mut retriever = MockRetriever::new();
let ev_r = make_test_event(1, &[]);
let id_r = ev_r.id();
retriever.add_event(ev_r);
let ev_n = make_test_event(2, &[id_r.clone()]);
let id_n = ev_n.id();
retriever.add_event(ev_n);
let ev_d = make_test_event(3, &[id_r.clone()]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_a = make_test_event(4, &[id_n.clone()]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_c = (10u8..=255)
.map(|seed| make_test_event(seed, &[id_n.clone()]))
.find(|ev| ev.id() > id_n)
.expect("some seed must yield id(C) > id(N)");
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_b = make_test_event(5, &[id_c.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_s = make_test_event(6, &[id_a.clone(), id_b.clone()]);
let id_s = ev_s.id();
retriever.add_event(ev_s);
let subject = clock!(id_n, id_d); let comparison = clock!(id_s);
let result = compare(retriever, &subject, &comparison, 100).await.unwrap();
match &result.relation {
AbstractCausalRelation::DivergedSince { meet, .. } => {
assert_eq!(meet, &vec![id_n.clone()], "meet should be exactly [N]");
}
other => panic!(
"comparison {{S}} does NOT reach concurrent tip D, so it cannot strictly ascend; expected DivergedSince {{ meet: [N] }} but got {:?}",
other
),
}
}
#[tokio::test]
async fn diamond_chain_completes_within_linear_budget() {
use std::collections::BTreeSet;
let mut retriever = MockRetriever::new();
const LEVELS: usize = 12;
let mut seed = 1u16;
let mut next_seed = move || {
let s = seed;
seed = seed.checked_add(1).expect("seed space exhausted");
s
};
let ev_r = make_test_event_u16(next_seed(), &[]);
let id_r = ev_r.id();
retriever.add_event(ev_r);
let mut prev = id_r.clone();
for _ in 0..LEVELS {
let ev_a = make_test_event_u16(next_seed(), &[prev.clone()]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_c = loop {
let candidate = make_test_event_u16(next_seed(), &[prev.clone()]);
if candidate.id() > prev {
break candidate;
}
};
let id_c = ev_c.id();
retriever.add_event(ev_c);
let ev_b = make_test_event_u16(next_seed(), &[id_c.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_j = make_test_event_u16(next_seed(), &[id_a.clone(), id_b.clone()]);
prev = ev_j.id();
retriever.add_event(ev_j);
}
let total_events = 1 + 4 * LEVELS;
let subject = clock!(prev); let comparison = clock!(id_r);
let result = compare(retriever, &subject, &comparison, 2 * total_events).await.unwrap();
match &result.relation {
AbstractCausalRelation::StrictDescends { chain } => {
let unique: BTreeSet<_> = chain.iter().collect();
assert_eq!(unique.len(), chain.len(), "chain must not contain duplicate events");
assert_eq!(chain.len(), total_events - 1, "chain covers every event except the comparison head R");
}
other => panic!("expected StrictDescends within a 2x-event-count budget (per-event, not per-path); got {:?}", other),
}
}
#[tokio::test]
async fn late_origin_propagation_yields_empty_meet() {
let mut retriever = MockRetriever::new();
let ev_m = make_test_event(1, &[]);
let id_m = ev_m.id();
retriever.add_event(ev_m);
let ev_c1 = make_test_event(2, &[id_m.clone()]);
let id_c1 = ev_c1.id();
retriever.add_event(ev_c1);
let ev_x = make_test_event(3, &[id_m.clone()]);
let id_x = ev_x.id();
retriever.add_event(ev_x);
let ev_c2 = make_test_event(4, &[id_x.clone()]);
let id_c2 = ev_c2.id();
retriever.add_event(ev_c2);
let ev_s = make_test_event(5, &[id_m.clone()]);
let id_s = ev_s.id();
retriever.add_event(ev_s);
let subject = clock!(id_s);
let comparison = clock!(id_c1, id_c2);
let result = compare(retriever, &subject, &comparison, 100).await.unwrap();
match &result.relation {
AbstractCausalRelation::DivergedSince { meet, .. } => {
assert_eq!(
meet,
&vec![id_m.clone()],
"meet should be [M]; an empty meet makes entity.rs re-layer from genesis and skips head tip removal"
);
}
other => panic!("expected DivergedSince {{ meet: [M] }}, got {:?}", other),
}
}
#[tokio::test]
async fn origin_stalled_at_processed_node_still_retires_head() {
let mut retriever = MockRetriever::new();
let ev_m = make_test_event(1, &[]);
let id_m = ev_m.id();
retriever.add_event(ev_m);
let ev_k = make_test_event(2, &[id_m.clone()]);
let id_k = ev_k.id();
retriever.add_event(ev_k);
let ev_c1 = make_test_event(3, &[id_k.clone()]);
let id_c1 = ev_c1.id();
retriever.add_event(ev_c1);
let ev_l = (10u16..=u16::MAX)
.map(|seed| make_test_event_u16(seed, &[id_k.clone()]))
.find(|ev| ev.id() > id_k)
.expect("some seed must yield id(L) > id(K)");
let id_l = ev_l.id();
retriever.add_event(ev_l);
let ev_h = make_test_event(4, &[id_l.clone()]);
let id_h = ev_h.id();
retriever.add_event(ev_h);
let ev_s = make_test_event(5, &[id_m.clone()]);
let id_s = ev_s.id();
retriever.add_event(ev_s);
let subject = clock!(id_s);
let comparison = clock!(id_c1, id_h);
let result = compare(retriever, &subject, &comparison, 100).await.unwrap();
match &result.relation {
AbstractCausalRelation::DivergedSince { meet, .. } => {
assert_eq!(meet, &vec![id_m.clone()], "meet should be [M] even though H's origin stalled at K");
}
other => panic!("expected DivergedSince {{ meet: [M] }}, got {:?}", other),
}
}
}
#[cfg(test)]
mod quick_check_disjoint_verify {
use super::*;
#[tokio::test]
async fn test_quick_check_disjoint_extra_root() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_x = make_test_event(3, &[]);
let id_x = ev_x.id();
retriever.add_event(ev_x);
let subject = clock!(id_b, id_x); let comparison = clock!(id_a);
let result = compare(retriever, &subject, &comparison, 100).await.unwrap();
assert!(
!matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }),
"subject {{B, X}} contains disjoint root X; must not be StrictDescends over {{A}}, got {:?}",
result.relation
);
}
#[tokio::test]
async fn test_quick_check_guard_falls_through_to_bfs() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_d = make_test_event(3, &[id_b.clone()]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let ev_c = make_test_event(4, &[id_a.clone()]);
let id_c = ev_c.id();
retriever.add_event(ev_c);
let subject = clock!(id_d);
let comparison = clock!(id_c);
let result = compare(retriever, &subject, &comparison, 100).await.unwrap();
match &result.relation {
AbstractCausalRelation::DivergedSince { meet, .. } => {
assert_eq!(meet, &vec![id_a.clone()], "meet should be [A]");
}
other => panic!("expected DivergedSince {{ meet: [A] }} via BFS fallthrough, got {:?}", other),
}
}
#[tokio::test]
async fn test_single_event_with_disjoint_extra_parent_is_not_strict_descends() {
let mut retriever = MockRetriever::new();
let ev_a = make_test_event(1, &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a);
let ev_b = make_test_event(2, &[id_a.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b);
let ev_x = make_test_event(3, &[]);
let id_x = ev_x.id();
retriever.add_event(ev_x);
let ev_d = make_test_event(4, &[id_b.clone(), id_x.clone()]);
let id_d = ev_d.id();
retriever.add_event(ev_d);
let subject = clock!(id_d);
let comparison = clock!(id_a);
let result = compare(retriever, &subject, &comparison, 100).await.unwrap();
assert!(
!matches!(result.relation, AbstractCausalRelation::StrictDescends { .. }),
"a graft event must not fast-forward over {{A}}, got {:?}",
result.relation
);
match &result.relation {
AbstractCausalRelation::DivergedSince { meet, .. } => {
assert_eq!(meet, &vec![id_a.clone()], "meet should be [A]");
}
other => panic!("expected DivergedSince {{ meet: [A] }}, got {:?}", other),
}
}
}
#[cfg(test)]
mod strict_descends_gap_jump {
use super::*;
use crate::entity::Entity;
use crate::event_dag::ordering::topo_sort_events;
use crate::property::backend::lww::LWWBackend;
use crate::property::backend::PropertyBackend;
use ankurah_proto::Attested;
fn read_lww(entity: &Entity, prop: &str) -> Option<Value> {
let state = entity.to_state().unwrap();
let buf = state.state_buffers.0.get("lww")?;
let backend = LWWBackend::from_state_buffer(buf).unwrap();
backend.get(&prop.into())
}
#[tokio::test]
async fn test_strict_descends_gap_jump_skips_ancestor_ops() {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = 42;
let entity_id = EntityId::from_bytes(entity_id_bytes);
let entity = Entity::create(entity_id, "test".into());
let mut retriever = MockRetriever::new();
let ev_a = make_lww_event_with_parent(1, vec![("p0", "genesis")], &[]);
let id_a = ev_a.id();
retriever.add_event(ev_a.clone());
assert!(ev_a.is_entity_create(), "A must be a creation event");
let ev_x = make_lww_event_with_parent(2, vec![("p1", "written_by_X")], &[id_a.clone()]);
let id_x = ev_x.id();
retriever.add_event(ev_x.clone());
let ev_b = make_lww_event_with_parent(3, vec![("p2", "written_by_B")], &[id_x.clone()]);
let id_b = ev_b.id();
retriever.add_event(ev_b.clone());
assert!(entity.apply_event(&retriever, &ev_a).await.unwrap(), "A should apply");
assert_eq!(entity.head(), Clock::from(vec![id_a.clone()]));
assert_eq!(read_lww(&entity, "p0"), Some(Value::String("genesis".into())));
let batch = vec![Attested::opt(ev_b.clone(), None), Attested::opt(ev_x.clone(), None)];
let sorted = topo_sort_events(batch).unwrap();
let sorted_ids: Vec<EventId> = sorted.iter().map(|e| e.payload.id()).collect();
assert_eq!(sorted_ids, vec![id_x.clone(), id_b.clone()], "sort must place parent X before child B");
for event in &sorted {
assert!(entity.apply_event(&retriever, &event.payload).await.unwrap(), "each event applies in causal order");
}
assert_eq!(entity.head(), Clock::from(vec![id_b.clone()]), "head advanced to B");
assert_eq!(read_lww(&entity, "p2"), Some(Value::String("written_by_B".into())), "B's op (p2) applied");
let p1 = read_lww(&entity, "p1");
assert_eq!(
p1,
Some(Value::String("written_by_X".into())),
"CONSISTENCY VIOLATION: head descends X but X's write (p1) is missing. p1={p1:?}"
);
}
}
#[cfg(test)]
mod comparison_property {
use super::*;
use std::collections::BTreeSet;
struct Rng(u32);
impl Rng {
fn next(&mut self) -> u32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
x
}
fn below(&mut self, n: usize) -> usize { (self.next() as usize) % n }
}
fn ancestry(parents: &BTreeMap<EventId, Vec<EventId>>, heads: &[EventId]) -> BTreeSet<EventId> {
let mut out = BTreeSet::new();
let mut stack: Vec<EventId> = heads.to_vec();
while let Some(id) = stack.pop() {
if !out.insert(id.clone()) {
continue;
}
if let Some(ps) = parents.get(&id) {
stack.extend(ps.iter().cloned());
}
}
out
}
fn to_antichain(parents: &BTreeMap<EventId, Vec<EventId>>, ids: &BTreeSet<EventId>) -> Vec<EventId> {
ids.iter()
.filter(|id| !ids.iter().any(|other| other != *id && ancestry(parents, std::slice::from_ref(other)).contains(*id)))
.cloned()
.collect()
}
#[derive(Debug)]
enum Expected {
Equal,
StrictDescends,
StrictAscends,
DivergedMeet(Vec<EventId>),
Disjoint,
}
fn oracle(parents: &BTreeMap<EventId, Vec<EventId>>, subject: &[EventId], comparison: &[EventId]) -> Expected {
let s_set: BTreeSet<EventId> = subject.iter().cloned().collect();
let c_set: BTreeSet<EventId> = comparison.iter().cloned().collect();
if s_set == c_set {
return Expected::Equal;
}
let s_cover = ancestry(parents, subject);
let c_cover = ancestry(parents, comparison);
if c_cover.is_subset(&s_cover)
&& s_cover.iter().filter(|id| parents.get(*id).is_some_and(|p| p.is_empty())).all(|root| c_cover.contains(root))
{
return Expected::StrictDescends;
}
if s_cover.is_subset(&c_cover) {
return Expected::StrictAscends;
}
let common: BTreeSet<EventId> = s_cover.intersection(&c_cover).cloned().collect();
if common.is_empty() {
return Expected::Disjoint;
}
if comparison.iter().any(|h| ancestry(parents, std::slice::from_ref(h)).intersection(&s_cover).next().is_none()) {
return Expected::DivergedMeet(vec![]);
}
Expected::DivergedMeet(to_antichain(parents, &common))
}
fn verdict_matches(expected: &Expected, actual: &AbstractCausalRelation<EventId>) -> bool {
match (expected, actual) {
(Expected::Equal, AbstractCausalRelation::Equal) => true,
(Expected::StrictDescends, AbstractCausalRelation::StrictDescends { .. }) => true,
(Expected::StrictAscends, AbstractCausalRelation::StrictAscends) => true,
(Expected::DivergedMeet(want), AbstractCausalRelation::DivergedSince { meet, .. }) => {
let mut got = meet.clone();
got.sort();
let mut want = want.clone();
want.sort();
got == want
}
(Expected::Disjoint, AbstractCausalRelation::Disjoint { .. }) => true,
_ => false,
}
}
#[tokio::test]
async fn randomized_dags_match_reachability_oracle() {
for dag_seed in 1u32..=300 {
let mut rng = Rng(dag_seed.wrapping_mul(0x9E37_79B9) | 1);
let n_events = 5 + rng.below(6);
let mut retriever = MockRetriever::new();
let mut ids: Vec<EventId> = Vec::new();
let mut parents_map: BTreeMap<EventId, Vec<EventId>> = BTreeMap::new();
for i in 0..n_events {
let parent_ids: Vec<EventId> = if i == 0 || rng.below(6) == 0 {
vec![]
} else {
let mut chosen = BTreeSet::new();
for _ in 0..(1 + rng.below(2)) {
chosen.insert(ids[rng.below(ids.len())].clone());
}
to_antichain(&parents_map, &chosen)
};
let ev = make_test_event_u16(i as u16 + 1, &parent_ids);
let id = ev.id();
parents_map.insert(id.clone(), parent_ids);
ids.push(id);
retriever.add_event(ev);
}
for _pair in 0..4 {
let mut pick_clock = |rng: &mut Rng| -> Vec<EventId> {
let mut set = BTreeSet::new();
for _ in 0..(1 + rng.below(3)) {
set.insert(ids[rng.below(ids.len())].clone());
}
to_antichain(&parents_map, &set)
};
let subject_ids = pick_clock(&mut rng);
let comparison_ids = pick_clock(&mut rng);
let subject = Clock::from(subject_ids.clone());
let comparison = Clock::from(comparison_ids.clone());
let result = compare(retriever.clone(), &subject, &comparison, 4 * n_events).await.unwrap();
let expected = oracle(&parents_map, &subject_ids, &comparison_ids);
assert!(
verdict_matches(&expected, &result.relation),
"verdict mismatch\n dag_seed={dag_seed}\n subject={subject_ids:?}\n comparison={comparison_ids:?}\n expected={expected:?}\n actual={:?}\n dag={parents_map:#?}",
result.relation
);
}
}
}
}
#[cfg(test)]
mod entity_change_batches {
use super::*;
use crate::changes::EntityChange;
use crate::entity::Entity;
use crate::property::backend::lww::LWWBackend;
use crate::property::backend::PropertyBackend;
use ankurah_proto::Attested;
fn lww_event_for(entity_id: EntityId, properties: Vec<(&str, &str)>, parent_ids: &[EventId]) -> Event {
let backend = LWWBackend::new();
for (name, value) in properties {
backend.set(name.into(), Some(Value::String(value.into())));
}
let ops = backend.to_operations().unwrap().unwrap();
Event {
entity_id,
collection: "test".into(),
operations: OperationSet(BTreeMap::from([("lww".to_string(), ops)])),
parent: Clock::from(parent_ids.to_vec()),
}
}
#[tokio::test]
async fn notification_accepts_batch_superseded_ancestors() {
let mut entity_id_bytes = [0u8; 16];
entity_id_bytes[0] = 77;
let entity_id = EntityId::from_bytes(entity_id_bytes);
let entity = Entity::create(entity_id, "test".into());
let mut retriever = MockRetriever::new();
let ev_a = lww_event_for(entity_id, vec![("p0", "genesis")], &[]);
retriever.add_event(ev_a.clone());
let ev_x = lww_event_for(entity_id, vec![("p1", "from_x")], &[ev_a.id()]);
retriever.add_event(ev_x.clone());
let ev_b = lww_event_for(entity_id, vec![("p2", "from_b")], &[ev_x.id()]);
retriever.add_event(ev_b.clone());
assert!(entity.apply_event(&retriever, &ev_a).await.unwrap());
assert!(entity.apply_event(&retriever, &ev_x).await.unwrap());
assert!(entity.apply_event(&retriever, &ev_b).await.unwrap());
assert_eq!(entity.head(), Clock::from(vec![ev_b.id()]));
let batch = vec![Attested::opt(ev_x.clone(), None), Attested::opt(ev_b.clone(), None)];
let change = EntityChange::new(entity.clone(), batch);
assert!(change.is_ok(), "superseded ancestor X must be acceptable in a batch notification: {:?}", change.err());
let stray = lww_event_for(entity_id, vec![("p9", "stray")], &[ev_a.id()]);
let bad = EntityChange::new(entity, vec![Attested::opt(stray, None)]);
assert!(bad.is_err(), "an event outside the head and unsuperseded in the batch must be rejected");
}
}