aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-05 16:19:00 +0100
committertv <tv@krebsco.de>2026-11-05 16:19:00 +0100
commit8a12c48e9bfe3e450caa2463a39c2b0a919e7520 (patch)
tree5ea3a89a3a5fb4d810f19ab68608a2007ebb60c7
parent250f99172058f733de3215703173115695297878 (diff)
automerge pinned
-rw-r--r--src/document/sync.rs103
1 files changed, 103 insertions, 0 deletions
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());