whiskers.git / crates / whiskers-store / tests / queue_and_chat.rs

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}