Skip to main content

entracte_lib/scheduler/commands/
chores.rs

1use crate::chores_store::sanitize_items;
2
3use super::super::chores::{persist_chores, rollover_if_new_day, ChoresState};
4use super::super::timers::local_today_string;
5use super::super::Scheduler;
6
7/// Today's chore list. Rolls over to an empty list if local midnight has
8/// passed since it was last entered (a chore post-it is a fresh thing each
9/// morning), persisting the reset so the overlay and the settings editor
10/// always agree on "today".
11#[tauri::command]
12pub async fn get_chores(scheduler: tauri::State<'_, Scheduler>) -> Result<ChoresState, String> {
13    Ok(get_chores_impl(scheduler.inner()).await)
14}
15
16/// Replace today's chore list with the user's edited lines. The list is
17/// trimmed, de-blanked, and capped before it is stored, so the overlay only
18/// ever sees clean, bounded text. Returns the stored state so the editor can
19/// re-seed from the canonical (sanitized) list.
20#[tauri::command]
21pub async fn set_chores(
22    scheduler: tauri::State<'_, Scheduler>,
23    items: Vec<String>,
24) -> Result<ChoresState, String> {
25    Ok(set_chores_impl(scheduler.inner(), items).await)
26}
27
28/// AppHandle-free core of [`get_chores`] so unit tests can drive it.
29pub(crate) async fn get_chores_impl(scheduler: &Scheduler) -> ChoresState {
30    let today = local_today_string();
31    let mut c = scheduler.chores.lock().await;
32    if rollover_if_new_day(&mut c, &today) {
33        persist_chores(&scheduler.chores_path, &c);
34    }
35    c.clone()
36}
37
38/// AppHandle-free core of [`set_chores`] so unit tests can drive it.
39pub(crate) async fn set_chores_impl(scheduler: &Scheduler, items: Vec<String>) -> ChoresState {
40    let today = local_today_string();
41    let cleaned = sanitize_items(items);
42    let mut c = scheduler.chores.lock().await;
43    rollover_if_new_day(&mut c, &today);
44    c.items = cleaned;
45    // Saving any real chore marks the user as a chore-user for good, so the
46    // morning prompt keeps nudging them on future (empty) mornings. Never
47    // cleared — clearing today's list doesn't mean they've stopped using it.
48    if !c.items.is_empty() {
49        c.ever_used_chores = true;
50    }
51    persist_chores(&scheduler.chores_path, &c);
52    c.clone()
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::scheduler::types::BreakKind;
59    use crate::scheduler::Settings;
60    use crate::test_support::test_scheduler;
61
62    #[tokio::test]
63    async fn set_then_get_round_trips_and_sanitizes() {
64        let (_dir, sched) = test_scheduler(Settings::default());
65        let stored = set_chores_impl(
66            &sched,
67            vec![
68                "  Water the plants  ".to_string(),
69                "   ".to_string(),
70                "Reply to Sam".to_string(),
71            ],
72        )
73        .await;
74        assert_eq!(
75            stored.items,
76            vec!["Water the plants".to_string(), "Reply to Sam".to_string()]
77        );
78        // A fresh read sees the same sanitized list (persisted + in memory).
79        let read = get_chores_impl(&sched).await;
80        assert_eq!(read.items, stored.items);
81    }
82
83    #[tokio::test]
84    async fn set_chores_marks_and_keeps_the_chore_user_flag() {
85        let (_dir, sched) = test_scheduler(Settings::default());
86        // Saving an all-blank (→ empty) list does not flip the flag.
87        let empty = set_chores_impl(&sched, vec!["   ".to_string()]).await;
88        assert!(empty.items.is_empty());
89        assert!(!empty.ever_used_chores);
90        // Saving a real chore marks the user as a chore-user...
91        let used = set_chores_impl(&sched, vec!["Water the plants".to_string()]).await;
92        assert!(used.ever_used_chores);
93        // ...and clearing the list again keeps it set — they still use chores,
94        // so the morning prompt should keep nudging them.
95        let cleared = set_chores_impl(&sched, vec![]).await;
96        assert!(cleared.items.is_empty());
97        assert!(cleared.ever_used_chores);
98    }
99
100    #[tokio::test]
101    async fn get_rolls_over_a_stale_day_to_empty() {
102        let (_dir, sched) = test_scheduler(Settings::default());
103        set_chores_impl(&sched, vec!["Yesterday".to_string()]).await;
104        // Backdate the in-memory list so the next read crosses a day boundary.
105        {
106            let mut c = sched.chores.lock().await;
107            c.date = "2000-01-01".to_string();
108        }
109        let read = get_chores_impl(&sched).await;
110        assert!(read.items.is_empty(), "stale list cleared on read");
111    }
112
113    #[tokio::test]
114    async fn resolve_chore_prompt_long_cycles_micro_skips() {
115        let (_dir, sched) = test_scheduler(Settings::default());
116        // Empty list: no nudge even on a long break.
117        assert_eq!(sched.resolve_chore_prompt(BreakKind::Long).await, None);
118
119        set_chores_impl(&sched, vec!["a".to_string(), "b".to_string()]).await;
120        assert_eq!(
121            sched.resolve_chore_prompt(BreakKind::Long).await.as_deref(),
122            Some("a")
123        );
124        assert_eq!(
125            sched.resolve_chore_prompt(BreakKind::Long).await.as_deref(),
126            Some("b")
127        );
128        // Micro never draws and never advances the rotation.
129        assert_eq!(sched.resolve_chore_prompt(BreakKind::Micro).await, None);
130        assert_eq!(
131            sched.resolve_chore_prompt(BreakKind::Long).await.as_deref(),
132            Some("a")
133        );
134    }
135}
136
137// Integration-test rig: drives the `#[tauri::command]` wrappers end-to-end
138// through a mock app so the `tauri::State` extraction lines are covered. The
139// `_impl` tests above own the behaviour coverage; these only prove the thin
140// wrappers thread the state through.
141#[cfg(all(test, not(target_os = "windows")))]
142mod rig_tests {
143    use super::*;
144    use crate::scheduler::Settings;
145    use crate::test_support::mock_app_with_scheduler;
146    use tauri::Manager;
147
148    #[tokio::test]
149    async fn get_and_set_chores_commands_via_rig() {
150        let (_dir, app, _sched) = mock_app_with_scheduler(Settings::default());
151        let stored = set_chores(app.state::<Scheduler>(), vec!["Mow the lawn".to_string()])
152            .await
153            .expect("set_chores succeeds");
154        assert_eq!(stored.items, vec!["Mow the lawn".to_string()]);
155        let read = get_chores(app.state::<Scheduler>())
156            .await
157            .expect("get_chores succeeds");
158        assert_eq!(read.items, vec!["Mow the lawn".to_string()]);
159    }
160}