15d7f5e873
- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity - Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys - Visual view: tables, note embeds, %% comments, wrapping property chips - Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin - Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
132 lines
6.7 KiB
Rust
132 lines
6.7 KiB
Rust
//! The reminder commands over a fixture vault with an injected clock.
|
|
|
|
mod common;
|
|
|
|
use common::{seed, temp_vault};
|
|
use notescraft_engine::{Registry, RemindersConfig, Session, SessionConfig, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
const STANDUP: &str = "---\ntitle: Standup\nstart: 2026-10-20T09:30\nreminders: [10]\n---\n";
|
|
|
|
fn registry() -> Registry<Session> {
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
registry
|
|
}
|
|
|
|
fn run(registry: &Registry<Session>, session: &mut Session, id: &str, params: &Value) -> Value {
|
|
registry.run(id, session, params).unwrap_or_else(|error| panic!("{id}: {error:?}"))
|
|
}
|
|
|
|
fn titles(alerts: &Value) -> Vec<&str> {
|
|
alerts.as_array().unwrap().iter().map(|alert| alert["title"].as_str().unwrap()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn a_reminder_fires_once_snoozes_and_is_dismissed() {
|
|
let vault = temp_vault("rem-flow");
|
|
seed(vault.path(), &[("Calendar/standup.md", STANDUP)]);
|
|
let reg = registry();
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
|
|
let start = run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:00", "catch_up": true }));
|
|
assert_eq!(start["notify"], json!([]));
|
|
assert!(vault.path().join(".notescraft/calendar-reminders.json").exists(), "the clock is remembered");
|
|
|
|
let early = run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:19" }));
|
|
assert_eq!(early["notify"], json!([]));
|
|
let due = run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:20" }));
|
|
assert_eq!(titles(&due["notify"]), ["Standup"]);
|
|
let key = due["notify"][0]["key"].as_str().unwrap().to_owned();
|
|
assert_eq!(run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:21" }))["notify"], json!([]), "never twice");
|
|
|
|
let shown = run(®, &mut session, "calendar.reminders", &json!({ "now": "2026-10-20T09:21" }));
|
|
assert_eq!(titles(&shown["inbox"]["due"]), ["Standup"]);
|
|
assert_eq!(shown["snooze_minutes"], json!([5, 15, 60]));
|
|
|
|
run(®, &mut session, "calendar.reminder_snooze", &json!({ "key": key, "minutes": 5, "now": "2026-10-20T09:21" }));
|
|
assert_eq!(run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:25" }))["notify"], json!([]), "still snoozed");
|
|
let back = run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:26" }));
|
|
assert_eq!(titles(&back["notify"]), ["Standup"]);
|
|
|
|
run(®, &mut session, "calendar.reminder_dismiss", &json!({ "key": key }));
|
|
let gone = run(®, &mut session, "calendar.reminders", &json!({ "now": "2026-10-20T09:27" }));
|
|
assert_eq!((gone["inbox"]["due"].clone(), gone["inbox"]["upcoming"].clone()), (json!([]), json!([])));
|
|
}
|
|
|
|
#[test]
|
|
fn what_fired_while_closed_is_missed_after_a_restart() {
|
|
let vault = temp_vault("rem-missed");
|
|
seed(vault.path(), &[("Calendar/standup.md", STANDUP)]);
|
|
let reg = registry();
|
|
{
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T08:00", "catch_up": true }));
|
|
}
|
|
let mut session = Session::open(vault.path()).expect("reopen");
|
|
let start = run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T12:00", "catch_up": true }));
|
|
assert_eq!(start["notify"], json!([]), "a missed alert is not notified as new");
|
|
assert_eq!(titles(&start["missed"]), ["Standup"]);
|
|
let shown = run(®, &mut session, "calendar.reminders", &json!({ "now": "2026-10-20T12:01" }));
|
|
assert_eq!(titles(&shown["inbox"]["missed"]), ["Standup"]);
|
|
}
|
|
|
|
#[test]
|
|
fn quiet_hours_and_default_reminders_come_from_the_config() {
|
|
let vault = temp_vault("rem-config");
|
|
seed(vault.path(), &[("Calendar/plain.md", "---\ntitle: Plain\nstart: 2026-10-20T23:30\n---\n")]);
|
|
let config = SessionConfig {
|
|
reminders: RemindersConfig { quiet_enabled: true, default_minutes: vec![10], ..RemindersConfig::default() },
|
|
..SessionConfig::default()
|
|
};
|
|
let reg = registry();
|
|
let mut session = Session::open_with(vault.path(), config).expect("open");
|
|
run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T23:00", "catch_up": true }));
|
|
let due = run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T23:20" }));
|
|
assert_eq!(due["notify"], json!([]), "quiet hours are on from 22:00");
|
|
assert_eq!(titles(&due["quiet"]), ["Plain"], "the default reminder applied to an event with none");
|
|
}
|
|
|
|
#[test]
|
|
fn bad_input_and_a_damaged_state_file_are_harmless() {
|
|
let vault = temp_vault("rem-bad");
|
|
seed(vault.path(), &[("Calendar/standup.md", STANDUP), (".notescraft/calendar-reminders.json", "{ not json")]);
|
|
let reg = registry();
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
for params in [json!({ "now": "tomorrow" }), json!({ "now": 5 }), json!({ "utc_offset_minutes": "x" })] {
|
|
assert!(reg.run("calendar.reminders", &mut session, ¶ms).is_err(), "{params}");
|
|
}
|
|
assert!(reg.run("calendar.reminder_snooze", &mut session, &json!({ "key": "nope|2026-10-20T09:30|10", "now": "2026-10-20T09:00" })).is_err());
|
|
assert!(reg.run("calendar.reminder_snooze", &mut session, &json!({ "key": "k", "minutes": 0 })).is_err());
|
|
let fine = run(®, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:20", "catch_up": true }));
|
|
assert_eq!(fine["notify"], json!([]), "a damaged file starts afresh");
|
|
}
|
|
|
|
#[test]
|
|
fn a_read_only_session_reads_but_never_writes_reminder_state() {
|
|
let vault = temp_vault("rem-readonly");
|
|
seed(vault.path(), &[("Calendar/standup.md", STANDUP)]);
|
|
let reg = registry();
|
|
let mut session = Session::open_read_only(vault.path()).expect("open");
|
|
run(®, &mut session, "calendar.reminders", &json!({ "now": "2026-10-20T09:00" }));
|
|
for (id, params) in [
|
|
("calendar.reminders_tick", json!({ "now": "2026-10-20T09:00" })),
|
|
("calendar.reminder_snooze", json!({ "key": "k" })),
|
|
("calendar.reminder_dismiss", json!({ "key": "k" })),
|
|
] {
|
|
assert!(reg.run(id, &mut session, ¶ms).is_err(), "{id}");
|
|
}
|
|
assert!(!vault.path().join(".notescraft/calendar-reminders.json").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn bad_reminder_settings_are_refused() {
|
|
for bad in [r#"{"reminders": {"quiet_start": 1440}}"#, r#"{"reminders": {"default_minutes": [0]}}"#, r#"{"reminders": {"snooze_minutes": [99999999]}}"#] {
|
|
let config: SessionConfig = serde_json::from_str(bad).unwrap();
|
|
assert!(config.validate().is_err(), "{bad}");
|
|
let mut repaired = config;
|
|
assert!(!repaired.repair().is_empty() && repaired.validate().is_ok(), "{bad}");
|
|
}
|
|
assert!(SessionConfig::default().validate().is_ok());
|
|
}
|