Files
Marcos Paulo ca0d68f9ab
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: export.pandoc, properties.rename_all, first wasm plugin (dice) (MiniMax M3.1 Flash)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 06:39:51 -03:00

230 lines
10 KiB
Rust

//! Integration tests of the property maintenance commands: renaming a key
//! in every note, the conflicts, the dry run, the values of a key and the
//! keys of the vault.
mod common;
use common::{on_disk, seed, temp_vault};
use notescraft_engine::{Error, Registry, Session, register_core};
use serde_json::{Value, json};
/// A registry with every core command.
fn registry() -> Registry<Session> {
let mut reg = Registry::new();
register_core(&mut reg).expect("registry");
reg
}
/// The code of a command failure, when the outcome is one.
fn failure_code(outcome: &Result<Value, Error>) -> Option<String> {
match outcome {
Err(Error::Command { source, .. }) => Some(source.code.clone()),
_ => None,
}
}
/// The paths of a report array, empty when it is not there.
fn paths(report: &Value, name: &str) -> Vec<String> {
report.get(name).and_then(Value::as_array).map(|rows| rows.iter().filter_map(|row| row.as_str().map(str::to_owned)).collect()).unwrap_or_default()
}
/// A key is renamed in every note that holds it; a note that already holds
/// the new key is a conflict and is left alone.
#[test]
fn a_key_is_renamed_in_every_note() {
let vault = temp_vault("prop-rename");
seed(
vault.path(),
&[
("a.md", "---\nstatus: open\n---\nbody a\n"),
("b.md", "---\nstatus: closed\n---\nbody b\n"),
("c.md", "---\nstatus: open\n---\nbody c\n"),
("d.md", "---\nstatus: open\n---\nbody d\n"),
("e.md", "---\nstatus: open\nstate: yes\n---\nbody e\n"),
("f.md", "no frontmatter here\n"),
],
);
let mut session = Session::open(vault.path()).expect("session");
let reg = registry();
let report = reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename");
assert_eq!(report.get("renamed"), Some(&json!(4)), "{report}");
assert_eq!(paths(&report, "notes"), vec![String::from("a.md"), String::from("b.md"), String::from("c.md"), String::from("d.md")], "{report}");
assert_eq!(paths(&report, "conflicts"), vec![String::from("e.md")], "{report}");
assert_eq!(on_disk(vault.path(), "a.md"), "---\nstate: open\n---\nbody a\n");
assert_eq!(on_disk(vault.path(), "e.md"), "---\nstatus: open\nstate: yes\n---\nbody e\n", "the conflict is left alone");
assert_eq!(on_disk(vault.path(), "f.md"), "no frontmatter here\n", "a note without the key is left alone");
}
/// The body of a note survives the rename, byte for byte.
#[test]
fn the_body_of_a_note_is_untouched() {
let vault = temp_vault("prop-body");
let body = "# Title\n\nA rule ---\n\nstatus: not a key\n";
seed(vault.path(), &[("a.md", &format!("---\nstatus: open\n---\n{body}"))]);
let mut session = Session::open(vault.path()).expect("session");
let reg = registry();
reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename");
assert_eq!(on_disk(vault.path(), "a.md"), format!("---\nstate: open\n---\n{body}"));
}
/// A dry run reports what would change and writes nothing at all.
#[test]
fn a_dry_run_changes_no_file() {
let vault = temp_vault("prop-dry-run");
seed(vault.path(), &[("a.md", "---\nstatus: open\n---\nbody\n")]);
let mut session = Session::open(vault.path()).expect("session");
let reg = registry();
let before = on_disk(vault.path(), "a.md");
let report = reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state", "dry_run": true })).expect("dry run");
assert_eq!(report.get("renamed"), Some(&json!(1)), "{report}");
assert_eq!(report.get("dry_run"), Some(&json!(true)), "{report}");
assert_eq!(on_disk(vault.path(), "a.md"), before, "the dry run wrote nothing");
reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename");
assert_eq!(on_disk(vault.path(), "a.md"), "---\nstate: open\n---\nbody\n");
}
/// The values of a key are counted, and the items of a list count one by
/// one.
#[test]
fn the_values_of_a_key_are_counted() {
let vault = temp_vault("prop-values");
seed(
vault.path(),
&[
("a.md", "---\nlabels: [work, home]\nstatus: open\n---\n"),
("b.md", "---\nlabels: [work]\nstatus: closed\n---\n"),
("c.md", "---\nlabels:\n - work\n - away\nstatus: open\n---\n"),
],
);
let mut session = Session::open(vault.path()).expect("session");
let reg = registry();
let report = reg.run("properties.values", &mut session, &json!({ "key": "labels" })).expect("values");
assert_eq!(report.get("type"), Some(&json!("list")), "{report}");
assert_eq!(report.get("total_notes"), Some(&json!(3)), "{report}");
assert_eq!(
report.get("values"),
Some(&json!([
{ "value": "work", "count": 3 },
{ "value": "away", "count": 1 },
{ "value": "home", "count": 1 },
])),
"{report}"
);
let scalars = reg.run("properties.values", &mut session, &json!({ "key": "status" })).expect("values");
assert_eq!(scalars.get("type"), Some(&json!("text")), "{scalars}");
assert_eq!(scalars.get("values"), Some(&json!([{ "value": "open", "count": 2 }, { "value": "closed", "count": 1 }])), "{scalars}");
let limited = reg.run("properties.values", &mut session, &json!({ "key": "labels", "limit": 1 })).expect("values");
assert_eq!(limited.get("values"), Some(&json!([{ "value": "work", "count": 3 }])), "{limited}");
}
/// Every key of the vault is listed, the most used first.
#[test]
fn the_keys_of_the_vault_are_sorted_by_count() {
let vault = temp_vault("prop-keys");
seed(vault.path(), &[("a.md", "---\nstatus: open\nlabels: [work]\n---\n"), ("b.md", "---\nstatus: closed\n---\n"), ("c.md", "---\nstatus: open\n---\n")]);
let mut session = Session::open(vault.path()).expect("session");
let reg = registry();
let report = reg.run("properties.keys", &mut session, &json!({})).expect("keys");
assert_eq!(
report.get("keys"),
Some(&json!([
{ "key": "status", "type": "text", "count": 3 },
{ "key": "labels", "type": "list", "count": 1 },
])),
"{report}"
);
}
/// A property name may hold spaces.
#[test]
fn a_key_with_spaces_works() {
let vault = temp_vault("prop-spaces");
seed(vault.path(), &[("a.md", "---\nmy key: value\n---\nbody\n")]);
let mut session = Session::open(vault.path()).expect("session");
let reg = registry();
let report = reg.run("properties.rename_all", &mut session, &json!({ "from": "my key", "to": "other key" })).expect("rename");
assert_eq!(report.get("renamed"), Some(&json!(1)), "{report}");
assert_eq!(on_disk(vault.path(), "a.md"), "---\nother key: value\n---\nbody\n");
}
/// A name that cannot be a frontmatter key is refused, on both sides of the
/// rename.
#[test]
fn an_invalid_key_is_refused() {
let vault = temp_vault("prop-invalid");
seed(vault.path(), &[("a.md", "---\nstatus: open\n---\n")]);
let mut session = Session::open(vault.path()).expect("session");
let reg = registry();
for key in ["", "a:b", "a\nb"] {
let from = reg.run("properties.rename_all", &mut session, &json!({ "from": key, "to": "state" }));
assert_eq!(failure_code(&from), Some(String::from("invalid_params")), "`{key}` must be refused");
let to = reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": key }));
assert_eq!(failure_code(&to), Some(String::from("invalid_params")), "`{key}` must be refused");
let values = reg.run("properties.values", &mut session, &json!({ "key": key }));
assert_eq!(failure_code(&values), Some(String::from("invalid_params")), "`{key}` must be refused");
}
assert_eq!(on_disk(vault.path(), "a.md"), "---\nstatus: open\n---\n", "nothing was written");
}
/// `.obsidian/types.json` is read-only: the report says the old key had a
/// type there and the file is untouched.
#[test]
fn the_types_file_is_read_only() {
let vault = temp_vault("prop-types");
seed(vault.path(), &[("a.md", "---\nstatus: open\n---\n"), (".obsidian/types.json", "{\"types\":{\"status\":\"text\"}}")]);
let mut session = Session::open(vault.path()).expect("session");
let reg = registry();
let report = reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename");
assert_eq!(report.get("types_json_note"), Some(&json!(true)), "{report}");
assert_eq!(on_disk(vault.path(), ".obsidian/types.json"), "{\"types\":{\"status\":\"text\"}}", "the types file is never edited");
}
/// A note open in the editor gets one undoable edit, the disk waits.
#[test]
fn an_open_buffer_gets_one_undoable_edit() {
let vault = temp_vault("prop-buffer");
seed(vault.path(), &[("a.md", "---\nstatus: open\n---\nbody\n")]);
let mut session = Session::open(vault.path()).expect("session");
session.open_buffer("a.md").expect("buffer");
let reg = registry();
reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename");
let buffer = session.buffer("a.md").expect("open buffer");
assert_eq!(buffer.text(), "---\nstate: open\n---\nbody\n");
assert!(buffer.is_dirty(), "the buffer edit marks the note dirty");
assert!(buffer.can_undo(), "one undo step covers the rewrite");
assert_eq!(on_disk(vault.path(), "a.md"), "---\nstatus: open\n---\nbody\n", "the disk still holds what was saved last");
let edit = session.buffer_mut("a.md").expect("buffer");
edit.undo().expect("undo");
assert_eq!(edit.text(), "---\nstatus: open\n---\nbody\n", "undo takes the rename back");
}
/// A read-only session refuses the write and keeps the two reads.
#[test]
fn a_read_only_session_refuses_the_write_only() {
let vault = temp_vault("prop-read-only");
seed(vault.path(), &[("a.md", "---\nstatus: open\n---\n")]);
let mut session = Session::open_read_only(vault.path()).expect("read-only session");
let reg = registry();
assert!(!reg.is_enabled("properties.rename_all", &session));
assert!(reg.is_enabled("properties.values", &session));
assert!(reg.is_enabled("properties.keys", &session));
assert!(matches!(reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })), Err(Error::Disabled(_))));
assert!(reg.run("properties.values", &mut session, &json!({ "key": "status" })).is_ok());
assert!(reg.run("properties.keys", &mut session, &json!({})).is_ok());
assert_eq!(on_disk(vault.path(), "a.md"), "---\nstatus: open\n---\n");
}