What waits to be thought about survives the process dying, and the chat emptied for a forgetting is emptied in the file.
3use std::path::PathBuf;
5use whiskers_core::{ChatState, ChatStore, ChatTurn, PictureId, ReflectInput, Speaker}; 6use whiskers_store::{SqliteChat, SqliteReflections, Store}; 7 8fn dir(name: &str) -> PathBuf { 9 let d = std::env::temp_dir().join(format!("whiskers-store-queue-{name}-{}", std::process::id())); 10 let _ = std::fs::remove_dir_all(&d); 11 std::fs::create_dir_all(&d).unwrap(); 12 d 13} 14 15fn input(n: u32) -> ReflectInput { 16 ReflectInput { 17 turn: format!("turn-{n}"), 18 heard: format!("heard {n}"), 19 said: format!("said {n}"), 20 description: (n % 2 == 0).then(|| format!("looked like {n}")), 21 pictures: vec![PictureId(format!("000000000000000{n}-0000.jpg"))], 22 } 23} 24 25#[test] 26fn what_waits_is_still_waiting_when_the_database_is_opened_again_and_only_done_takes_it_off() { 27 let d = dir("restart"); 28 let db = d.join("w.db"); 29 { 30 let queue = SqliteReflections::new(Store::open(&db).unwrap()); 31 for n in 1..=3 { 32 queue.push(&input(n), u64::from(n) * 10, 8).unwrap(); 33 } 34 // The process dies here: nothing is closed politely. 35 } 36 let queue = SqliteReflections::new(Store::open(&db).unwrap()); 37 let waiting = queue.waiting().unwrap(); 38 assert_eq!(waiting.iter().map(|w| w.input.clone()).collect::<Vec<_>>(), [input(1), input(2), input(3)], "in the order they came, whole"); 39 // A pass over them that stops after the first leaves the rest. 40 queue.done(waiting[0].id).unwrap(); 41 drop(queue); 42 let queue = SqliteReflections::new(Store::open(&db).unwrap()); 43 assert_eq!(queue.waiting().unwrap().iter().map(|w| w.input.turn.clone()).collect::<Vec<_>>(), ["turn-2", "turn-3"]); 44 for w in queue.waiting().unwrap() { 45 queue.done(w.id).unwrap(); 46 } 47 assert!(queue.is_empty().unwrap()); 48} 49 50#[test] 51fn a_long_outage_does_not_queue_the_whole_evening() { 52 let queue = SqliteReflections::new(Store::open_in_memory().unwrap()); 53 let mut dropped = 0; 54 for n in 1..=5 { 55 dropped += queue.push(&input(n), u64::from(n), 3).unwrap(); 56 } 57 assert_eq!(dropped, 2); 58 assert_eq!(queue.waiting().unwrap().iter().map(|w| w.input.turn.clone()).collect::<Vec<_>>(), ["turn-3", "turn-4", "turn-5"], "the oldest make room"); 59 assert!(queue.has_turn("turn-4").unwrap() && !queue.has_turn("turn-1").unwrap()); 60} 61 62fn contains(db: &std::path::Path, needle: &[u8]) -> bool { 63 let wal = db.with_file_name(format!("{}-wal", db.file_name().unwrap().to_string_lossy())); 64 [std::fs::read(db).unwrap(), std::fs::read(wal).unwrap_or_default()].iter().any(|b| b.windows(needle.len()).any(|w| w == needle)) 65} 66 67#[test] 68fn a_chat_emptied_for_a_forgetting_is_gone_from_the_file_and_its_log_and_what_it_was_emptied_for_is_kept() { 69 let d = dir("chat"); 70 let db = d.join("w.db"); 71 let mut chat = SqliteChat::new(Store::open(&db).unwrap()); 72 let full = ChatState { 73 summary: "SUMMARY-WORDS-7413-QX".into(), 74 turns: vec![ChatTurn { speaker: Speaker::Child, text: "TURN-WORDS-8524-QX".into() }, ChatTurn { speaker: Speaker::Whiskers, text: "REPLY-WORDS-9635-QX".into() }], 75 last_active_ms: 5, 76 version: 3, 77 scrubbed: vec![], 78 }; 79 chat.save(&full).unwrap(); 80 for w in ["SUMMARY-WORDS-7413-QX", "TURN-WORDS-8524-QX", "REPLY-WORDS-9635-QX"] { 81 assert!(contains(&db, w.as_bytes()), "{w} is kept while it is the chat"); 82 } 83 let mut emptied = full.clone(); 84 assert!(emptied.scrub(&["g1".into()])); 85 chat.save(&emptied).unwrap(); 86 for w in ["SUMMARY-WORDS-7413-QX", "TURN-WORDS-8524-QX", "REPLY-WORDS-9635-QX"] { 87 assert!(!contains(&db, w.as_bytes()), "{w} is gone from the file and its write-ahead log"); 88 } 89 let (file, wal) = (std::fs::metadata(&db).unwrap().len(), std::fs::metadata(db.with_file_name("w.db-wal")).map_or(0, |m| m.len())); 90 assert!(file > 0 && wal == 0, "the log was truncated"); 91 drop(chat); 92 let again = SqliteChat::new(Store::open(&db).unwrap()).load().unwrap(); 93 assert_eq!((again.scrubbed, again.turns.len(), again.version, again.last_active_ms), (vec!["g1".to_owned()], 0, 4, 5)); 94}