Undoing a soft delete: she puts a memory away and has UNDO_WINDOW_MS to take it back. The memory comes
back as it was; after the window, twice, for an action that does not exist, or once the parents have forgotten
the memory, it is refused and nothing changes.
5use std::path::PathBuf;
7use whiskers_core::*; 8use whiskers_store::{SqliteMemory, Store}; 9 10fn dir(name: &str) -> PathBuf { 11 use std::sync::atomic::{AtomicUsize, Ordering}; 12 static N: AtomicUsize = AtomicUsize::new(0); 13 let d = std::env::temp_dir().join(format!("whiskers-store-undo-{name}-{}-{}", std::process::id(), N.fetch_add(1, Ordering::SeqCst))); 14 let _ = std::fs::remove_dir_all(&d); 15 std::fs::create_dir_all(&d).unwrap(); 16 d 17}
A memory with everything that hangs off one: mentions, an older wording, an embedding, two pictures and a star.
20fn rich(m: &mut SqliteMemory) -> Fact { 21 let (a, b) = (PictureId("0000000000000001-0000.jpg".into()), PictureId("0000000000000002-0000.jpg".into())); 22 let f = m.add(NewFact { text: "a toy cat".into(), pictures: vec![a.clone(), b.clone()], ..NewFact::default() }, 10).unwrap(); 23 m.mention(f.id, 500, "").unwrap(); 24 m.reword(f.id, "a big toy cat".into(), 800).unwrap(); 25 m.set_embedding(f.id, vec![0.5, 0.25]).unwrap(); 26 m.choose_cover(f.id, &b, 900).unwrap(); 27 m.facts().into_iter().find(|x| x.id == f.id).unwrap() 28}
The fact with the register's stamp taken out, which is the one thing a hide and an undo are meant to change.
36#[test] 37fn an_undo_inside_the_window_puts_the_memory_back_exactly() { 38 let d = dir("exact"); 39 let mut m = SqliteMemory::open(Store::open(&d.join("d.db")).unwrap(), "phone").unwrap(); 40 let before = rich(&mut m); 41 m.hide(before.id, 100_000).unwrap(); 42 assert!(m.facts()[0].visibility.is_hidden()); 43 let actions = m.soft_actions(); 44 assert_eq!(actions.len(), 1); 45 assert_eq!((actions[0].kind, actions[0].gid.as_str(), actions[0].at_ms, actions[0].undone), (SoftKind::PutAway, before.gid.as_str(), 100_000, false)); 46 47 let back = m.undo(actions[0].id, 100_000 + UNDO_WINDOW_MS - 1).unwrap(); 48 assert!(!back.visibility.is_hidden()); 49 let now = m.facts().remove(0); 50 assert_eq!(without_visibility(now.clone()), without_visibility(before.clone()), "its mentions, older words, embedding, pictures and star are what they were"); 51 assert_eq!((now.mentions.len(), now.revisions.len(), now.pictures.len(), now.embedding.len()), (2, 1, 2, 2)); 52 assert!(now.visibility.at_ms() > 100_000, "the restore is stamped later than the hide, so it wins when devices meet"); 53 assert!(m.soft_actions()[0].undone); 54 // It is on disk, not only in the process. 55 drop(m); 56 let again = SqliteMemory::open(Store::open(&d.join("d.db")).unwrap(), "phone").unwrap(); 57 assert!(!again.facts()[0].visibility.is_hidden()); 58 assert!(again.soft_actions()[0].undone); 59} 60 61#[test] 62fn an_undo_after_the_window_is_refused_and_changes_nothing() { 63 let d = dir("late"); 64 let mut m = SqliteMemory::open(Store::open(&d.join("d.db")).unwrap(), "phone").unwrap(); 65 let f = rich(&mut m); 66 m.hide(f.id, 50_000).unwrap(); 67 let id = m.soft_actions()[0].id; 68 let hidden = m.facts(); 69 assert_eq!(m.undo(id, 50_000 + UNDO_WINDOW_MS), Err(UndoRefusal::Closed), "the window is closed at its end"); 70 assert_eq!(m.undo(id, 5_000_000), Err(UndoRefusal::Closed)); 71 assert_eq!(m.facts(), hidden, "still put away, nothing else touched"); 72 assert!(!m.soft_actions()[0].undone, "and it stays unspent, for the record"); 73} 74 75#[test] 76fn an_undo_is_spent_once() { 77 let d = dir("twice"); 78 let mut m = SqliteMemory::open(Store::open(&d.join("d.db")).unwrap(), "phone").unwrap(); 79 let f = rich(&mut m); 80 m.hide(f.id, 1_000).unwrap(); 81 let id = m.soft_actions()[0].id; 82 m.undo(id, 2_000).unwrap(); 83 assert_eq!(m.undo(id, 2_500), Err(UndoRefusal::Spent)); 84 // She puts it away again: a new action, with its own window. 85 m.hide(f.id, 20_000).unwrap(); 86 let actions = m.soft_actions(); 87 assert_eq!(actions.len(), 2); 88 assert!(actions[1].id > id && !actions[1].undone); 89 assert_eq!(m.undo(id, 21_000), Err(UndoRefusal::Spent), "the first is still spent"); 90 m.undo(actions[1].id, 21_000).unwrap(); 91 assert!(!m.facts()[0].visibility.is_hidden()); 92} 93 94#[test] 95fn what_is_not_a_soft_action_cannot_be_undone() { 96 let d = dir("not-soft"); 97 let mut m = SqliteMemory::open(Store::open(&d.join("d.db")).unwrap(), "phone").unwrap(); 98 let f = rich(&mut m); 99 // A number nobody was given, a memory's own id (a fact is not an action), and the forgetting of another memory. 100 assert_eq!(m.undo(1, 10), Err(UndoRefusal::Gone)); 101 assert_eq!(m.undo(f.id, 10), Err(UndoRefusal::Gone)); 102 let other = m.add(NewFact { text: "other".into(), ..NewFact::default() }, 20).unwrap(); 103 m.forget(other.id, 30).unwrap(); 104 assert_eq!(m.undo(2, 40), Err(UndoRefusal::Gone)); 105 assert!(m.soft_actions().is_empty(), "nothing was put away, so there is nothing to offer"); 106 assert!(!m.facts()[0].visibility.is_hidden()); 107} 108 109#[test] 110fn putting_away_what_is_away_already_adds_no_action() { 111 let d = dir("twice-away"); 112 let mut m = SqliteMemory::open(Store::open(&d.join("d.db")).unwrap(), "phone").unwrap(); 113 let f = rich(&mut m); 114 m.hide(f.id, 1_000).unwrap(); 115 m.hide(f.id, 2_000).unwrap(); 116 assert_eq!(m.soft_actions().len(), 1); 117 assert!(m.hide(999, 3_000).is_err()); 118} 119 120#[test] 121fn the_parents_forgetting_a_memory_leaves_nothing_to_undo_and_cannot_be_undone() { 122 let d = dir("forget"); 123 let db = d.join("d.db"); 124 let store = Store::open(&db).unwrap(); 125 let mut m = SqliteMemory::open(store.clone(), "phone").unwrap(); 126 let f = rich(&mut m); 127 let keep = m.add(NewFact { text: "stays".into(), ..NewFact::default() }, 20).unwrap(); 128 m.hide(f.id, 1_000).unwrap(); 129 m.hide(keep.id, 1_100).unwrap(); 130 let (id_forgotten, id_kept) = (m.soft_actions()[0].id, m.soft_actions()[1].id); 131 m.forget(f.id, 1_500).unwrap(); 132 // Inside the window, and still refused: the action went with the memory. 133 assert_eq!(m.undo(id_forgotten, 1_600), Err(UndoRefusal::Gone)); 134 assert_eq!(m.soft_actions().iter().map(|a| a.id).collect::<Vec<_>>(), vec![id_kept], "only the other memory's action is left"); 135 // The forget itself is not a soft action: it never appears in the log, whatever its number. 136 assert!(m.soft_actions().iter().all(|a| a.kind == SoftKind::PutAway)); 137 // And the schema cannot hold an action for a memory that is gone, whatever code asks. 138 drop((m, store)); 139 let conn = rusqlite::Connection::open(&db).unwrap(); 140 conn.pragma_update(None, "foreign_keys", "ON").unwrap(); 141 let err = conn.execute("INSERT INTO soft_action (kind, gid, at_ms) VALUES ('put_away', ?1, 1)", [&f.gid]).unwrap_err(); 142 assert!(err.to_string().to_lowercase().contains("foreign key"), "{err}"); 143 let count: i64 = conn.query_row("SELECT COUNT(*) FROM soft_action WHERE gid = ?1", [&f.gid], |r| r.get(0)).unwrap(); 144 assert_eq!(count, 0); 145} 146 147#[test] 148fn an_undo_reaches_the_other_devices_and_beats_the_hide_that_got_there_first() { 149 let d = dir("sync"); 150 let mut phone = SqliteMemory::open(Store::open(&d.join("phone.db")).unwrap(), "phone").unwrap(); 151 let mut tablet = SqliteMemory::open(Store::open(&d.join("tablet.db")).unwrap(), "tablet").unwrap(); 152 let f = rich(&mut phone); 153 tablet.merge(phone.snapshot()).unwrap(); 154 phone.hide(f.id, 100_000).unwrap(); 155 tablet.merge(phone.snapshot()).unwrap(); 156 assert!(tablet.facts()[0].visibility.is_hidden(), "the tablet heard of the hide"); 157 let id = phone.soft_actions()[0].id; 158 phone.undo(id, 102_000).unwrap(); 159 tablet.merge(phone.snapshot()).unwrap(); 160 assert!(!tablet.facts()[0].visibility.is_hidden(), "and then of the undo"); 161 phone.merge(tablet.snapshot()).unwrap(); 162 assert!(!phone.facts()[0].visibility.is_hidden(), "which does not come back round as a hide"); 163 assert!(tablet.soft_actions().is_empty(), "the tablet's child did not put it away, so it has nothing to undo"); 164}