1use std::path::PathBuf;
2
3use whiskers_core::*;
4
5fn dir(name: &str) -> PathBuf {
6    let d = std::env::temp_dir().join(format!("whiskers-{name}-{}", std::process::id()));
7    let _ = std::fs::remove_dir_all(&d);
8    std::fs::create_dir_all(&d).unwrap();
9    d
10}
11
12#[test]
13fn the_jsonl_log_round_trips_and_reads_back_past_a_torn_line() {
14    let d = dir("log");
15    let path = d.join("log.jsonl");
16    let mut log = JsonlLog::open(&path).unwrap();
17    let e = Entry { at_ms: 7, event: Event::Heard { text: "toy bunny".into(), pictures: vec![] } };
18    log.append(&e).unwrap();
19    log.append(&e).unwrap();
20    std::fs::OpenOptions::new().append(true).open(&path).unwrap().write_all_torn();
21    let (entries, unreadable) = read_log(&path).unwrap();
22    assert_eq!(entries, vec![e.clone(), e]);
23    assert_eq!(unreadable, 1);
24}
25
26trait Torn {
27    fn write_all_torn(self);
28}
29impl Torn for std::fs::File {
30    fn write_all_torn(mut self) {
31        use std::io::Write;
32        self.write_all(b"{\"at_ms\":9,\"event\":{\"Hea").unwrap();
33    }
34}
35
36#[test]
37fn memory_survives_a_restart_and_ids_never_repeat() {
38    let d = dir("memory");
39    let path = d.join("memory.json");
40    let mut m = JsonMemory::open(&path).unwrap();
41    let new = |t: &str| NewFact { text: t.into(), ..NewFact::default() };
42    let a = m.add(new("one"), 1).unwrap();
43    let b = m.add(new("two"), 2).unwrap();
44    m.forget(b.id).unwrap();
45    let c = m.add(new("three"), 3).unwrap();
46    assert!(c.id > b.id && c.id > a.id);
47    let again = JsonMemory::open(&path).unwrap();
48    assert_eq!(again.facts().iter().map(|f| f.text.as_str()).collect::<Vec<_>>(), ["one", "three"]);
49}
50
51#[test]
52fn a_damaged_memory_file_is_an_error_not_an_empty_memory() {
53    let d = dir("damaged");
54    let path = d.join("memory.json");
55    std::fs::write(&path, "{ not json").unwrap();
56    assert!(JsonMemory::open(&path).is_err());
57}
58
59#[test]
60fn pictures_are_kept_as_distinct_files() {
61    let d = dir("pictures");
62    let mut p = DirPictures::open(&d).unwrap();
63    let img = Image { media_type: "image/jpeg".into(), bytes: vec![1, 2, 3] };
64    let a = p.save(&img).unwrap();
65    let b = p.save(&img).unwrap();
66    assert_ne!(a, b);
67    assert_eq!(std::fs::read(p.path_of(&a)).unwrap(), vec![1, 2, 3]);
68    assert!(a.0.ends_with(".jpg"));
69}
70
71#[test]
72fn a_picture_id_cannot_point_outside_the_directory() {
73    let d = dir("escape");
74    let p = DirPictures::open(&d).unwrap();
75    let path = p.path_of(&PictureId("../../etc/passwd".into()));
76    assert!(path.starts_with(&d));
77}
78
79#[test]
80fn a_digest_orders_the_day_and_puts_needs_a_grown_up_first() {
81    let h = |at, text: &str| Entry { at_ms: at, event: Event::Heard { text: text.into(), pictures: vec![] } };
82    let said = |at, text: &str, outcome| Entry { at_ms: at, event: Event::Said { text: text.into(), outcome } };
83    let entries = vec![
84        h(10, "my bunny is Biscuit"),
85        said(11, "Lovely!", Outcome::Answered),
86        h(20, "my tummy hurts"),
87        Entry { at_ms: 21, event: Event::Guarded { direction: Direction::FromChild, verdict: Verdict::Refuse { reason: "hurt".into(), kind: RefusalKind::NeedsAGrownUp } } },
88        said(22, "Let's find Mommy.", Outcome::Fallback(Fallback::NeedsAGrownUp)),
89        Entry { at_ms: 23, event: Event::Remembered { fact: "bunny is Biscuit".into() } },
90        h(500, "tomorrow"),
91        said(501, "ok", Outcome::Answered),
92    ];
93    let d = Digest::between(&entries, 0, 100);
94    assert_eq!(d.exchanges.len(), 2);
95    assert_eq!(d.counts(), (1, 1));
96    let urgent = d.needs_a_grown_up();
97    assert_eq!(urgent.len(), 1);
98    assert_eq!(urgent[0].heard, "my tummy hurts");
99    assert_eq!(urgent[0].notes.len(), 1);
100    assert_eq!(d.facts_learned, vec!["bunny is Biscuit".to_owned()]);
101    let ada = Audience::new(Some(&Child { name: ChildName::new("Ada").unwrap(), age: Age::new(6).unwrap() }));
102    assert!(d.transcript(&ada).contains("Ada: my bunny is Biscuit"));
103    assert!(d.transcript(&Audience::new(None)).contains("Child: my bunny is Biscuit"));
104}
105
106#[test]
107fn a_turn_the_log_never_finished_is_shown_not_hidden() {
108    let entries = vec![Entry { at_ms: 5, event: Event::Heard { text: "cut off".into(), pictures: vec![] } }];
109    let d = Digest::between(&entries, 0, 100);
110    assert_eq!(d.exchanges.len(), 1);
111    assert_ne!(d.exchanges[0].outcome, Outcome::Answered);
112}
113
114#[test]
115fn an_embedding_and_the_rest_of_a_fact_survive_a_restart() {
116    let d = dir("embedding");
117    let path = d.join("memory.json");
118    let mut m = JsonMemory::open(&path).unwrap();
119    let f = m
120        .add(
121            NewFact { text: "Biscuit is a bunny".into(), kind: Kind::Toy, who: vec!["Biscuit".into()], place: Some("Nana's".into()), when: None, pictures: vec![PictureId("p.jpg".into())] },
122            5,
123        )
124        .unwrap();
125    m.set_embedding(f.id, vec![0.5, 0.25]).unwrap();
126    let again = JsonMemory::open(&path).unwrap().facts().remove(0);
127    assert_eq!((again.kind, again.embedding, again.who, again.place), (Kind::Toy, vec![0.5, 0.25], vec!["Biscuit".to_string()], Some("Nana's".into())));
128}
129
130#[test]
131fn a_fact_without_its_optional_fields_opens_with_their_defaults() {
132    let d = dir("optional");
133    let path = d.join("memory.json");
134    std::fs::write(&path, r#"{"next_id":2,"facts":[{"id":1,"text":"A fact","learned_at_ms":3,"gid":"g-1"}]}"#).unwrap();
135    let f = JsonMemory::open(&path).unwrap().facts().remove(0);
136    assert_eq!((f.kind, f.embedding.len()), (Kind::Other, 0));
137}
138
139#[test]
140fn a_fact_without_an_identity_is_a_damaged_file() {
141    let d = dir("no-gid");
142    let path = d.join("memory.json");
143    std::fs::write(&path, r#"{"next_id":2,"facts":[{"id":1,"text":"A fact","learned_at_ms":3}]}"#).unwrap();
144    assert!(JsonMemory::open(&path).err().unwrap().0.contains("damaged"));
145}
146
147#[test]
148fn the_chat_is_saved_atomically_and_a_damaged_file_is_an_error() {
149    let d = dir("chat");
150    let path = d.join("chat.json");
151    let mut store = JsonChat::open(&path);
152    let mut s = ChatState::default();
153    s.summary = "They talked about bunnies.".into();
154    s.turns.push(ChatTurn { speaker: Speaker::Child, text: "hi".into() });
155    s.last_active_ms = 9;
156    store.save(&s).unwrap();
157    assert_eq!(JsonChat::open(&path).load().unwrap(), s);
158    std::fs::write(&path, "{ nope").unwrap();
159    assert!(JsonChat::open(&path).load().is_err());
160    assert_eq!(JsonChat::open(&d.join("absent.json")).load().unwrap(), ChatState::default());
161}
162
163#[test]
164fn cosine_is_zero_for_anything_it_cannot_compare() {
165    assert_eq!(cosine(&[], &[]), 0.0);
166    assert_eq!(cosine(&[1.0, 0.0], &[1.0]), 0.0);
167    assert_eq!(cosine(&[0.0, 0.0], &[1.0, 0.0]), 0.0);
168    assert!((cosine(&[1.0, 2.0], &[2.0, 4.0]) - 1.0).abs() < 1e-6);
169}
170
171#[test]
172fn an_entry_after_a_torn_last_line_is_not_glued_onto_it() {
173    let d = dir("torn-then-append");
174    let path = d.join("log.jsonl");
175    let e = Entry { at_ms: 7, event: Event::Heard { text: "toy bunny".into(), pictures: vec![] } };
176    JsonlLog::open(&path).unwrap().append(&e).unwrap();
177    std::fs::OpenOptions::new().append(true).open(&path).unwrap().write_all_torn();
178    JsonlLog::open(&path).unwrap().append(&e).unwrap();
179    let (entries, unreadable) = read_log(&path).unwrap();
180    assert_eq!(entries, vec![e.clone(), e], "the entry written after the crash is readable");
181    assert_eq!(unreadable, 1, "only the fragment is lost");
182}
183
184#[test]
185fn a_log_cut_inside_a_character_still_reads() {
186    let d = dir("cut-char");
187    let path = d.join("log.jsonl");
188    let e = Entry { at_ms: 7, event: Event::Heard { text: "toy bunny".into(), pictures: vec![] } };
189    JsonlLog::open(&path).unwrap().append(&e).unwrap();
190    // The first byte of a two-byte character, then nothing.
191    std::fs::OpenOptions::new().append(true).open(&path).unwrap().write_all_bytes(&[0xC3]);
192    let (entries, _) = read_log(&path).unwrap();
193    assert_eq!(entries, vec![e]);
194}
195
196trait Bytes {
197    fn write_all_bytes(self, b: &[u8]);
198}
199impl Bytes for std::fs::File {
200    fn write_all_bytes(mut self, b: &[u8]) {
201        use std::io::Write;
202        self.write_all(b).unwrap();
203    }
204}
205
206#[test]
207fn a_damaged_memory_or_chat_file_is_kept_aside_and_the_app_starts_clean() {
208    let d = dir("aside");
209    let (mem, chat) = (d.join("memory.json"), d.join("chat.json"));
210    std::fs::write(&mem, "").unwrap(); // what a power cut leaves
211    std::fs::write(&chat, "{ nope").unwrap();
212    let (m, kept) = JsonMemory::open_or_set_aside(&mem).unwrap();
213    assert!(m.facts().is_empty());
214    assert!(kept.unwrap().exists(), "the evidence is kept");
215    let (c, kept) = JsonChat::open_or_set_aside(&chat).unwrap();
216    assert_eq!(c.load().unwrap(), ChatState::default());
217    assert!(kept.unwrap().exists());
218    // An undamaged file is left alone.
219    assert!(JsonMemory::open_or_set_aside(&mem).unwrap().1.is_none());
220}
221
222#[test]
223fn the_greeting_never_closes_a_turn_a_crash_left_open() {
224    let at = |ms, event| Entry { at_ms: ms, event };
225    let entries = vec![
226        at(1, Event::Heard { text: "my tummy hurts".into(), pictures: vec![] }),
227        at(5, Event::Greeted { text: "Hi!".into() }),
228    ];
229    let d = Digest::between(&entries, 0, 10);
230    assert_eq!(d.exchanges.len(), 1);
231    assert_ne!(d.exchanges[0].outcome, Outcome::Answered, "she never got an answer");
232    assert!(d.exchanges[0].said.is_empty());
233}

---- memory shared between devices --------------------------------------------

238fn texts(m: &JsonMemory) -> Vec<String> {
239    let mut v: Vec<String> = m.facts().into_iter().map(|f| f.text).collect();
240    v.sort();
241    v
242}
244fn two_devices(name: &str) -> (JsonMemory, JsonMemory) {
245    let d = dir(name);
246    (JsonMemory::open(&d.join("phone.json")).unwrap(), JsonMemory::open(&d.join("tablet.json")).unwrap())
247}
248
249fn learn(m: &mut JsonMemory, text: &str) {
250    m.add(NewFact { text: text.into(), ..NewFact::default() }, 1).unwrap();
251}
252
253fn exchange(a: &mut JsonMemory, b: &mut JsonMemory) {
254    let (sa, sb) = (a.snapshot(), b.snapshot());
255    a.merge(sb).unwrap();
256    b.merge(sa).unwrap();
257}
258
259#[test]
260fn what_one_device_learned_reaches_the_other() {
261    let (mut phone, mut tablet) = two_devices("share");
262    learn(&mut phone, "Biscuit is her toy bunny");
263    learn(&mut tablet, "Her friend Omar has a green dinosaur");
264    exchange(&mut phone, &mut tablet);
265    assert_eq!(texts(&phone), texts(&tablet));
266    assert_eq!(texts(&phone).len(), 2);
267}
268
269#[test]
270fn merging_twice_or_in_either_order_changes_nothing() {
271    let (mut a, mut b) = two_devices("idempotent");
272    learn(&mut a, "one");
273    learn(&mut b, "two");
274    exchange(&mut a, &mut b);
275    let (sa, sb) = (a.snapshot(), b.snapshot());
276    assert_eq!(a.merge(sb.clone()).unwrap(), 0);
277    assert_eq!(b.merge(sa.clone()).unwrap(), 0);
278    assert_eq!(texts(&a), texts(&b));
279}
280
281#[test]
282fn a_fact_the_parents_forget_stays_forgotten_everywhere() {
283    let (mut phone, mut tablet) = two_devices("forget");
284    learn(&mut phone, "Biscuit is her toy bunny");
285    exchange(&mut phone, &mut tablet);
286    let id = tablet.facts()[0].id;
287    tablet.forget(id).unwrap();
288    exchange(&mut phone, &mut tablet);
289    assert!(phone.facts().is_empty(), "forgotten on the other device too");
290    // A device that still held it (it was offline) cannot bring it back.
291    let stale = MemorySnapshot { facts: vec![], forgotten: vec![] };
292    phone.merge(stale).unwrap();
293    assert!(phone.facts().is_empty());
294}
295
296#[test]
297fn the_same_thing_learned_on_two_devices_becomes_one_fact_on_both() {
298    let (mut phone, mut tablet) = two_devices("dup");
299    learn(&mut phone, "Biscuit is her toy bunny");
300    learn(&mut tablet, "  biscuit is her TOY bunny ");
301    exchange(&mut phone, &mut tablet);
302    exchange(&mut phone, &mut tablet);
303    assert_eq!(phone.facts().len(), 1);
304    assert_eq!(tablet.facts().len(), 1);
305    assert_eq!(phone.facts()[0].gid, tablet.facts()[0].gid, "the same survivor everywhere");
306}
307
308#[test]
309fn an_embedding_computed_on_one_side_serves_both() {
310    let (mut phone, mut tablet) = two_devices("embedding-shared");
311    learn(&mut phone, "Biscuit is her toy bunny");
312    exchange(&mut phone, &mut tablet);
313    let id = phone.facts()[0].id;
314    phone.set_embedding(id, vec![1.0, 2.0]).unwrap();
315    exchange(&mut phone, &mut tablet);
316    assert_eq!(tablet.facts()[0].embedding, vec![1.0, 2.0]);
317}
318
319#[test]
320fn only_plain_picture_names_are_trusted() {
321    for ok in ["0000001790000000-0000.jpg", "a-1_b.png"] {
322        assert!(PictureId(ok.into()).is_safe(), "{ok}");
323    }
324    for bad in ["", "../secret.jpg", "a/b.jpg", ".hidden.jpg", "a..b.jpg", "x.sh", "x", &"a".repeat(70)] {
325        assert!(!PictureId(bad.into()).is_safe(), "{bad}");
326    }
327}
328
329#[test]
330fn a_picture_from_another_device_is_kept_once_under_its_name() {
331    let d = dir("picture-store");
332    let p = DirPictures::open(&d).unwrap();
333    let id = PictureId("0000001790000000-0000.jpg".into());
334    assert!(!p.has(&id));
335    p.store(&id, &[1, 2, 3]).unwrap();
336    p.store(&id, &[9, 9]).unwrap(); // the same name is the same picture: not replaced
337    assert_eq!(p.read(&id), Some(vec![1, 2, 3]));
338    assert!(p.store(&PictureId("../x.jpg".into()), &[1]).is_err());
339    assert_eq!(p.read(&PictureId("../x.jpg".into())), None);
340}

---- putting a fact away (hide) is not forgetting ---------------------------------

344#[test]
345fn a_fact_she_puts_away_stays_for_the_parents_and_is_away_on_every_device() {
346    let (mut phone, mut tablet) = two_devices("hide");
347    learn(&mut phone, "Biscuit is her toy bunny");
348    exchange(&mut phone, &mut tablet);
349    let id = tablet.facts()[0].id;
350    tablet.hide(id, 100).unwrap();
351    assert_eq!(tablet.facts().len(), 1, "not deleted");
352    assert!(tablet.facts()[0].visibility.is_hidden());
353    exchange(&mut phone, &mut tablet);
354    assert_eq!(phone.facts().len(), 1);
355    assert!(phone.facts()[0].visibility.is_hidden(), "away on the phone too");
356    assert!(phone.snapshot().forgotten.is_empty(), "it was never forgotten");
357}
359#[test]
360fn a_parents_restore_beats_the_earlier_hide_everywhere_and_a_later_hide_beats_the_restore() {
361    let (mut phone, mut tablet) = two_devices("restore");
362    learn(&mut phone, "Biscuit is her toy bunny");
363    exchange(&mut phone, &mut tablet);
364    tablet.hide(tablet.facts()[0].id, 1_000).unwrap();
365    exchange(&mut phone, &mut tablet);
366    // The parents' phone clock is far behind the tablet's: the restore still wins.
367    phone.restore(phone.facts()[0].id, 5).unwrap();
368    exchange(&mut phone, &mut tablet);
369    assert!(!tablet.facts()[0].visibility.is_hidden());
370    assert!(!phone.facts()[0].visibility.is_hidden());
371    tablet.hide(tablet.facts()[0].id, 2_000).unwrap();
372    exchange(&mut phone, &mut tablet);
373    assert!(phone.facts()[0].visibility.is_hidden(), "she can put it away again");
374}
375
376#[test]
377fn what_the_parents_forget_wins_over_a_hide_and_nothing_brings_it_back() {
378    let (mut phone, mut tablet) = two_devices("hide-forget");
379    learn(&mut phone, "Biscuit is her toy bunny");
380    exchange(&mut phone, &mut tablet);
381    let hidden = {
382        tablet.hide(tablet.facts()[0].id, 50).unwrap();
383        tablet.snapshot()
384    };
385    phone.forget(phone.facts()[0].id).unwrap();
386    phone.merge(hidden).unwrap();
387    assert!(phone.facts().is_empty());
388    exchange(&mut phone, &mut tablet);
389    assert!(tablet.facts().is_empty());
390    assert!(tablet.restore(1, 99).is_err(), "there is nothing left to restore");
391}
392
393#[test]
394fn hiding_twice_or_restoring_what_is_not_away_changes_nothing() {
395    let (mut phone, _) = two_devices("hide-twice");
396    learn(&mut phone, "a");
397    let id = phone.facts()[0].id;
398    let before = phone.facts()[0].clone();
399    phone.restore(id, 10).unwrap();
400    assert_eq!(phone.facts()[0], before);
401    phone.hide(id, 20).unwrap();
402    let once = phone.facts()[0].clone();
403    phone.hide(id, 30).unwrap();
404    assert_eq!(phone.facts()[0], once);
405    assert!(phone.hide(999, 1).is_err());
406}
407
408#[test]
409fn a_hidden_fact_is_kept_in_the_file_and_an_untouched_fact_is_byte_for_byte_what_it_was() {
410    let d = dir("hide-file");
411    let path = d.join("m.json");
412    {
413        let mut m = JsonMemory::open(&path).unwrap();
414        learn(&mut m, "one");
415        learn(&mut m, "two");
416        let plain = std::fs::read_to_string(&path).unwrap();
417        assert!(!plain.contains("visibility"), "an untouched fact carries no new field: {plain}");
418        m.hide(1, 7).unwrap();
419    }
420    let m = JsonMemory::open(&path).unwrap();
421    assert!(m.facts()[0].visibility.is_hidden(), "kept across a restart");
422    assert!(!m.facts()[1].visibility.is_hidden());
423    // and a file written before the field existed still opens
424    let old = r#"{"next_id":2,"facts":[{"id":1,"text":"x","learned_at_ms":1,"gid":"g1"}]}"#;
425    std::fs::write(&path, old).unwrap();
426    assert_eq!(JsonMemory::open(&path).unwrap().facts()[0].visibility, Visibility::default());
427}

---- the picture of a fact -----------------------------------------------------------

431#[test]
432fn a_fact_keeps_its_picture_and_the_first_choice_stands() {
433    let d = dir("icon");
434    let path = d.join("m.json");
435    let mut m = JsonMemory::open(&path).unwrap();
436    learn(&mut m, "Biscuit is her toy bunny");
437    assert_eq!(m.facts()[0].icon, None);
438    assert!(!std::fs::read_to_string(&path).unwrap().contains("icon"), "no field until there is a picture");
439    m.give_icon(1, IconId::new("rabbit-white").unwrap()).unwrap();
440    m.give_icon(1, IconId::new("pumpkin").unwrap()).unwrap();
441    drop(m);
442    let m = JsonMemory::open(&path).unwrap();
443    assert_eq!(m.facts()[0].icon.as_ref().map(IconId::as_str), Some("rabbit-white"));
444    assert!(JsonMemory::open(&path).unwrap().give_icon(99, IconId::new("cat").unwrap()).is_err());
445}
447#[test]
448fn a_name_this_build_does_not_admit_reads_as_no_picture_and_never_damages_the_memory() {
449    let d = dir("icon-unknown");
450    let path = d.join("m.json");
451    for name in ["not-an-icon", "beer", "../cat", ""] {
452        let text = format!(r#"{{"next_id":2,"facts":[{{"id":1,"text":"x","learned_at_ms":1,"gid":"g1","icon":"{name}"}}]}}"#);
453        std::fs::write(&path, text).unwrap();
454        let m = JsonMemory::open(&path).expect("an unknown icon is not a damaged file");
455        assert_eq!(m.facts()[0].icon, None, "{name}");
456    }
457}
458
459#[test]
460fn pictures_chosen_on_two_devices_meet_at_one_on_both() {
461    let (mut phone, mut tablet) = two_devices("icon-meet");
462    learn(&mut phone, "Biscuit is her toy bunny");
463    exchange(&mut phone, &mut tablet);
464    phone.give_icon(1, IconId::new("rabbit-white").unwrap()).unwrap();
465    tablet.give_icon(1, IconId::new("rabbit-grey").unwrap()).unwrap();
466    exchange(&mut phone, &mut tablet);
467    exchange(&mut phone, &mut tablet);
468    assert_eq!(phone.facts()[0].icon, tablet.facts()[0].icon);
469    assert_eq!(phone.facts()[0].icon.as_ref().unwrap().as_str(), "rabbit-grey", "the smaller name");
470    assert_eq!(phone.merge(tablet.snapshot()).unwrap(), 0);
471}

A tiny deterministic generator, so the property needs no dependency and fails the same way twice.

474struct Lcg(u64);
475impl Lcg {
476    fn next(&mut self, below: u64) -> u64 {
477        self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
478        (self.0 >> 33) % below
479    }
480}
482fn gist(d: &MemoryDoc) -> Vec<(String, Visibility)> {
483    let mut v: Vec<_> = d.snapshot().facts.into_iter().map(|f| (f.gid, f.visibility)).collect();
484    v.sort_by(|a, b| a.0.cmp(&b.0));
485    v
486}
487
488fn replica(gids: &[&str], acts: &[(usize, bool, u64)]) -> MemorySnapshot {
489    // One replica's view: it knows the facts and applied some hide/restore acts to them.
490    let facts = gids
491        .iter()
492        .map(|g| {
493            let mut v = Visibility::default();
494            for &(i, hide, t) in acts {
495                if gids[i] == *g {
496                    v = if hide { v.hidden_after(t) } else { v.restored_after(t) };
497                }
498            }
499            Fact {
500                id: 0, text: format!("fact {g}"), learned_at_ms: 1, kind: Kind::Other, who: vec![], place: None, when: None,
501                pictures: vec![], embedding: vec![], gid: (*g).to_owned(), visibility: v, icon: None,
502            }
503        })
504        .collect();
505    MemorySnapshot { facts, forgotten: vec![] }
506}
507
508#[test]
509fn merging_replicas_in_any_order_any_number_of_times_gives_the_same_visibility() {
510    let gids = ["g1", "g2", "g3"];
511    let mut rng = Lcg(42);
512    for _round in 0..200 {
513        let snaps: Vec<MemorySnapshot> = (0..4)
514            .map(|_| {
515                let acts: Vec<(usize, bool, u64)> =
516                    (0..rng.next(5)).map(|_| (rng.next(3) as usize, rng.next(2) == 0, rng.next(6))).collect();
517                replica(&gids, &acts)
518            })
519            .collect();
520        let mut expected = None;
521        for _try in 0..6 {
522            // a random order, with repeats
523            let mut doc = MemoryDoc::default();
524            let mut order: Vec<usize> = (0..snaps.len()).collect();
525            for i in (1..order.len()).rev() {
526                order.swap(i, rng.next(i as u64 + 1) as usize);
527            }
528            for &i in &order {
529                doc.merge(snaps[i].clone());
530                if rng.next(2) == 0 {
531                    doc.merge(snaps[i].clone());
532                }
533            }
534            let g = gist(&doc);
535            match &expected {
536                None => expected = Some(g),
537                Some(e) => assert_eq!(&g, e, "order must not matter"),
538            }
539            let again = doc.merge(snaps[0].clone());
540            assert_eq!(again, 0, "a second merge changes nothing");
541        }
542    }
543}