Skip to main content

blockworx_doc/
reconcile.rs

1//! The reconciliation suite: one [`Host`](crate::session::Host),
2//! N [`ClientSession`](crate::session::ClientSession)s, and
3//! explicit message queues.
4//!
5//! Convergence alone proves nothing here: everyone folds one canonical
6//! linearization, so it holds however the merge rule compares. What can
7//! actually be wrong is the client's optimistic layer — rebuilds against
8//! a moving confirmed head, ack/broadcast interleavings, queue discipline
9//! — and the merge rule itself, which needs an independent oracle rather
10//! than agreement (see `assert_log_semantics`).
11//!
12//! Interleaving is randomized **across** queues and never within one:
13//! per-connection FIFO is the transport's guarantee, and the sessions
14//! assert contiguity against it.
15
16#[cfg(test)]
17mod tests {
18    use std::collections::VecDeque;
19
20    use proptest::prelude::*;
21    use uuid::Uuid;
22
23    use crate::{
24        block_model::{BlockInit, BlockUpdate, Icon, LabelInit, LabelUpdate},
25        commit::{Commit, CommitEnvelope},
26        document::{Document, TitleBlockUpdate},
27        fixtures::{commit, projection},
28        geometry::{FracVal, GridPoint, GridRect, GridSize},
29        id::BlockId,
30        opcode::{Crud, OpCodes},
31        protocol::{Nonce, ServerMsg},
32        rev::{Confirmed, Rev},
33        session::{ClientSession, Host},
34        values::{LabelSide, Role},
35    };
36
37    const CLIENTS: usize = 3;
38
39    struct Client {
40        session: ClientSession,
41        /// Submitted, in flight toward the host.
42        outbox: VecDeque<(Nonce, Commit)>,
43        /// Sequenced, in flight toward this client. FIFO, always.
44        inbox: VecDeque<ServerMsg>,
45        /// The harness's own shadow of the session's pending queue. Kept
46        /// independently so the oracle never consults the thing it checks.
47        pending: VecDeque<(Nonce, Commit)>,
48        next_block: u8,
49    }
50
51    struct World {
52        host: Host,
53        clients: Vec<Client>,
54    }
55
56    /// Distinct per client, so concurrent creates cannot collide.
57    fn block_of(client: usize, nth: u8) -> BlockId {
58        let mut bytes = [0u8; 16];
59        bytes[0] = u8::try_from(client).expect("few clients");
60        bytes[1] = nth;
61        // Never the nil uuid, which means "the document" as a parent.
62        bytes[2] = 0xb1;
63        BlockId::from_uuid(Uuid::from_bytes(bytes))
64    }
65
66    fn rect(x: i32) -> GridRect {
67        GridRect {
68            top_left: GridPoint { x, y: 0 },
69            size: GridSize { w: 4, h: 4 },
70        }
71    }
72
73    fn label_init(name: &str) -> LabelInit {
74        LabelInit {
75            name: name.into(),
76            side: LabelSide::Bottom,
77            offset: FracVal::from(1.5),
78            hidden: false,
79        }
80    }
81
82    fn block_init(parent: BlockId, name: &str) -> BlockInit {
83        BlockInit {
84            parent,
85            rect: rect(0),
86            locked: false,
87            role: Role::default(),
88            title: label_init(name),
89            type_label: label_init("kind"),
90            icon: Icon::default(),
91        }
92    }
93
94    fn create_block(id: BlockId, name: &str) -> OpCodes {
95        OpCodes::Block(id, Crud::Create(block_init(BlockId::NULL, name)))
96    }
97
98    fn rename(id: BlockId, name: &str) -> OpCodes {
99        OpCodes::Block(
100            id,
101            Crud::Update(BlockUpdate::Title(LabelUpdate::Name(name.into()))),
102        )
103    }
104
105    fn move_to(id: BlockId, x: i32) -> OpCodes {
106        OpCodes::Block(id, Crud::Update(BlockUpdate::Rect(rect(x))))
107    }
108
109    fn reparent(child: BlockId, parent: BlockId) -> OpCodes {
110        OpCodes::Block(child, Crud::Update(BlockUpdate::Parent(parent)))
111    }
112
113    impl World {
114        fn new() -> Self {
115            Self {
116                host: Host::default(),
117                clients: (0..CLIENTS)
118                    .map(|_| Client {
119                        session: ClientSession::default(),
120                        outbox: VecDeque::new(),
121                        inbox: VecDeque::new(),
122                        pending: VecDeque::new(),
123                        next_block: 0,
124                    })
125                    .collect(),
126            }
127        }
128
129        /// Seal, journal, predict, and put on the wire. Returns false if
130        /// the client refused its own edit, which the random pool is
131        /// built never to provoke.
132        fn edit(&mut self, client: usize, commit: Commit) -> bool {
133            let session = &mut self.clients[client].session;
134            let Ok(nonce) = session.submit(commit) else {
135                return false;
136            };
137            let outbound = session
138                .last_submission()
139                .expect("a submission is queued")
140                .1
141                .clone();
142            self.clients[client]
143                .outbox
144                .push_back((nonce, outbound.clone()));
145            self.clients[client].pending.push_back((nonce, outbound));
146            true
147        }
148
149        /// The host takes this client's oldest in-flight submission.
150        fn sequence(&mut self, client: usize) {
151            let Some((nonce, commit)) = self.clients[client].outbox.pop_front() else {
152                return;
153            };
154            match self.host.ingest(&commit) {
155                Ok(rev) => {
156                    self.clients[client]
157                        .inbox
158                        .push_back(ServerMsg::Committed { nonce, rev });
159                    for (ix, other) in self.clients.iter_mut().enumerate() {
160                        if ix != client {
161                            other.inbox.push_back(ServerMsg::Apply {
162                                rev,
163                                commit: CommitEnvelope::CommitV1(commit.clone()),
164                            });
165                        }
166                    }
167                }
168                Err(refusal) => self.clients[client].inbox.push_back(ServerMsg::Rejected {
169                    nonce,
170                    reason: refusal.to_string(),
171                }),
172            }
173        }
174
175        fn deliver(&mut self, client: usize, count: usize) {
176            for _ in 0..count {
177                let Some(message) = self.clients[client].inbox.pop_front() else {
178                    return;
179                };
180                let client = &mut self.clients[client];
181                match message {
182                    ServerMsg::Committed { nonce, rev } => {
183                        client
184                            .session
185                            .committed(nonce, rev)
186                            .expect("an ack for the front of the queue lands");
187                        let (shadow, _) = client.pending.pop_front().expect("a pending entry");
188                        assert_eq!(shadow, nonce, "answers arrive in submission order");
189                    }
190                    ServerMsg::Rejected { nonce, .. } => {
191                        client.session.rejected(nonce).expect("the rejection lands");
192                        let (shadow, _) = client.pending.pop_front().expect("a pending entry");
193                        assert_eq!(shadow, nonce);
194                    }
195                    ServerMsg::Apply { rev, commit } => {
196                        let CommitEnvelope::CommitV1(commit) = commit;
197                        client
198                            .session
199                            .apply_foreign(rev, &commit)
200                            .expect("a sequenced commit lands");
201                    }
202                    // Every client here connects before the first commit, so
203                    // there is no reconnect path to drive; the real one is
204                    // covered by `a_late_joiner_folds_the_log_and_converges`.
205                    ServerMsg::Welcome { .. } => panic!("this harness never reconnects"),
206                }
207            }
208        }
209
210        fn quiesce(&mut self) {
211            for client in 0..CLIENTS {
212                while !self.clients[client].outbox.is_empty() {
213                    self.sequence(client);
214                }
215            }
216            for client in 0..CLIENTS {
217                let waiting = self.clients[client].inbox.len();
218                self.deliver(client, waiting);
219            }
220        }
221
222        /// **The oracle.** Rebuilt from the *host's* log rather than from
223        /// the session's incrementally maintained `confirmed`, so a session
224        /// that mistracks its head is caught rather than agreed with.
225        fn assert_oracle(&self) {
226            for (ix, client) in self.clients.iter().enumerate() {
227                let confirmed_count =
228                    usize::try_from(client.session.rev().get()).expect("the log fits in memory");
229                let mut replayed = Document::<Confirmed>::default();
230                for commit in &self.host.commits_after(Rev::ZERO)[..confirmed_count] {
231                    replayed = replayed
232                        .try_apply(commit)
233                        .expect("a sequenced commit folds on replay");
234                }
235                let mut predicted = replayed.predict();
236                for (_, commit) in &client.pending {
237                    predicted = predicted
238                        .try_apply(commit)
239                        .expect("the random pool never authors a commit that cannot fold");
240                }
241                assert_eq!(
242                    client.session.optimistic().content_hash(),
243                    predicted.content_hash(),
244                    "client {ix}: optimistic != confirmed-log ⊕ pending",
245                );
246            }
247        }
248
249        /// **The semantic oracle.** Convergence and the prediction oracle
250        /// are both blind to the merge *rule*: everyone folds one canonical
251        /// linearization, so `f(x) == f(x)` holds however `Register::apply`
252        /// compares. Replaying the log with dumb sequential last-wins
253        /// bookkeeping — plain assignment, no `WriteOrder`, no `Register` —
254        /// is an independent implementation to disagree with.
255        fn assert_log_semantics(&self) {
256            let mut title = String::new();
257            let mut blocks: std::collections::BTreeMap<BlockId, (GridRect, String, bool)> =
258                std::collections::BTreeMap::new();
259
260            for commit in self.host.commits_after(Rev::ZERO) {
261                for op in commit.ops() {
262                    match op {
263                        OpCodes::Document(TitleBlockUpdate::Name(name)) => title = name.clone(),
264                        OpCodes::Block(id, Crud::Create(init)) => {
265                            blocks.insert(*id, (init.rect, init.title.name.clone(), true));
266                        }
267                        OpCodes::Block(id, Crud::Update(BlockUpdate::Rect(to))) => {
268                            blocks.entry(*id).or_default().0 = *to;
269                        }
270                        OpCodes::Block(
271                            id,
272                            Crud::Update(BlockUpdate::Title(LabelUpdate::Name(name))),
273                        ) => {
274                            blocks.entry(*id).or_default().1 = name.clone();
275                        }
276                        OpCodes::Block(id, Crud::Delete) => {
277                            blocks.entry(*id).or_default().2 = false;
278                        }
279                        OpCodes::Block(id, Crud::Restore) => {
280                            blocks.entry(*id).or_default().2 = true;
281                        }
282                        _ => panic!("the random pool authored an op this oracle cannot replay"),
283                    }
284                }
285            }
286
287            let expected: Vec<_> = blocks
288                .into_iter()
289                .filter(|(_, (_, _, alive))| *alive)
290                .map(|(id, (rect, name, _))| (id, BlockId::NULL, rect, name))
291                .collect();
292            assert_eq!(
293                projection(self.host.state()),
294                expected,
295                "the fold disagrees with last-write-wins over the log",
296            );
297            assert_eq!(
298                self.host.state().title_block().name.as_ref(),
299                &title,
300                "the singleton's register disagrees with the log's last write",
301            );
302        }
303
304        fn assert_converged(&self) {
305            let host = self.host.state().content_hash();
306            for (ix, client) in self.clients.iter().enumerate() {
307                assert!(client.pending.is_empty(), "client {ix} is not quiesced");
308                assert_eq!(
309                    client.session.confirmed().content_hash(),
310                    host,
311                    "client {ix}'s confirmed document differs from the host's",
312                );
313                assert_eq!(
314                    client.session.optimistic().content_hash(),
315                    host,
316                    "client {ix}'s prediction differs from the host's document",
317                );
318            }
319        }
320
321        fn block_ids(&self, client: usize) -> Vec<BlockId> {
322            let mut ids: Vec<_> = self.clients[client]
323                .session
324                .optimistic()
325                .blocks()
326                .map(|(id, _)| id)
327                .collect();
328            ids.sort();
329            ids
330        }
331
332        fn title_of(&self, client: usize, id: BlockId) -> String {
333            self.clients[client]
334                .session
335                .confirmed()
336                .block(&id)
337                .expect("the block is in the document")
338                .as_ref()
339                .title
340                .name
341                .as_ref()
342                .clone()
343        }
344    }
345
346    #[derive(Debug, Clone, Copy)]
347    enum Edit {
348        Create,
349        Move {
350            which: usize,
351            x: i32,
352        },
353        Rename {
354            which: usize,
355            name: u8,
356        },
357        Delete {
358            which: usize,
359        },
360        Restore {
361            which: usize,
362        },
363        Title {
364            name: u8,
365        },
366        /// One commit writing one register twice — the intra-commit
367        /// ordering case, exercised inside the random drive as well as in
368        /// its own scenario.
369        MoveTwice {
370            which: usize,
371            first: i32,
372            second: i32,
373        },
374    }
375
376    #[derive(Debug, Clone, Copy)]
377    enum Step {
378        Edit { client: usize, edit: Edit },
379        Sequence { client: usize },
380        Deliver { client: usize, count: usize },
381    }
382
383    impl World {
384        /// Every edit the pool authors is valid against any state it could
385        /// meet: fresh ids, existing targets, and no reparenting — the one
386        /// op a foreign commit can invalidate. Rejection is provoked
387        /// deliberately in its own scenario instead.
388        fn apply_edit(&mut self, client: usize, edit: Edit) {
389            let ids = self.block_ids(client);
390            let pick = |which: usize| ids[which % ids.len()];
391            let ops = match edit {
392                Edit::Create => None,
393                _ if ids.is_empty() => None,
394                Edit::Move { which, x } => Some(vec![move_to(pick(which), x)]),
395                Edit::Rename { which, name } => {
396                    Some(vec![rename(pick(which), &format!("n{name}"))])
397                }
398                Edit::Delete { which } => Some(vec![OpCodes::Block(pick(which), Crud::Delete)]),
399                Edit::Restore { which } => Some(vec![OpCodes::Block(pick(which), Crud::Restore)]),
400                Edit::Title { name } => Some(vec![OpCodes::Document(TitleBlockUpdate::Name(
401                    format!("doc{name}"),
402                ))]),
403                Edit::MoveTwice {
404                    which,
405                    first,
406                    second,
407                } => {
408                    let id = pick(which);
409                    Some(vec![move_to(id, first), move_to(id, second)])
410                }
411            };
412
413            let ops = ops.unwrap_or_else(|| {
414                let nth = self.clients[client].next_block;
415                self.clients[client].next_block = nth.wrapping_add(1);
416                vec![create_block(block_of(client, nth), "fresh")]
417            });
418            assert!(
419                self.edit(client, commit("edit", ops)),
420                "the random pool must never author an edit the client refuses",
421            );
422        }
423
424        fn step(&mut self, step: Step) {
425            match step {
426                Step::Edit { client, edit } => self.apply_edit(client, edit),
427                Step::Sequence { client } => self.sequence(client),
428                Step::Deliver { client, count } => self.deliver(client, count),
429            }
430        }
431    }
432
433    fn any_edit() -> impl Strategy<Value = Edit> {
434        prop_oneof![
435            2 => Just(Edit::Create),
436            3 => (any::<usize>(), -50i32..50).prop_map(|(which, x)| Edit::Move { which, x }),
437            3 => (any::<usize>(), any::<u8>()).prop_map(|(which, name)| Edit::Rename { which, name }),
438            1 => any::<usize>().prop_map(|which| Edit::Delete { which }),
439            1 => any::<usize>().prop_map(|which| Edit::Restore { which }),
440            1 => any::<u8>().prop_map(|name| Edit::Title { name }),
441            2 => (any::<usize>(), -50i32..50, -50i32..50)
442                .prop_map(|(which, first, second)| Edit::MoveTwice { which, first, second }),
443        ]
444    }
445
446    fn any_step() -> impl Strategy<Value = Step> {
447        prop_oneof![
448            3 => (0..CLIENTS, any_edit()).prop_map(|(client, edit)| Step::Edit { client, edit }),
449            3 => (0..CLIENTS).prop_map(|client| Step::Sequence { client }),
450            3 => (0..CLIENTS, 0usize..4).prop_map(|(client, count)| Step::Deliver { client, count }),
451        ]
452    }
453
454    proptest! {
455        /// The two properties that matter, checked together: the oracle
456        /// after **every** step (a prediction is never allowed to drift,
457        /// not merely to end up right), and byte-identical convergence
458        /// once the wires drain.
459        #[test]
460        fn predictions_reconcile_and_everyone_converges(steps in prop::collection::vec(any_step(), 1..80)) {
461            let mut world = World::new();
462            for step in steps {
463                world.step(step);
464                world.assert_oracle();
465            }
466            world.quiesce();
467            world.assert_oracle();
468            world.assert_converged();
469            world.assert_log_semantics();
470        }
471    }
472
473    /// A semantic oracle: not "they agree" but *which value won*. A fold
474    /// that dropped every write would agree with itself perfectly.
475    #[test]
476    fn the_later_commit_in_server_order_wins_the_register_everywhere() {
477        let mut world = World::new();
478        let id = block_of(0, 0);
479        world.edit(0, commit("create", vec![create_block(id, "original")]));
480        world.quiesce();
481
482        // Both clients rename the same register, neither having seen the
483        // other — the concurrent write the whole design is about.
484        world.edit(0, commit("rename", vec![rename(id, "from client 0")]));
485        world.edit(1, commit("rename", vec![rename(id, "from client 1")]));
486        assert_eq!(
487            world.title_of(0, id),
488            "original",
489            "neither rename is confirmed yet",
490        );
491
492        // The host takes client 1's first, so client 0's is the later rev.
493        world.sequence(1);
494        world.sequence(0);
495        world.quiesce();
496
497        for client in 0..CLIENTS {
498            assert_eq!(
499                world.title_of(client, id),
500                "from client 0",
501                "client {client} must show the value the later rev wrote",
502            );
503        }
504        world.assert_converged();
505    }
506
507    /// The losing client's own value stands in its prediction until the
508    /// winning commit is confirmed — the Figma suppression rule, which is
509    /// the reason unacked writes sort above confirmed ones.
510    #[test]
511    fn an_unacked_local_write_outranks_an_incoming_confirmed_one() {
512        let mut world = World::new();
513        let id = block_of(0, 0);
514        world.edit(0, commit("create", vec![create_block(id, "original")]));
515        world.quiesce();
516
517        world.edit(1, commit("rename", vec![rename(id, "local")]));
518        world.edit(0, commit("rename", vec![rename(id, "remote")]));
519        world.sequence(0);
520        world.deliver(1, 1);
521
522        assert_eq!(
523            world.clients[1]
524                .session
525                .optimistic()
526                .block(&id)
527                .expect("the block is present")
528                .as_ref()
529                .title
530                .name
531                .as_ref()
532                .as_str(),
533            "local",
534            "the unacked local edit still suppresses the confirmed one",
535        );
536        assert_eq!(
537            world.title_of(1, id),
538            "remote",
539            "while the confirmed document has already taken the foreign value",
540        );
541
542        world.quiesce();
543        world.assert_converged();
544    }
545
546    /// Intra-commit ordering, end to end through a session: `seq` resolves
547    /// a commit that writes one register twice, and every client sees the
548    /// last write.
549    #[test]
550    fn a_commit_writing_one_register_twice_ends_on_its_last_write() {
551        let mut world = World::new();
552        let id = block_of(0, 0);
553        world.edit(0, commit("create", vec![create_block(id, "b")]));
554        world.quiesce();
555
556        world.edit(0, commit("drag", vec![move_to(id, 5), move_to(id, 9)]));
557        world.quiesce();
558
559        for client in 0..CLIENTS {
560            assert_eq!(
561                *world.clients[client]
562                    .session
563                    .confirmed()
564                    .block(&id)
565                    .expect("the block is present")
566                    .as_ref()
567                    .rect
568                    .as_ref(),
569                rect(9),
570                "client {client} must end on the commit's last write",
571            );
572        }
573    }
574
575    /// Re-delivery is refused by the contiguity check, not absorbed by the
576    /// fold — and refused *loudly*, because a repeated rev means the
577    /// transport broke a guarantee the sessions are built on.
578    ///
579    /// The rest of idempotence lives elsewhere: a re-folded commit mints a
580    /// *different* rev, so fold-level re-apply is not idempotent by design,
581    /// and the register keeps the original guarantee (an equal write order
582    /// loses, `register.rs`).
583    #[test]
584    #[should_panic(expected = "gap in the per-connection rev sequence")]
585    fn re_delivering_a_confirmed_commit_is_refused() {
586        let mut world = World::new();
587        let id = block_of(0, 0);
588        world.edit(0, commit("create", vec![create_block(id, "b")]));
589        world.quiesce();
590
591        let replayed = world.host.commits_after(Rev::ZERO)[0].clone();
592        let stale = world.clients[1].session.rev();
593        let _ = world.clients[1].session.apply_foreign(stale, &replayed);
594    }
595
596    /// Rejection safety. The cycle can only be seen by the host: each
597    /// client's reparent is valid against what it holds, and the second to
598    /// be sequenced closes the loop.
599    #[test]
600    fn a_commit_the_host_refuses_is_dropped_and_the_client_re_converges() {
601        let mut world = World::new();
602        let (first, second) = (block_of(0, 0), block_of(0, 1));
603        world.edit(
604            0,
605            commit(
606                "two blocks",
607                vec![create_block(first, "a"), create_block(second, "b")],
608            ),
609        );
610        world.quiesce();
611
612        world.edit(0, commit("nest a under b", vec![reparent(first, second)]));
613        world.edit(1, commit("nest b under a", vec![reparent(second, first)]));
614
615        world.sequence(1);
616        let host_after_first = world.host.state().content_hash();
617        world.sequence(0);
618        assert_eq!(
619            world.host.state().content_hash(),
620            host_after_first,
621            "the refused commit must leave the host untouched",
622        );
623        assert_eq!(
624            world.host.rev(),
625            Rev::new(2),
626            "and must consume no rev — the log cannot gap",
627        );
628
629        world.quiesce();
630        world.assert_converged();
631        assert_eq!(
632            *world.clients[0]
633                .session
634                .confirmed()
635                .block(&first)
636                .expect("the block is present")
637                .as_ref()
638                .parent
639                .as_ref(),
640            BlockId::NULL,
641            "client 0's refused reparent is gone from its document",
642        );
643    }
644
645    /// The undo round trip through a session, over the cases the journal's
646    /// rules turn on. The edit is asserted **observable** first, or an
647    /// inverse that restored nothing would pass vacuously.
648    #[test]
649    fn undo_restores_the_visible_document_through_the_wire() {
650        let id = block_of(0, 0);
651        let created = block_of(0, 1);
652        for edit in [
653            commit("move", vec![move_to(id, 7)]),
654            commit("rename", vec![rename(id, "renamed")]),
655            commit("delete", vec![OpCodes::Block(id, Crud::Delete)]),
656            commit(
657                "create",
658                vec![create_block(created, "new"), move_to(created, 3)],
659            ),
660            commit("drag", vec![move_to(id, 4), move_to(id, 8)]),
661            commit(
662                "multi",
663                vec![
664                    move_to(id, 2),
665                    rename(id, "multi"),
666                    create_block(created, "c"),
667                ],
668            ),
669        ] {
670            let mut world = World::new();
671            world.edit(0, commit("create", vec![create_block(id, "original")]));
672            world.quiesce();
673            let before = projection(world.clients[0].session.confirmed());
674
675            let label = edit.label().to_owned();
676            world.edit(0, edit);
677            world.quiesce();
678            assert_ne!(
679                projection(world.clients[0].session.confirmed()),
680                before,
681                "{label}: the edit must be observable or the round trip proves nothing",
682            );
683
684            let top = world.clients[0]
685                .session
686                .next_undo()
687                .expect("something to undo");
688            let undone = world.clients[0]
689                .session
690                .undo(top)
691                .expect("the inverse folds");
692            let outbound = world.clients[0]
693                .session
694                .last_submission()
695                .expect("the undo is queued")
696                .1
697                .clone();
698            world.clients[0]
699                .outbox
700                .push_back((undone, outbound.clone()));
701            world.clients[0].pending.push_back((undone, outbound));
702            world.quiesce();
703
704            assert_eq!(
705                projection(world.clients[0].session.confirmed()),
706                before,
707                "{label}: undo must restore the visible document",
708            );
709            world.assert_converged();
710        }
711    }
712
713    /// A client joining mid-history folds the log and lands byte-identical
714    /// with everyone already connected. This is the reconnect path too —
715    /// `Welcome` carries commits, never a document, so a joiner reaches
716    /// agreement by replaying rather than by being told the answer.
717    #[test]
718    fn a_late_joiner_folds_the_log_and_converges() {
719        let mut world = World::new();
720        let id = block_of(0, 0);
721        world.edit(0, commit("create", vec![create_block(id, "b")]));
722        world.quiesce();
723        world.edit(1, commit("rename", vec![rename(id, "edited")]));
724        world.edit(2, commit("move", vec![move_to(id, 6)]));
725        world.quiesce();
726
727        let joiner =
728            ClientSession::welcome(world.host.commits_after(Rev::ZERO)).expect("the log folds");
729        assert_eq!(joiner.rev(), world.host.rev());
730        assert_eq!(
731            joiner.confirmed().content_hash(),
732            world.host.state().content_hash(),
733        );
734        assert_eq!(
735            joiner.optimistic().content_hash(),
736            world.clients[0].session.optimistic().content_hash(),
737            "and with the clients that were there all along",
738        );
739    }
740
741    /// A tombstoned block is restorable, and the restore travels: delete
742    /// and restore are inverses through the wire, not only in the fold.
743    #[test]
744    fn a_delete_and_its_restore_travel_to_every_client() {
745        let mut world = World::new();
746        let id = block_of(0, 0);
747        world.edit(0, commit("create", vec![create_block(id, "b")]));
748        world.quiesce();
749
750        world.edit(0, commit("delete", vec![OpCodes::Block(id, Crud::Delete)]));
751        world.quiesce();
752        for client in 0..CLIENTS {
753            assert!(
754                projection(world.clients[client].session.confirmed()).is_empty(),
755                "client {client} must not show a tombstoned block",
756            );
757        }
758
759        world.edit(
760            1,
761            commit("restore", vec![OpCodes::Block(id, Crud::Restore)]),
762        );
763        world.quiesce();
764        for client in 0..CLIENTS {
765            assert_eq!(
766                world.title_of(client, id),
767                "b",
768                "client {client} must see the restored block, inner values intact",
769            );
770        }
771        world.assert_converged();
772    }
773}