From 8a12c48e9bfe3e450caa2463a39c2b0a919e7520 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 5 Nov 2026 16:19:00 +0100 Subject: automerge pinned --- src/document/sync.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) (limited to 'src/document') diff --git a/src/document/sync.rs b/src/document/sync.rs index 688d455..19956c4 100644 --- a/src/document/sync.rs +++ b/src/document/sync.rs @@ -1187,6 +1187,109 @@ mod tests { assert!(!a.state.sent_hashes.contains(&news)); } + /// Automerge, as we rely on it: a message with no heads says the peer lost all it + /// had, and what we counted as its goes again (why a push never goes without heads: + /// `Received::pushed`, and an opening never goes empty: `leave_opening`). + #[test] + fn no_heads_say_the_peer_lost_everything() { + use crate::document::testing::change; + use automerge::{ROOT, transaction::Transactable}; + let (mut ours, mut theirs) = (Document::new(), Document::new()); + change(&mut ours, |doc| doc.put(ROOT, "early", 1).unwrap()); + let (mut a, _b) = in_step(&mut ours, &mut theirs); + let (now, bytes) = (Instant::now(), SyncBytes::default()); + // A change sent, not yet known to have arrived. + change(&mut ours, |doc| doc.put(ROOT, "news", 2).unwrap()); + a.generate(&ours, now, bytes).expect("the news"); + assert!(!a.state.sent_hashes.is_empty()); + + // From a session over an empty document: no heads. + let mut lost = session(); + let empty = lost + .generate(&Document::new(), now, bytes) + .expect("an opening"); + let empty = decode(&empty).unwrap(); + assert!(empty.heads().is_empty()); + a.receive(&mut ours, empty).unwrap(); + assert!(a.state.sent_hashes.is_empty(), "all of it goes again"); + } + + /// Automerge, as we rely on it: a change that came without its predecessor waits, + /// and what the receiver asks for (`need`) is that predecessor alone, not the + /// change it holds already (a lost push costs one ask: `swarm::relay`). + #[test] + fn a_lost_predecessor_is_asked_for_alone() { + use crate::document::testing::change; + use automerge::{ROOT, transaction::Transactable}; + let (mut ours, mut theirs) = (Document::new(), Document::new()); + change(&mut ours, |doc| doc.put(ROOT, "early", 1).unwrap()); + let (mut a, mut b) = in_step(&mut ours, &mut theirs); + let (now, bytes) = (Instant::now(), SyncBytes::default()); + + change(&mut ours, |doc| doc.put(ROOT, "lost", 2).unwrap()); + let lost = ours.get_heads()[0]; + a.push(&ours, 1 << 20).expect("pushed, and lost"); + a.assume_delivered(&ours.get_heads()); + a.send_succeeded(a.id()); + change(&mut ours, |doc| doc.put(ROOT, "later", 3).unwrap()); + let (heads, changes) = a.push(&ours, 1 << 20).expect("the later one"); + assert_eq!( + changes.len(), + 1, + "only the later one: the lost one counts as sent" + ); + + b.receive( + &mut theirs, + Received::pushed(heads, changes).expect("decodes"), + ) + .unwrap(); + assert!(theirs.get_change_by_hash(&lost).is_none()); + let ask = b.generate(&theirs, now, bytes).expect("an ask"); + assert_eq!(decode(&ask).unwrap().0.need, vec![lost]); + } + + /// Automerge, as we rely on it: a message taken back for a delta leaves Automerge as + /// if it hadn't been generated (`withdraw_for_delta` restores what it had sent): the + /// peer opened, our answer was the delta's message, so our heads never went. Once + /// the peer has the delta and says its heads are ours, Automerge still answers with + /// them (it hadn't answered yet), and the peer knows it holds all we have (a joining + /// node finishes its initial sync on that). + #[test] + fn after_a_delta_the_offerer_still_answers() { + use crate::document::testing::change; + use automerge::{ROOT, transaction::Transactable}; + let mut rng = crate::testrand::Rng::new(2); + let big: String = (0..1024) + .map(|_| format!("{:016x}", rng.next_u64())) + .collect(); + let mut ours = Document::new(); + change(&mut ours, |ours| ours.put(ROOT, "big", big).unwrap()); + let mut theirs = Document::new(); + let (mut a, mut b) = (session(), session()); + let (now, bytes) = (Instant::now(), SyncBytes::default()); + + // b opens (it has nothing); a's first message would carry everything. + let opening = b.generate(&theirs, now, bytes).expect("b opens"); + b.send_succeeded(b.id()); + a.receive(&mut ours, decode(&opening).unwrap()).unwrap(); + a.generate(&ours, now, bytes).expect("the changes"); + a.withdraw_for_delta(&ours, now); + + // The delta: b loads all of a's changes, as from a blob. + let before = theirs.get_heads(); + theirs.merge(&mut ours.clone()).unwrap(); + b.came_from_peer(&theirs, &before); + let has_all = b.generate(&theirs, now, bytes).expect("b says what it has"); + b.send_succeeded(b.id()); + a.receive(&mut ours, decode(&has_all).unwrap()).unwrap(); + let heads = a + .generate(&ours, now, bytes) + .expect("a answers, though b has all of it"); + b.receive(&mut theirs, decode(&heads).unwrap()).unwrap(); + assert!(b.have_all_theirs(&theirs)); + } + /// Two sessions exchanging messages until neither has anything to say. fn in_step(ours: &mut Document, theirs: &mut Document) -> (SyncSession, SyncSession) { let (mut a, mut b) = (session(), session()); -- cgit v1.3.1