Files
2026-10-14 12:45:32 -03:00

169 lines
8.3 KiB
Rust

//! Bookmark commands share the registry and atomic private vault state.
use notescraft_engine::{Error, Registry, Session, register_core};
use serde_json::{Value, json};
mod common;
fn registry() -> Registry<Session> {
let mut registry = Registry::new();
register_core(&mut registry).expect("register commands");
registry
}
#[test]
fn add_list_remove_and_dedupe_by_path_and_heading() {
let vault = common::temp_vault("bookmark-commands");
common::seed(vault.path(), &[("folder/note.md", "# Heading\n"), ("map.canvas", "{}")]);
let mut session = Session::open(vault.path()).expect("session");
let registry = registry();
let list = |session: &mut Session| registry.run("bookmarks.list", session, &Value::Null).expect("list");
assert_eq!(list(&mut session), json!({"bookmarks": []}));
let note = json!({"path": "folder/note.md", "title": "My note"});
assert_eq!(registry.run("bookmarks.add", &mut session, &note).expect("add")["added"], true);
assert_eq!(registry.run("bookmarks.add", &mut session, &note).expect("dedupe")["added"], false);
let heading = json!({"path": "folder/note.md", "heading": "Heading", "title": "Jump here"});
registry.run("bookmarks.add", &mut session, &heading).expect("heading");
registry.run("bookmarks.add", &mut session, &json!({"path":"map.canvas"})).expect("canvas");
assert_eq!(
list(&mut session),
json!({"bookmarks": [
{"path":"folder/note.md", "heading":null, "title":"My note"},
{"path":"folder/note.md", "heading":"Heading", "title":"Jump here"},
{"path":"map.canvas", "heading":null, "title":null}
]})
);
let remove = json!({"path":"folder/note.md", "heading":"Heading"});
assert_eq!(registry.run("bookmarks.remove", &mut session, &remove).expect("remove")["removed"], true);
assert_eq!(registry.run("bookmarks.remove", &mut session, &remove).expect("absent")["removed"], false);
assert_eq!(list(&mut session)["bookmarks"].as_array().expect("array").len(), 2);
}
#[test]
fn bookmarks_persist_across_session_reopen_and_removal() {
let vault = common::temp_vault("bookmark-reopen");
common::seed(vault.path(), &[("note.md", "# Heading\n")]);
let registry = registry();
let params = json!({"path":"note.md", "heading":"Heading", "title":"Saved"});
let expected;
{
let mut session = Session::open(vault.path()).expect("session");
registry.run("bookmarks.add", &mut session, &params).expect("add");
expected = registry.run("bookmarks.list", &mut session, &Value::Null).expect("list");
assert!(vault.path().join(".notescraft/bookmarks.json").is_file());
assert!(!vault.path().join(".obsidian").exists());
}
{
let mut session = Session::open(vault.path()).expect("reopen");
assert_eq!(registry.run("bookmarks.list", &mut session, &Value::Null).expect("list"), expected);
registry.run("bookmarks.remove", &mut session, &json!({"path":"note.md", "heading":"Heading"})).expect("remove");
}
let mut session = Session::open(vault.path()).expect("reopen removal");
assert_eq!(registry.run("bookmarks.list", &mut session, &Value::Null).expect("list"), json!({"bookmarks":[]}));
}
#[test]
fn read_only_sessions_list_but_cannot_change_bookmarks() {
let vault = common::temp_vault("bookmark-read-only");
common::seed(vault.path(), &[("note.md", "# Note\n")]);
let registry = registry();
{
let mut session = Session::open(vault.path()).expect("session");
registry.run("bookmarks.add", &mut session, &json!({"path":"note.md"})).expect("add");
}
let mut session = Session::open_read_only(vault.path()).expect("read only");
assert_eq!(registry.run("bookmarks.list", &mut session, &Value::Null).expect("list")["bookmarks"].as_array().expect("array").len(), 1);
for id in ["bookmarks.add", "bookmarks.remove"] {
assert!(matches!(registry.run(id, &mut session, &json!({"path":"note.md"})), Err(Error::Disabled(_))));
}
}
#[test]
fn invalid_targets_and_fields_never_write_state() {
let vault = common::temp_vault("bookmark-invalid");
common::seed(vault.path(), &[("note.md", "# Note\n")]);
let mut session = Session::open(vault.path()).expect("session");
let registry = registry();
for params in [
json!({"path":"../note.md"}),
json!({"path":"/note.md"}),
json!({"path":".obsidian/note.md"}),
json!({"path":".notescraft/note.md"}),
json!({"path":"missing.md"}),
json!({"path":"./note.md"}),
json!({"path":"note.md", "heading":""}),
json!({"path":"note.md", "title":""}),
json!({"path":"note.md", "heading":42}),
json!({"path":"note.md", "title":"x".repeat(1025)}),
] {
assert!(registry.run("bookmarks.add", &mut session, &params).is_err(), "{params}");
}
assert!(!vault.path().join(".notescraft/bookmarks.json").exists());
}
#[test]
fn damaged_private_state_is_reported_and_preserved() {
let vault = common::temp_vault("bookmark-corrupt");
common::seed(vault.path(), &[("note.md", "# Note\n")]);
let mut session = Session::open(vault.path()).expect("session");
session.vault().write_private(".notescraft/bookmarks.json", b"broken json").expect("seed corrupt state");
let registry = registry();
for id in ["bookmarks.list", "bookmarks.add", "bookmarks.remove"] {
let params = if id == "bookmarks.list" { Value::Null } else { json!({"path":"note.md"}) };
assert!(registry.run(id, &mut session, &params).is_err());
}
assert_eq!(session.vault().read(".notescraft/bookmarks.json").expect("preserved"), b"broken json");
}
#[test]
fn note_rename_moves_bookmark_targets_and_preserves_heading_title() {
let vault = common::temp_vault("bookmark-rename");
common::seed(vault.path(), &[("note.md", "# Heading\n"), ("taken.md", "taken")]);
let mut session = Session::open(vault.path()).expect("session");
let registry = registry();
registry.run("bookmarks.add", &mut session, &json!({"path":"note.md", "heading":"Heading", "title":"Saved"})).expect("add");
assert!(registry.run("note.rename", &mut session, &json!({"from":"note.md", "to":"taken.md"})).is_err());
assert_eq!(registry.run("bookmarks.list", &mut session, &Value::Null).expect("list")["bookmarks"][0]["path"], "note.md");
registry.run("note.rename", &mut session, &json!({"from":"note.md", "to":"folder/moved.md"})).expect("move");
assert_eq!(
registry.run("bookmarks.list", &mut session, &Value::Null).expect("list"),
json!({"bookmarks":[
{"path":"folder/moved.md", "heading":"Heading", "title":"Saved"}
]})
);
drop(session);
let mut reopened = Session::open(vault.path()).expect("reopen");
assert_eq!(registry.run("bookmarks.list", &mut reopened, &Value::Null).expect("list")["bookmarks"][0]["path"], "folder/moved.md");
}
#[test]
fn deleted_file_bookmark_can_still_be_removed() {
let vault = common::temp_vault("bookmark-deleted");
common::seed(vault.path(), &[("note.md", "# Note\n")]);
let mut session = Session::open(vault.path()).expect("session");
let registry = registry();
registry.run("bookmarks.add", &mut session, &json!({"path":"note.md"})).expect("add");
session.delete_note("note.md").expect("delete note");
assert_eq!(registry.run("bookmarks.remove", &mut session, &json!({"path":"note.md"})).expect("remove")["removed"], true);
}
#[test]
fn invalid_stored_bookmarks_never_get_repaired_or_overwritten() {
let vault = common::temp_vault("bookmark-state-invalid");
common::seed(vault.path(), &[("note.md", "# Note\n")]);
let mut session = Session::open(vault.path()).expect("session");
let registry = registry();
for state in [
json!([{"path":"../note.md"}]),
json!([{"path":"note.md"}, {"path":"note.md"}]),
json!([{"path":"note.md", "title":" "}]),
json!(vec![json!({"path":"note.md"}); 1025]),
] {
let bytes = serde_json::to_vec(&state).expect("json");
session.vault().write_private(".notescraft/bookmarks.json", &bytes).expect("seed state");
assert!(registry.run("bookmarks.list", &mut session, &Value::Null).is_err(), "{state}");
assert!(registry.run("bookmarks.add", &mut session, &json!({"path":"note.md"})).is_err());
assert_eq!(session.vault().read(".notescraft/bookmarks.json").expect("preserved"), bytes);
}
}