177 lines
8.5 KiB
Rust
177 lines
8.5 KiB
Rust
//! Periodic notes: the weekly, monthly, quarterly and yearly notes
|
|
//! (`note.periodic`).
|
|
|
|
mod common;
|
|
|
|
use common::{on_disk, seed, temp_vault};
|
|
|
|
use notescraft_engine::{Error, PeriodKind, Registry, Session, SessionConfig, period_bounds, register_core};
|
|
use notescraft_templates::Date;
|
|
use serde_json::{Value, json};
|
|
|
|
/// A registry and a session; `config` picks the folders, formats and templates.
|
|
fn fixture(name: &str, config: SessionConfig) -> (Registry<Session>, Session, common::TempVault) {
|
|
let vault = temp_vault(name);
|
|
seed(vault.path(), &[("Templates/Week.md", "Week {{title}}\nFrom {{start}} to {{end}}\n")]);
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
let session = Session::open_with(vault.path(), config).expect("open");
|
|
(registry, session, vault)
|
|
}
|
|
|
|
/// The path the command answered with.
|
|
fn path_of(answer: &Value) -> String {
|
|
answer.get("path").and_then(Value::as_str).expect("a path").to_owned()
|
|
}
|
|
|
|
/// The weekly note follows the ISO weeks, whose year differs from the
|
|
/// calendar year at the turn of the year.
|
|
#[test]
|
|
fn the_weekly_note_follows_the_iso_weeks() {
|
|
let (reg, mut session, vault) = fixture("periodic-week", SessionConfig::default());
|
|
|
|
let opened = reg.run("note.periodic", &mut session, &json!({ "kind": "weekly", "date": "2020-12-31" })).expect("note.periodic");
|
|
assert_eq!(path_of(&opened), "Weekly/2020-W53.md");
|
|
assert_eq!(opened.get("created"), Some(&json!(true)));
|
|
|
|
// 2021-01-03 is a Sunday of the same ISO week: the note is already there.
|
|
let same = reg.run("note.periodic", &mut session, &json!({ "kind": "weekly", "date": "2021-01-03" })).expect("note.periodic");
|
|
assert_eq!(path_of(&same), "Weekly/2020-W53.md");
|
|
assert_eq!(same.get("created"), Some(&json!(false)), "the week already has its note");
|
|
|
|
// The turn of 2026: 2026-12-31 and 2027-01-01 share the ISO week 53.
|
|
let edge = reg.run("note.periodic", &mut session, &json!({ "kind": "weekly", "date": "2026-12-31" })).expect("note.periodic");
|
|
assert_eq!(path_of(&edge), "Weekly/2026-W53.md");
|
|
let edge = reg.run("note.periodic", &mut session, &json!({ "kind": "weekly", "date": "2027-01-01" })).expect("note.periodic");
|
|
assert_eq!(path_of(&edge), "Weekly/2026-W53.md");
|
|
assert_eq!(edge.get("created"), Some(&json!(false)), "the same ISO week");
|
|
|
|
assert!(on_disk(vault.path(), "Weekly/2026-W53.md").is_empty(), "a note without a template starts empty");
|
|
}
|
|
|
|
/// The monthly, quarterly and yearly notes are named after their period.
|
|
#[test]
|
|
fn the_other_notes_are_named_after_their_period() {
|
|
let (reg, mut session, _vault) = fixture("periodic-periods", SessionConfig::default());
|
|
|
|
let monthly = reg.run("note.periodic", &mut session, &json!({ "kind": "monthly", "date": "2026-10-13" })).expect("note.periodic");
|
|
assert_eq!(path_of(&monthly), "Monthly/2026-10.md");
|
|
|
|
let quarterly = reg.run("note.periodic", &mut session, &json!({ "kind": "quarterly", "date": "2026-10-13" })).expect("note.periodic");
|
|
assert_eq!(path_of(&quarterly), "Quarterly/2026-Q4.md");
|
|
|
|
let yearly = reg.run("note.periodic", &mut session, &json!({ "kind": "yearly", "date": "2026-10-13" })).expect("note.periodic");
|
|
assert_eq!(path_of(&yearly), "Yearly/2026.md");
|
|
}
|
|
|
|
/// A periodic note is created once and never overwritten.
|
|
#[test]
|
|
fn a_periodic_note_is_created_once() {
|
|
let (reg, mut session, vault) = fixture("periodic-once", SessionConfig::default());
|
|
|
|
let first = reg.run("note.periodic", &mut session, &json!({ "kind": "monthly", "date": "2026-10-13" })).expect("note.periodic");
|
|
assert_eq!(first.get("created"), Some(&json!(true)));
|
|
let _ = on_disk(vault.path(), "Monthly/2026-10.md");
|
|
|
|
let second = reg.run("note.periodic", &mut session, &json!({ "kind": "monthly", "date": "2026-10-13" })).expect("note.periodic");
|
|
assert_eq!(second.get("created"), Some(&json!(false)), "idempotent second call");
|
|
assert_eq!(path_of(&second), "Monthly/2026-10.md");
|
|
}
|
|
|
|
/// The template of the kind is expanded, with the bounds of the period.
|
|
#[test]
|
|
fn a_periodic_note_is_created_from_its_template() {
|
|
let mut config = SessionConfig::default();
|
|
config.periodic.weekly.template = String::from("Templates/Week.md");
|
|
let (reg, mut session, vault) = fixture("periodic-template", config);
|
|
|
|
let opened = reg.run("note.periodic", &mut session, &json!({ "kind": "weekly", "date": "2026-10-14" })).expect("note.periodic");
|
|
assert_eq!(path_of(&opened), "Weekly/2026-W42.md");
|
|
let content = on_disk(vault.path(), "Weekly/2026-W42.md");
|
|
assert_eq!(content, "Week 2026-W42\nFrom 2026-10-12 to 2026-10-18\n", "the title and the bounds of the week");
|
|
}
|
|
|
|
/// The folder of the kind is created with the note.
|
|
#[test]
|
|
fn the_folder_of_a_periodic_note_is_created() {
|
|
let (reg, mut session, vault) = fixture("periodic-folder", SessionConfig::default());
|
|
assert!(!vault.path().join("Weekly").exists(), "the folder is not there yet");
|
|
|
|
reg.run("note.periodic", &mut session, &json!({ "kind": "weekly", "date": "2026-10-14" })).expect("note.periodic");
|
|
assert!(vault.path().join("Weekly").is_dir(), "the folder is created with the note");
|
|
assert!(vault.path().join("Weekly/2026-W42.md").is_file(), "and so is the note");
|
|
}
|
|
|
|
/// A format that cannot be used is a settings error: nothing is created.
|
|
#[test]
|
|
fn a_bad_format_is_refused_and_nothing_is_created() {
|
|
let vault = temp_vault("periodic-bad-format");
|
|
|
|
let mut empty = SessionConfig::default();
|
|
empty.periodic.weekly.format = String::from(" ");
|
|
match Session::open_with(vault.path(), empty) {
|
|
Err(Error::Config(message)) => assert!(message.contains("periodic.weekly.format"), "{message}"),
|
|
other => panic!("an empty format must be refused, got {other:?}"),
|
|
}
|
|
|
|
let mut escaping = SessionConfig::default();
|
|
escaping.periodic.weekly.format = String::from("../up");
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
let mut session = Session::open_with(vault.path(), escaping).expect("open");
|
|
match registry.run("note.periodic", &mut session, &json!({ "kind": "weekly", "date": "2026-10-14" })) {
|
|
Err(Error::Command { source, .. }) => assert_eq!(source.code, "config", "{}", source.message),
|
|
other => panic!("a format that leaves the vault must be refused, got {other:?}"),
|
|
}
|
|
assert!(!vault.path().join("Weekly").exists(), "nothing was created");
|
|
}
|
|
|
|
/// A read-only session creates nothing.
|
|
#[test]
|
|
fn a_read_only_session_refuses_a_periodic_note() {
|
|
let vault = temp_vault("periodic-read-only");
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
let mut session = Session::open_read_only(vault.path()).expect("open");
|
|
|
|
match registry.run("note.periodic", &mut session, &json!({ "kind": "weekly", "date": "2026-10-14" })) {
|
|
Err(Error::Disabled(id)) => assert_eq!(id, "note.periodic"),
|
|
other => panic!("a read-only session must refuse the command, got {other:?}"),
|
|
}
|
|
assert!(!vault.path().join("Weekly").exists(), "nothing was created");
|
|
}
|
|
|
|
/// A kind nobody knows is refused.
|
|
#[test]
|
|
fn an_unknown_kind_is_refused() {
|
|
let (reg, mut session, _vault) = fixture("periodic-kind", SessionConfig::default());
|
|
for bad in ["daily", "Weekly", ""] {
|
|
match reg.run("note.periodic", &mut session, &json!({ "kind": bad })) {
|
|
Err(Error::Command { source, .. }) => assert_eq!(source.code, "invalid_params", "`{bad}`: {}", source.message),
|
|
other => panic!("`{bad}` must be refused, got {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The bounds of a period are the ones the UI steps through.
|
|
#[test]
|
|
fn the_period_bounds_cover_the_period() {
|
|
let date = Date::new(2026, 10, 14).expect("a date");
|
|
|
|
let (start, end) = period_bounds(PeriodKind::Weekly, date).expect("the bounds of the week");
|
|
assert_eq!((start.year(), start.month(), start.day()), (2026, 10, 12));
|
|
assert_eq!((end.year(), end.month(), end.day()), (2026, 10, 18));
|
|
|
|
let (start, end) = period_bounds(PeriodKind::Monthly, date).expect("the bounds of the month");
|
|
assert_eq!((start.month(), start.day()), (10, 1));
|
|
assert_eq!((end.month(), end.day()), (10, 31));
|
|
|
|
let (start, end) = period_bounds(PeriodKind::Quarterly, date).expect("the bounds of the quarter");
|
|
assert_eq!((start.month(), start.day()), (10, 1));
|
|
assert_eq!((end.month(), end.day()), (12, 31));
|
|
|
|
let (start, end) = period_bounds(PeriodKind::Yearly, date).expect("the bounds of the year");
|
|
assert_eq!((start.month(), start.day()), (1, 1));
|
|
assert_eq!((end.month(), end.day()), (12, 31));
|
|
}
|