//! Folder and duplicate commands share the headless registry surface. mod common; use common::{seed, temp_vault}; use notescraft_engine::{Registry, Session, register_core}; use serde_json::json; #[test] fn folder_commands_move_links_bookmarks_buffers_and_attachments() { let vault = temp_vault("folders"); seed( vault.path(), &[("old/A.md", "[[old/deep/B]]"), ("old/deep/B.md", "[[old/A]]"), ("root.md", "[[old/A]] [[old/deep/B]] ![[old/pic.png]]"), ("old/pic.png", "image")], ); let mut session = Session::open(vault.path()).expect("session"); session.add_bookmark("old/A.md", None, None).expect("bookmark"); session.open_buffer("old/A.md").expect("buffer"); session.buffer_mut("old/A.md").expect("buffer").edit(0..0, "unsaved ").expect("edit"); let mut reg = Registry::new(); register_core(&mut reg).expect("registry"); reg.run("folder.create", &mut session, &json!({"path":"empty"})).expect("create"); assert!(vault.path().join("empty").is_dir()); reg.run("folder.rename", &mut session, &json!({"from":"old","to":"new"})).expect("rename"); assert_eq!(session.read_note("root.md").expect("read"), "[[new/A]] [[new/deep/B]] ![[new/pic.png]]"); assert_eq!(session.read_note("new/A.md").expect("read"), "[[new/deep/B]]"); assert_eq!(session.read_note("new/deep/B.md").expect("read"), "[[new/A]]"); assert_eq!(session.bookmarks().expect("bookmarks").first().map(|b| b.path.as_str()), Some("new/A.md")); assert!(session.buffer("new/A.md").is_some_and(|buffer| buffer.is_dirty() && buffer.text().starts_with("unsaved"))); assert!(session.index().files().contains(&"new/pic.png")); assert!(session.index().note("old/A.md").is_none()); reg.run("folder.delete", &mut session, &json!({"path":"new"})).expect("trash"); assert!(!vault.path().join("new").exists()); assert!(session.index().note("new/A.md").is_none()); assert!(!session.index().files().contains(&"new/pic.png")); assert!(session.buffer("new/A.md").is_none()); } #[test] fn duplicate_preserves_content_and_uses_first_free_copy_name() { let vault = temp_vault("duplicate"); seed(vault.path(), &[("sub/Note.md", "héllo\r\n"), ("sub/Note copy.md", "occupied")]); let mut session = Session::open(vault.path()).expect("session"); let mut reg = Registry::new(); register_core(&mut reg).expect("registry"); let result = reg.run("note.duplicate", &mut session, &json!({"path":"sub/Note.md"})).expect("duplicate"); assert_eq!(result.get("path"), Some(&json!("sub/Note copy 2.md"))); assert_eq!(session.read_note("sub/Note copy 2.md").expect("read"), "héllo\r\n"); assert!(session.index().note("sub/Note copy 2.md").is_some()); } #[test] fn folders_refuse_root_reserved_collisions_and_read_only() { let vault = temp_vault("folder-guards"); seed(vault.path(), &[("old/A.md", "body"), ("taken/B.md", "body")]); let mut session = Session::open(vault.path()).expect("session"); let mut reg = Registry::new(); register_core(&mut reg).expect("registry"); for path in ["", ".", ".obsidian", ".notescraft", "../outside"] { for id in ["folder.create", "folder.delete"] { assert!(reg.run(id, &mut session, &json!({"path":path})).is_err(), "{id}: {path}"); } assert!(reg.run("folder.rename", &mut session, &json!({"from":"old","to":path})).is_err()); } for to in ["taken", "old/nested"] { assert!(reg.run("folder.rename", &mut session, &json!({"from":"old","to":to})).is_err()); } let mut readonly = Session::open_read_only(vault.path()).expect("readonly"); for id in ["folder.create", "folder.rename", "folder.delete", "note.duplicate"] { assert!(!reg.is_enabled(id, &readonly)); assert!(reg.run(id, &mut readonly, &json!({"path":"old"})).is_err()); } } #[cfg(unix)] #[test] fn folder_aliases_cannot_move_vault_root_or_private_state() { let vault = temp_vault("folder-symlinks"); let mut session = Session::open(vault.path()).expect("session"); std::os::unix::fs::symlink(vault.path(), vault.path().join("alias")).expect("symlink"); std::os::unix::fs::symlink(vault.path().join(".notescraft"), vault.path().join("private")).expect("symlink"); let mut reg = Registry::new(); register_core(&mut reg).expect("registry"); for path in ["alias", "private"] { assert!(reg.run("folder.rename", &mut session, &json!({"from":path,"to":"moved"})).is_err()); } assert!(vault.path().is_dir()); assert!(vault.path().join(".notescraft").is_dir()); } #[test] fn folder_move_without_link_updates_keeps_links_and_moves_bookmarks() { let vault = temp_vault("folder-no-rewrite"); seed(vault.path(), &[("old/A.md", "body"), ("root.md", "[[old/A]]")]); let mut session = Session::open(vault.path()).expect("session"); session.add_bookmark("old/A.md", Some("Heading"), None).expect("bookmark"); let mut reg = Registry::new(); register_core(&mut reg).expect("registry"); reg.run("folder.rename", &mut session, &json!({"from":"old","to":"new","update_links":false})).expect("rename"); assert_eq!(session.read_note("root.md").expect("read"), "[[old/A]]"); assert_eq!(session.bookmarks().expect("bookmarks").first().map(|b| b.path.as_str()), Some("new/A.md")); } #[test] fn renamed_links_refresh_clean_buffers_and_search() { let vault = temp_vault("rename-mirrors"); seed(vault.path(), &[("A.md", "# A"), ("root.md", "[[A]]")]); let mut session = Session::open(vault.path()).expect("session"); session.open_buffer("root.md").expect("buffer"); let mut reg = Registry::new(); register_core(&mut reg).expect("registry"); reg.run("note.rename", &mut session, &json!({"from":"A.md","to":"Zebra.md"})).expect("rename"); assert_eq!(session.buffer("root.md").map(notescraft_engine::Buffer::text), Some("[[Zebra]]")); let result = reg.run("search", &mut session, &json!({"query":"Zebra"})).expect("search"); assert!(result.to_string().contains("root.md")); }