Skip to main content

sioul_core/
dose_reminders.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! What each device did to remind each dose (docs/health.md, "What became of
5//! a reminder"): `$XDG_STATE_HOME/sioul/health-reminders.toml`, in the
6//! sharing's health part.
7//!
8//! The rule it serves: a dose's reminder is plain only on the device that
9//! knows it is the dose's first; every other reminder says the doubt first,
10//! "Check first". A device knows it only from what the others wrote here, so
11//! each writes, for each dose, before each notice of it goes (`maybe`), when
12//! one was shown (`shown`), when its first try surely did not go out
13//! (`unshown`), and what it promises (`quiet`: no notice of the dose from it
14//! until then). Each device writes only its own entry, which travels whole
15//! (`dose.<key>.<device>`): the sharing, where the later word wins entry by
16//! entry, never has two devices fighting over one.
17//!
18//! A file of its own, as `health-doses.toml` is: an older Sioul (0.0.3 to
19//! 0.0.5) does not know it, keeps its changes waiting and writes nothing in
20//! it; a table of `health-state.toml` or a field of `health-doses.toml`
21//! would be dropped by its next save, and the sharing would then take it out
22//! on every device.
23
24use crate::health::{Problem, Unsound};
25use serde::{Deserialize, Serialize};
26use std::collections::BTreeMap;
27use std::path::{Path, PathBuf};
28
29/// The file's name, in Sioul's state folder.
30pub const FILE: &str = "health-reminders.toml";
31
32/// How long an entry is kept after its dose was due: the day's doses, and one more.
33pub const KEPT_DAYS: i64 = 2;
34
35/// A promise without end (`Tried::quiet`): no notice of the dose from this device again.
36pub const NEVER: i64 = i64::MAX;
37
38/// How long after its dose any device takes another device's entry out (a
39/// device forgotten, reinstalled under a new id, gone without a word): far
40/// past `KEPT_DAYS`, beyond any clock's error (the eighth review's L6);
41/// counted from the newest dose the other devices' entries name when it is
42/// earlier than this device's clock (`save`: the ninth review's P1).
43pub const STALE_DAYS: i64 = 30;
44
45/// What each device did to remind each dose, by the dose's key, then the device's id.
46#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
47pub struct DoseReminders {
48    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
49    pub dose: BTreeMap<String, BTreeMap<String, Tried>>,
50}
51
52/// What one device did to remind one dose (Unix seconds, its own clock; 0: never).
53#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
54pub struct Tried {
55    /// When its last notice of the dose was about to go: written before it
56    /// went, so that no notice ever goes unrecorded. It may be on the screen
57    /// since, until `shown` says it is.
58    #[serde(default)]
59    pub maybe: i64,
60    /// When one was known shown: its notification server answered (or, on a
61    /// phone, its alarm showed it). A reminder.
62    #[serde(default)]
63    pub shown: i64,
64    /// When its first try surely did not go out: nothing was sent.
65    #[serde(default)]
66    pub unshown: i64,
67    /// A promise: from the moment this was written until then, no notice of
68    /// the dose goes from this device; `NEVER`, none again.
69    #[serde(default)]
70    pub quiet: i64,
71}
72
73impl DoseReminders {
74    pub fn default_path() -> PathBuf {
75        crate::config::state_dir().join(FILE)
76    }
77
78    /// The entries as written, or why they cannot be trusted. Never empty in
79    /// place of a file that does not read, nor of one lost after it was
80    /// written here (`health::witnessed`): a device's word that it sent a
81    /// notice would look never said.
82    pub fn read(path: &Path) -> Result<DoseReminders, Unsound> {
83        match std::fs::read_to_string(path) {
84            Ok(text) if text.trim().is_empty() && crate::health::witnessed(path) => Err(Unsound::Lost),
85            Ok(text) => toml::from_str(&text).map_err(|_| Unsound::Unreadable),
86            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
87                if crate::health::witnessed(path) {
88                    Err(Unsound::Lost)
89                } else {
90                    Ok(DoseReminders::default())
91                }
92            }
93            Err(_) => Err(Unsound::Unreadable),
94        }
95    }
96
97    /// The entries changed by `change`, one writer at a time, read again just
98    /// before, so that nothing written meanwhile is written over; `me`, this
99    /// device's id: its own entries past `KEPT_DAYS` dropped.
100    pub fn update<T>(path: &Path, now: i64, me: &str, change: impl FnOnce(&mut DoseReminders) -> T) -> Result<T, Problem> {
101        crate::filelock::with_lock(path, || {
102            let mut all = DoseReminders::read(path).map_err(Problem::Unsound)?;
103            let out = change(&mut all);
104            all.save(path, now, me).map_err(Problem::Write)?;
105            Ok(out)
106        })
107    }
108
109    /// Saved, this device's own entries past `KEPT_DAYS` dropped, by its
110    /// clock (each device takes its own out, and the sharing carries that: a
111    /// clock days ahead would empty the others' else, the sixth review's L3,
112    /// R8); every device's entries `STALE_DAYS` after their dose, counted not
113    /// by this clock alone but by the earlier of it and the newest dose the
114    /// other devices' entries name, so that a clock a month ahead, here or on
115    /// one other device, takes no entry of today's out (the ninth review's P1).
116    /// Use `update`: it holds the lock.
117    fn save(&mut self, path: &Path, now: i64, me: &str) -> Result<(), String> {
118        // The others' newest dose: a time this clock alone cannot push.
119        let others = self.dose.iter().filter(|(_, devices)| devices.keys().any(|device| device != me)).map(|(key, _)| due_of(key)).max().unwrap_or(0);
120        let (oldest, stale) = (now - KEPT_DAYS * 86_400, now.min(others) - STALE_DAYS * 86_400);
121        for (key, devices) in &mut self.dose {
122            if due_of(key) < stale {
123                devices.clear();
124            } else if due_of(key) < oldest {
125                devices.remove(me);
126            }
127        }
128        self.dose.retain(|_, devices| !devices.is_empty());
129        let text = format!("# What each device did to remind each dose (docs/health.md, \"What became of a reminder\").\n{}", toml::to_string(self).map_err(|e| e.to_string())?);
130        crate::health::write_witnessed(path, text.as_bytes())
131    }
132
133    /// The entries of devices that stopped sharing (their own entry says so),
134    /// taken out by any device that reads it: the one exception to "each its
135    /// own" but `STALE_DAYS` (the seventh review's L5). A device only
136    /// forgotten here keeps them: forgetting is one device's view, and its
137    /// entries may still be today's for the others (the eighth review's L6).
138    /// Whether any was.
139    pub fn forget(&mut self, gone: &[String]) -> bool {
140        let mut changed = false;
141        for devices in self.dose.values_mut() {
142            for id in gone {
143                changed |= devices.remove(id).is_some();
144            }
145        }
146        self.dose.retain(|_, devices| !devices.is_empty());
147        changed
148    }
149
150    /// Whether any entry of these devices is here.
151    pub fn holds_any(&self, devices: &[String]) -> bool {
152        self.dose.values().any(|by| devices.iter().any(|id| by.contains_key(id)))
153    }
154
155    /// `device`'s entry for the dose `key`, if it wrote one.
156    pub fn of(&self, key: &str, device: &str) -> Option<Tried> {
157        self.dose.get(key)?.get(device).copied()
158    }
159
160    /// `device`'s entry for the dose `key`, made when missing.
161    pub fn entry(&mut self, key: &str, device: &str) -> &mut Tried {
162        self.dose.entry(key.to_string()).or_default().entry(device.to_string()).or_default()
163    }
164
165    /// Whether a device's notice of the dose was shown, as far as read here.
166    pub fn shown(&self, key: &str) -> bool {
167        self.dose.get(key).is_some_and(|devices| devices.values().any(|t| t.shown > 0))
168    }
169
170    /// When the latest notice of the dose another device than `me` sent went,
171    /// not known shown; none.
172    pub fn maybe_elsewhere(&self, key: &str, me: &str) -> Option<i64> {
173        self.dose.get(key)?.iter().filter(|(device, t)| device.as_str() != me && t.maybe > 0 && t.shown == 0).map(|(_, t)| t.maybe).max()
174    }
175
176    /// The device whose notice of the dose went latest, not known shown, other
177    /// than `me`, and when; none.
178    pub fn maybe_by(&self, key: &str, me: &str) -> Option<(String, i64)> {
179        self.dose.get(key)?.iter().filter(|(device, t)| device.as_str() != me && t.maybe > 0 && t.shown == 0).max_by_key(|(_, t)| t.maybe).map(|(device, t)| (device.clone(), t.maybe))
180    }
181
182    /// When a device's notice of the dose went without being known shown: a
183    /// reminder that may not have shown (its question on the past says so).
184    pub fn maybe_anywhere(&self, key: &str) -> Option<i64> {
185        self.maybe_elsewhere(key, "")
186    }
187}
188
189/// When a dose was due, from its key ("levo@1800000000"); 0 when it does not say.
190fn due_of(key: &str) -> i64 {
191    key.rsplit_once('@').and_then(|(_, due)| due.parse().ok()).unwrap_or(0)
192}
193
194/// Why a phone's alarm cannot know it is a dose's first reminder: a device
195/// that may have reminded it before (`others`).
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub enum Elsewhere {
198    /// It sent a notice of it then, not known shown: it may be on the screen.
199    Sent { device: crate::health::Named, at: i64 },
200    /// Its first try did not go out then, and it may have tried again since.
201    Unshown { device: crate::health::Named, at: i64 },
202    /// In use around the dose's time, and no word of its own on this dose read here.
203    Unknown { device: crate::health::Named },
204    /// Closed then, as last heard, and no word of its own on this dose read
205    /// here; not taken as its word that it did not remind it: this device's
206    /// own pull did not go through since the dose, or something it wrote
207    /// later was read (a claim, an entry: it opened since).
208    Closed { device: crate::health::Named, at: i64 },
209    /// Another phone, or a device whose entry does not say what it is (an
210    /// older Sioul, one not read): its own alarm may remind it too.
211    Alarm { device: crate::health::Named },
212    /// Its clock runs more than ten minutes ahead of this device's: what it
213    /// says, and did, is set aside here (`Peer::set_aside`).
214    Ahead { device: crate::health::Named },
215    /// The reminders' file does not read here: what the others did is not known.
216    Unread,
217}
218
219/// What the other devices did to remind a dose, as a phone's alarm reads it.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub enum Others {
222    /// One was shown somewhere: reminded.
223    Reminded,
224    /// Each other device that could have reminded it is known not to have,
225    /// until `at`: this alarm is its first reminder, plainly.
226    First,
227    /// Not known: why, each device said, the doubt first.
228    Doubt(Vec<Elsewhere>),
229}
230
231/// How far after `since` (the dose's time, or the waking it waited for) a
232/// computer that did not keep the doses then sends no notice of it: past a
233/// phone's alarm (`health::ALARM_AFTER`), its pull, and clocks apart. Its
234/// promise says so (`Tried::quiet`); a computer closed then has it too.
235pub const QUIET_AFTER: i64 = crate::health::ALARM_AFTER + 5 * 60;
236
237/// What the devices other than `me` did to remind the dose `key`, to remind
238/// from `since`, as a phone's alarm reads it at `at` (docs/health.md, "What
239/// became of a reminder"). Of each other device that counts and shares its
240/// doses (`health::counts`):
241/// - its notice shown: reminded;
242/// - its entry set aside here, its clock far ahead of this one's
243///   (`Peer::set_aside`): its times and promises say nothing, said so;
244/// - a notice sent, not known shown: it may be on the screen;
245/// - its promise covering `at` (no notice from it until then, its clock's
246///   lead and `SKEW` allowed), nothing sent: it did not remind it;
247/// - its first try not gone out, no promise covering `at`: it may have tried since;
248/// - no word of its own on the dose: a computer closed cleanly, everything it
249///   wrote read here, did not remind it, until `QUIET_AFTER` past `since`
250///   (opened since, it is never the first before every phone acknowledged its
251///   opening, and promises that much before it may remind), but only for a
252///   phone (`me_phone`; a computer needs no word of another: the seventh
253///   review's C2), for the opening this phone acknowledged or a later one
254///   (`Peer::acknowledged`: M1), when its own pull went through since the
255///   dose (`pulled`) and nothing that computer wrote after its close was read
256///   (a claim renewed while open, an entry of the reminders: the sixth
257///   review's C1); a computer in use may have; another phone's own alarm,
258///   or a device whose entry does not say what it is, may remind it.
259///
260/// `entries`: none when the file does not read here (nothing known).
261/// `me_phone`: the device reading is a phone (a phone is then named, never
262/// "the phone", when two count, it among them).
263#[allow(clippy::too_many_arguments)]
264pub fn others(key: &str, since: i64, at: i64, me: &str, me_phone: bool, entries: Option<&DoseReminders>, peers: &[crate::health::Peer], pulled: bool) -> Others {
265    use crate::health::{Named, SKEW, counts};
266    let Some(entries) = entries else { return Others::Doubt(vec![Elsewhere::Unread]) };
267    if entries.shown(key) {
268        return Others::Reminded;
269    }
270    let counted: Vec<&crate::health::Peer> = peers.iter().filter(|p| p.id != me && counts(p, at) && p.said.as_ref().is_none_or(|s| s.doses)).collect();
271    // Named for the words: "the phone" only when one phone counts.
272    let phones = counted.iter().filter(|p| p.said.as_ref().is_some_and(|s| s.phone)).count() + usize::from(me_phone);
273    let mut doubts = Vec::new();
274    for peer in counted {
275        let phone = peer.said.as_ref().is_some_and(|s| s.phone);
276        let device = Named { id: peer.id.clone(), name: peer.name.clone(), phone: phone && phones == 1 };
277        let margin = SKEW + peer.ahead.max(0);
278        // Its entry set aside, its clock far ahead of this one's: its times say nothing here, its promise neither.
279        if peer.set_aside {
280            doubts.push(Elsewhere::Ahead { device });
281            continue;
282        }
283        match entries.of(key, &peer.id) {
284            Some(tried) if tried.maybe > 0 => doubts.push(Elsewhere::Sent { device, at: tried.maybe }),
285            Some(tried) if tried.quiet == NEVER || tried.quiet.saturating_sub(margin) >= at => {}
286            Some(tried) if tried.unshown > 0 => doubts.push(Elsewhere::Unshown { device, at: tried.unshown }),
287            // A phone, or a device whose entry does not say (it may be one): never "in use" (L3).
288            _ if phone || peer.said.is_none() => doubts.push(Elsewhere::Alarm { device }),
289            _ => match peer.said.as_ref().filter(|s| peer.complete && !s.working && s.closed >= s.started) {
290                Some(said) => {
291                    // Anything it wrote after its close: it opened since.
292                    let later = peer.claimed > said.closed || entries.dose.values().filter_map(|devices| devices.get(&peer.id)).any(|t| t.maybe.max(t.shown).max(t.unshown) > said.closed);
293                    // Its word for a phone only, whose word that computer needs
294                    // before it may be the first; and only for the opening this
295                    // phone acknowledged, or a later one it read (C2, M1).
296                    let ours = me_phone && peer.acknowledged > 0 && said.started >= peer.acknowledged;
297                    if !(ours && pulled && !later && at <= since + QUIET_AFTER - margin) {
298                        doubts.push(Elsewhere::Closed { device, at: said.closed });
299                    }
300                }
301                None => doubts.push(Elsewhere::Unknown { device }),
302            },
303        }
304    }
305    if doubts.is_empty() { Others::First } else { Others::Doubt(doubts) }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use crate::health::{Peer, Said};
312
313    const DUE: i64 = 1_800_000_000;
314    const KEY: &str = "levo@1800000000";
315
316    fn computer(id: &str, exported: i64, working: bool) -> Peer {
317        Peer {
318            name: id.into(),
319            id: id.into(),
320            said: Some(Said { started: DUE - 3_600, closed: if working { 0 } else { DUE - 600 }, working, exported, imported: exported, doses: true, left: false, phone: false }),
321            complete: true,
322            seen: exported,
323            heard: exported,
324            ..Peer::default()
325        }
326    }
327
328    fn phone(id: &str) -> Peer {
329        Peer { said: Some(Said { phone: true, ..computer(id, DUE + 300, false).said.unwrap() }), ..computer(id, DUE + 300, false) }
330    }
331
332    fn with(device: &str, tried: Tried) -> DoseReminders {
333        let mut all = DoseReminders::default();
334        *all.entry(KEY, device) = tried;
335        all
336    }
337
338    /// THE RULE at a phone's alarm, ten minutes after the dose, each case of
339    /// the fifth review: the desk's word read, the doubt said unless it proves
340    /// it did not remind; another phone always a doubt (its own alarm); the
341    /// file unread, a doubt.
342    #[test]
343    fn a_phone_is_first_only_on_the_others_word() {
344        let at = DUE + 600;
345        let desk = computer("desk", DUE + 540, true);
346        // Shown on the desk: reminded.
347        assert_eq!(others(KEY, DUE, at, "me", true, Some(&with("desk", Tried { maybe: DUE + 1, shown: DUE + 2, ..Tried::default() })), std::slice::from_ref(&desk), true), Others::Reminded);
348        // Sent, not known shown (a server answering late, a bus stalled under it): may be on the screen.
349        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&with("desk", Tried { maybe: DUE + 1, ..Tried::default() })), std::slice::from_ref(&desk), true), Others::Doubt(d) if matches!(d[..], [Elsewhere::Sent { at, .. }] if at == DUE + 1)));
350        // Not gone out, then quiet past the alarm, promised: first.
351        let quiet = Tried { unshown: DUE + 5, quiet: DUE + QUIET_AFTER, ..Tried::default() };
352        assert_eq!(others(KEY, DUE, at, "me", true, Some(&with("desk", quiet)), std::slice::from_ref(&desk), true), Others::First);
353        // The same, the alarm late (a phone off, turned on at +20 min): past the promise, a doubt.
354        assert!(matches!(others(KEY, DUE, DUE + 1_200, "me", true, Some(&with("desk", quiet)), std::slice::from_ref(&desk), true), Others::Doubt(d) if matches!(d[..], [Elsewhere::Unshown { .. }])));
355        // Not gone out, no promise read yet (its tries may go on): a doubt.
356        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&with("desk", Tried { unshown: DUE + 5, ..Tried::default() })), std::slice::from_ref(&desk), true), Others::Doubt(_)));
357        // A clock ahead by three minutes: its promise read three minutes shorter.
358        let ahead = Peer { ahead: 180, ..desk.clone() };
359        assert!(matches!(others(KEY, DUE, DUE + QUIET_AFTER - 200, "me", true, Some(&with("desk", quiet)), std::slice::from_ref(&ahead), true), Others::Doubt(_)));
360        // In use, no word on this dose: may have reminded it.
361        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&DoseReminders::default()), std::slice::from_ref(&desk), true), Others::Doubt(d) if matches!(d[..], [Elsewhere::Unknown { .. }])));
362        // Closed before the dose, everything read: did not, until QUIET_AFTER.
363        let closed = Peer { acknowledged: DUE - 3_600, ..computer("desk", DUE - 600, false) };
364        assert_eq!(others(KEY, DUE, at, "me", true, Some(&DoseReminders::default()), std::slice::from_ref(&closed), true), Others::First);
365        assert!(matches!(others(KEY, DUE, DUE + QUIET_AFTER, "me", true, Some(&DoseReminders::default()), std::slice::from_ref(&closed), true), Others::Doubt(_)));
366        // Closed, but this phone's own pull did not go through since the dose: not its word (C1).
367        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&DoseReminders::default()), std::slice::from_ref(&closed), false), Others::Doubt(d) if matches!(d[..], [Elsewhere::Closed { at, .. }] if at == DUE - 600)));
368        // Closed as its entry last came, its claim renewed later while open (R1d): opened since.
369        let claimed = Peer { claimed: DUE - 30, ..closed.clone() };
370        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&DoseReminders::default()), std::slice::from_ref(&claimed), true), Others::Doubt(d) if matches!(d[..], [Elsewhere::Closed { .. }])));
371        // Or an entry of its own on another dose, dated after its close.
372        let mut later = DoseReminders::default();
373        later.entry("iron@1800000000", "desk").maybe = DUE - 60;
374        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&later), std::slice::from_ref(&closed), true), Others::Doubt(_)));
375        // A computer reading: another computer's "closed" is never its word (C2).
376        assert!(matches!(others(KEY, DUE, at, "laptop", false, Some(&DoseReminders::default()), std::slice::from_ref(&closed), true), Others::Doubt(d) if matches!(d[..], [Elsewhere::Closed { .. }])));
377        // An older copy of its entry put back (its opening before the one this phone acknowledged): not its word (M1).
378        let stale = Peer { acknowledged: DUE - 60, ..closed.clone() };
379        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&DoseReminders::default()), std::slice::from_ref(&stale), true), Others::Doubt(_)));
380        // Never acknowledged here: not its word either.
381        let unacknowledged = Peer { acknowledged: 0, ..closed.clone() };
382        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&DoseReminders::default()), std::slice::from_ref(&unacknowledged), true), Others::Doubt(_)));
383        // A device whose entry does not say what it is (an older Sioul): it may ring by its own alarm, never "in use" (L3).
384        let older = Peer { said: None, heard: at - 60, ..desk.clone() };
385        assert!(matches!(others(KEY, DUE, at, "me", true, Some(&DoseReminders::default()), std::slice::from_ref(&older), true), Others::Doubt(d) if matches!(d[..], [Elsewhere::Alarm { .. }])));
386        // Never again, promised (its tries over): first, whenever.
387        assert_eq!(others(KEY, DUE, DUE + 7_200, "me", true, Some(&with("desk", Tried { unshown: DUE + 5, quiet: NEVER, ..Tried::default() })), std::slice::from_ref(&desk), true), Others::First);
388        // Another phone, whatever it wrote of nothing: its own alarm may ring too.
389        let peers = [desk.clone(), phone("tablet")];
390        match others(KEY, DUE, at, "me", true, Some(&with("desk", quiet)), &peers, true) {
391            Others::Doubt(d) => assert!(matches!(&d[..], [Elsewhere::Alarm { device }] if device.name == "tablet" && !device.phone), "{d:?}"),
392            other => panic!("{other:?}"),
393        }
394        // Not read here: nothing known.
395        assert_eq!(others(KEY, DUE, at, "me", true, None, std::slice::from_ref(&desk), true), Others::Doubt(vec![Elsewhere::Unread]));
396        // A device of mine is not another; one said off does not count.
397        assert_eq!(others(KEY, DUE, at, "desk", true, Some(&DoseReminders::default()), std::slice::from_ref(&desk), true), Others::First);
398        assert_eq!(others(KEY, DUE, at, "me", true, Some(&DoseReminders::default()), &[Peer { off: true, ..desk }], true), Others::First);
399    }
400
401    /// The file: each device's entry, written whole and kept two days; a file
402    /// that does not read, or is lost after it was written, is never read empty.
403    #[test]
404    fn each_device_writes_its_own_entry_and_a_lost_file_is_not_empty() {
405        let dir = std::env::temp_dir().join(format!("sioul-dose-reminders-{}", std::process::id()));
406        let _ = std::fs::remove_dir_all(&dir);
407        std::fs::create_dir_all(&dir).unwrap();
408        crate::health::APART.with(|apart| *apart.borrow_mut() = Some(dir.join("apart")));
409        let path = dir.join(FILE);
410        assert_eq!(DoseReminders::read(&path), Ok(DoseReminders::default()), "never written: empty");
411        DoseReminders::update(&path, DUE + 1, "desk", |all| all.entry(KEY, "desk").maybe = DUE + 1).unwrap();
412        DoseReminders::update(&path, DUE + 2, "phone", |all| all.entry(KEY, "phone").shown = DUE + 2).unwrap();
413        let read = DoseReminders::read(&path).unwrap();
414        assert_eq!(read.of(KEY, "desk"), Some(Tried { maybe: DUE + 1, ..Tried::default() }));
415        assert!(read.shown(KEY), "shown on the phone: reminded");
416        assert_eq!(read.maybe_elsewhere(KEY, "phone"), Some(DUE + 1), "the desk's, as the phone reads it");
417        assert_eq!(read.maybe_elsewhere(KEY, "desk"), None, "none but its own, as the desk reads it");
418        let text = std::fs::read_to_string(&path).unwrap();
419        assert!(text.contains("[dose.\"levo@1800000000\".desk]"), "{text}");
420        // A clock three days ahead (R8): its own entries go, never another device's.
421        DoseReminders::update(&path, DUE + 3 * 86_400, "phone", |_| ()).unwrap();
422        let read = DoseReminders::read(&path).unwrap();
423        assert!(read.of(KEY, "phone").is_none() && read.of(KEY, "desk").is_some(), "the desk's entry is the desk's to take out");
424        DoseReminders::update(&path, DUE + 3 * 86_400, "desk", |_| ()).unwrap();
425        assert!(DoseReminders::read(&path).unwrap().dose.is_empty(), "each its own, two days on");
426        // A clock a month ahead (P1): its own old entries go, today's of the others stay; a month-old one goes.
427        let month = 31 * 86_400;
428        DoseReminders::update(&path, DUE + 1, "phone", |all| all.entry(KEY, "phone").shown = DUE + 1).unwrap();
429        DoseReminders::update(&path, DUE + 2, "desk", |all| {
430            all.entry(KEY, "desk").maybe = DUE + 2;
431            all.entry("iron@1797320000", "desk").maybe = 1_797_320_000;
432        })
433        .unwrap();
434        DoseReminders::update(&path, DUE + month, "ahead", |all| all.entry("zinc@1802678400", "ahead").shown = DUE + month).unwrap();
435        let read = DoseReminders::read(&path).unwrap();
436        assert!(read.of(KEY, "phone").is_some() && read.of(KEY, "desk").is_some(), "today's entries of the others kept by a clock a month ahead");
437        assert!(read.of("iron@1797320000", "desk").is_none(), "a month older than the others' newest dose: gone");
438        DoseReminders::update(&path, DUE + 3 * 86_400, "desk", |all| all.dose.clear()).unwrap();
439        // A device gone (stopped sharing, or forgotten): its entries taken out by any device (L5).
440        DoseReminders::update(&path, DUE + 1, "phone", |all| all.entry(KEY, "old-phone").shown = DUE + 1).unwrap();
441        let mut read = DoseReminders::read(&path).unwrap();
442        assert!(read.holds_any(&["old-phone".to_string()]) && read.forget(&["old-phone".to_string()]) && read.dose.is_empty());
443        // Lost after it was written: never empty.
444        std::fs::remove_file(&path).unwrap();
445        assert_eq!(DoseReminders::read(&path), Err(Unsound::Lost));
446        std::fs::write(&path, "[dose.\"x\"").unwrap();
447        assert_eq!(DoseReminders::read(&path), Err(Unsound::Unreadable));
448        assert!(DoseReminders::update(&path, DUE, "desk", |_| ()).is_err(), "nothing written over it");
449        crate::health::APART.with(|apart| *apart.borrow_mut() = None);
450        let _ = std::fs::remove_dir_all(&dir);
451    }
452}