Files
notescraft/crates/engine/tests/calendar_reminders.rs
Marcos Paulo 15d7f5e873 feat: editor blocks and lint, vault knowledge, calendar plugin, macOS menu
- 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>
2026-10-18 12:36:00 -03:00

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(&reg, &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(&reg, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:19" }));
assert_eq!(early["notify"], json!([]));
let due = run(&reg, &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(&reg, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:21" }))["notify"], json!([]), "never twice");
let shown = run(&reg, &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(&reg, &mut session, "calendar.reminder_snooze", &json!({ "key": key, "minutes": 5, "now": "2026-10-20T09:21" }));
assert_eq!(run(&reg, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:25" }))["notify"], json!([]), "still snoozed");
let back = run(&reg, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T09:26" }));
assert_eq!(titles(&back["notify"]), ["Standup"]);
run(&reg, &mut session, "calendar.reminder_dismiss", &json!({ "key": key }));
let gone = run(&reg, &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(&reg, &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(&reg, &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(&reg, &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(&reg, &mut session, "calendar.reminders_tick", &json!({ "now": "2026-10-20T23:00", "catch_up": true }));
let due = run(&reg, &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, &params).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(&reg, &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(&reg, &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, &params).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());
}