use automerge::{AutomergeError, ReadDoc, transaction::Transactable};
use samod_core::network::ConnectionEvent;
use samod_test_harness::{Network, RunningDocIds};
fn init_logging() {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init();
}
#[test]
fn find_after_create() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice_Original");
let RunningDocIds { doc_id, actor_id } = network.samod(&alice).create_document();
network
.samod(&alice)
.with_document_by_actor(actor_id, |doc| {
let mut tx = doc.transaction();
tx.put(automerge::ROOT, "foo", "bar").unwrap();
tx.commit();
})
.expect("with_document should succeed");
network.run_until_quiescent();
let alice_storage = network.samod(&alice).storage().clone();
let alice2 = network.create_samod_with_storage("Alice_Second", alice_storage);
let actor_id2 = network.samod(&alice2).find_document(&doc_id).unwrap();
let result = {
network
.samod(&alice2)
.with_document_by_actor(actor_id2, |doc| {
doc.get(automerge::ROOT, "foo")
.unwrap()
.map(|(value, _)| match value {
automerge::Value::Scalar(s) => match s.as_ref() {
automerge::ScalarValue::Str(string) => string.to_string(),
_ => s.to_string(),
},
_ => value.to_string(),
})
.unwrap_or_default()
})
.expect("with_document should succeed")
};
assert_eq!(result, "bar");
}
#[test]
fn three_peer_chain_sync() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice");
let bob = network.create_samod("Bob");
let charlie = network.create_samod("Charlie");
network.connect(alice, bob);
network.connect(bob, charlie);
network.run_until_quiescent();
for (name, peer_id) in [("Alice", alice), ("Bob", bob), ("Charlie", charlie)] {
let events = network.samod(&peer_id).connection_events();
let handshake_completed = events
.iter()
.any(|event| matches!(event, ConnectionEvent::HandshakeCompleted { .. }));
assert!(handshake_completed, "{name}'s handshake should complete");
}
let RunningDocIds { doc_id, actor_id } = network.samod(&charlie).create_document();
let result = network
.samod(&charlie)
.with_document_by_actor(actor_id, |doc| {
let mut tx = doc.transaction();
tx.put(automerge::ROOT, "creator", "charlie").unwrap();
tx.put(automerge::ROOT, "message", "hello from charlie")
.unwrap();
tx.commit();
"change_applied"
})
.unwrap();
assert_eq!(result, "change_applied");
network.run_until_quiescent();
let alice_actor_id = network.samod(&alice).find_document(&doc_id);
assert!(
alice_actor_id.is_some(),
"Alice should find the document through Bob from Charlie"
);
let alice_actor_id = alice_actor_id.unwrap();
let verification_result = network
.samod(&alice)
.with_document_by_actor(alice_actor_id, |doc| {
let creator = doc
.get(automerge::ROOT, "creator")
.unwrap()
.map(|(value, _)| value.into_string().unwrap())
.unwrap_or_default();
let message = doc
.get(automerge::ROOT, "message")
.unwrap()
.map(|(value, _)| value.into_string().unwrap())
.unwrap_or_default();
(creator, message)
})
.expect("with_document should succeed");
assert_eq!(verification_result.0, "charlie");
assert_eq!(verification_result.1, "hello from charlie");
let bob_actor_id = network.samod(&bob).find_document(&doc_id);
assert!(bob_actor_id.is_some(), "Bob should also have the document");
}
#[test]
fn document_persistence_across_restart() {
init_logging();
let mut network = Network::new();
let alice_original = network.create_samod("Alice_Original");
let bob = network.create_samod("Bob");
let RunningDocIds { doc_id, actor_id } = network.samod(&alice_original).create_document();
let result = {
network
.samod(&alice_original)
.with_document_by_actor(actor_id, |doc| {
let mut tx = doc.transaction();
tx.put(automerge::ROOT, "persistent_data", "survives_restart")
.unwrap();
tx.commit();
"data_added"
})
.expect("with_document should succeed")
};
assert_eq!(result, "data_added");
let alice_storage = network.samod(&alice_original).storage().clone();
let alice_restarted = network.create_samod_with_storage("Alice_Restarted", alice_storage);
network.connect(alice_restarted, bob);
network.run_until_quiescent();
let alice_events = network.samod(&alice_restarted).connection_events();
let bob_events = network.samod(&bob).connection_events();
let alice_handshake_completed = alice_events
.iter()
.any(|event| matches!(event, ConnectionEvent::HandshakeCompleted { .. }));
let bob_handshake_completed = bob_events
.iter()
.any(|event| matches!(event, ConnectionEvent::HandshakeCompleted { .. }));
assert!(
alice_handshake_completed,
"Restarted Alice's handshake should complete"
);
assert!(bob_handshake_completed, "Bob's handshake should complete");
let bob_actor_id = network
.samod(&bob)
.find_document(&doc_id)
.expect("document should be found on bob");
let verification_result = network
.samod(&bob)
.with_document_by_actor(bob_actor_id, |doc| {
println!("🔍 Bob's document keys: {:?}", doc.keys(automerge::ROOT));
doc
.get(automerge::ROOT, "persistent_data")
.unwrap()
.map(|(value, _)| match value {
automerge::Value::Scalar(s) => match s.as_ref() {
automerge::ScalarValue::Str(string) => string.to_string(),
_ => s.to_string(),
},
_ => value.to_string(),
})
.unwrap_or_default()
})
.expect("with_document should succeed");
assert_eq!(verification_result, "survives_restart");
}
#[test]
fn unavailable_document_multiple_peers() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice");
let bob = network.create_samod("Bob");
let charlie = network.create_samod("Charlie");
network.connect(alice, bob);
network.connect(alice, charlie);
network.connect(bob, charlie);
network.run_until_quiescent();
println!("✅ All three peers connected to each other");
let fake_doc_id = samod_core::DocumentId::new(&mut rand::rng());
let alice_result = network.samod(&alice).find_document(&fake_doc_id);
assert!(alice_result.is_none(), "Document should not be found");
}
#[test]
fn unavailable_document_single_peer() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice");
let fake_doc_id = samod_core::DocumentId::new(&mut rand::rng());
let alice_result = network.samod(&alice).find_document(&fake_doc_id);
assert!(alice_result.is_none(), "Document should not be found");
}
#[test]
fn request_document_before_connection() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice");
let bob = network.create_samod("Bob");
let RunningDocIds { doc_id, actor_id } = network.samod(&alice).create_document();
let result = network
.samod(&alice)
.with_document_by_actor(actor_id, |doc| {
let mut tx = doc.transaction();
tx.put(
automerge::ROOT,
"delayed_sync",
"should_work_after_connection",
)
.unwrap();
tx.commit();
"data_added"
})
.expect("with document should succeed");
assert_eq!(result, "data_added");
let bob_result = network.samod(&bob).find_document(&doc_id);
assert!(
bob_result.is_none(),
"document should not be found before connection"
);
network.connect(alice, bob);
network.run_until_quiescent();
let alice_events = network.samod(&alice).connection_events();
let bob_events = network.samod(&bob).connection_events();
let alice_handshake_completed = alice_events
.iter()
.any(|event| matches!(event, ConnectionEvent::HandshakeCompleted { .. }));
let bob_handshake_completed = bob_events
.iter()
.any(|event| matches!(event, ConnectionEvent::HandshakeCompleted { .. }));
assert!(
alice_handshake_completed,
"Alice's handshake should complete"
);
assert!(bob_handshake_completed, "Bob's handshake should complete");
let bob_actor_id = network
.samod(&bob)
.find_document(&doc_id)
.expect("document should be found on bob after connection");
let verification_result = network
.samod(&bob)
.with_document_by_actor(bob_actor_id, |doc| {
doc
.get(automerge::ROOT, "delayed_sync")
.unwrap()
.map(|(value, _)| match value {
automerge::Value::Scalar(s) => match s.as_ref() {
automerge::ScalarValue::Str(string) => string.to_string(),
_ => s.to_string(),
},
_ => value.to_string(),
})
.unwrap_or_default()
})
.expect("with_document should succeed");
assert_eq!(verification_result, "should_work_after_connection");
}
#[test]
fn peer_with_announce_policy_set_to_true_should_announce_to_peers() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice");
let bob = network.create_samod("Bob");
network.connect(alice, bob);
network.run_until_quiescent();
let RunningDocIds { doc_id, actor_id } = network.samod(&alice).create_document();
network
.samod(&alice)
.with_document_by_actor(actor_id, |doc| {
doc.transact::<_, _, AutomergeError>(|tx| {
tx.put(automerge::ROOT, "foo", "bar")?;
Ok(())
})
.unwrap()
})
.unwrap();
network.run_until_quiescent();
network.disconnect(alice, bob);
assert!(network.samod(&bob).find_document(&doc_id).is_some());
}
#[test]
fn peer_with_announce_policy_set_to_false_does_not_announce() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice");
network
.samod(&alice)
.set_announce_policy(Box::new(|_doc_id, _peer_id| false));
let bob = network.create_samod("Bob");
network.connect(alice, bob);
network.run_until_quiescent();
let RunningDocIds { doc_id, actor_id } = network.samod(&alice).create_document();
network
.samod(&alice)
.with_document_by_actor(actor_id, |doc| {
doc.transact::<_, _, AutomergeError>(|tx| {
tx.put(automerge::ROOT, "foo", "bar")?;
Ok(())
})
.unwrap()
})
.unwrap();
network.run_until_quiescent();
network.disconnect(alice, bob);
assert!(network.samod(&bob).find_document(&doc_id).is_none());
}
#[test]
fn peer_with_announce_policy_set_to_false_does_not_request() {
init_logging();
let mut network = Network::new();
let alice = network.create_samod("Alice");
network
.samod(&alice)
.set_announce_policy(Box::new(|_doc_id, _peer_id| false));
let bob = network.create_samod("Bob");
network
.samod(&bob)
.set_announce_policy(Box::new(|_, _| false));
network.connect(alice, bob);
network.run_until_quiescent();
let RunningDocIds { doc_id, actor_id } = network.samod(&alice).create_document();
network
.samod(&alice)
.with_document_by_actor(actor_id, |doc| {
doc.transact::<_, _, AutomergeError>(|tx| {
tx.put(automerge::ROOT, "foo", "bar")?;
Ok(())
})
.unwrap()
})
.unwrap();
network.run_until_quiescent();
assert!(network.samod(&bob).find_document(&doc_id).is_none());
}