6281c5830c
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
134 lines
6.1 KiB
Rust
134 lines
6.1 KiB
Rust
//! The index must stay correct under incremental updates: a new basename
|
|
//! re-resolves links that were waiting for it, a removal breaks them again,
|
|
//! and a rename only re-keys.
|
|
|
|
use notescraft_index::{Error, Index, Resolution};
|
|
|
|
#[test]
|
|
fn a_new_note_resolves_the_links_waiting_for_it() {
|
|
let mut index = Index::new();
|
|
index.update_note("a/src.md", "[[Later]] [[later]] ![[Later#Head]]\n").unwrap();
|
|
assert_eq!(index.unresolved().len(), 3);
|
|
|
|
index.update_note("deep/Later.md", "# Head\n").unwrap();
|
|
assert_eq!(index.unresolved().len(), 0, "the new basename resolves the old links");
|
|
let outgoing = index.outgoing("a/src.md");
|
|
assert!(outgoing.iter().all(|l| l.resolution.path() == Some("deep/Later.md")));
|
|
assert!(matches!(outgoing.last().map(|l| &l.resolution), Some(Resolution::Resolved { subpath_ok: true, .. })));
|
|
|
|
index.remove_note("deep/Later.md").unwrap();
|
|
assert_eq!(index.unresolved().len(), 3, "removing it breaks them again");
|
|
assert_eq!(index.backlinks("a/src.md"), Vec::new());
|
|
}
|
|
|
|
#[test]
|
|
fn a_new_heading_or_block_repairs_a_subpath() {
|
|
let mut index = Index::new();
|
|
index.update_note("t.md", "text\n").unwrap();
|
|
index.update_note("s.md", "[[t#Fresh]] [[t#^new]]\n").unwrap();
|
|
assert!(index.outgoing("s.md").iter().any(|l| matches!(l.resolution, Resolution::Resolved { subpath_ok: false, .. })));
|
|
|
|
index.update_note("t.md", "text\n\n# Fresh\n\nmore ^new\n").unwrap();
|
|
assert!(index.outgoing("s.md").iter().all(|l| matches!(l.resolution, Resolution::Resolved { subpath_ok: true, .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn ambiguity_disappears_when_the_closer_match_is_removed() {
|
|
let mut index = Index::new();
|
|
index.update_note("far/Doc.md", "").unwrap();
|
|
index.update_note("near/Doc.md", "").unwrap();
|
|
index.update_note("near/src.md", "[[Doc]]\n").unwrap();
|
|
assert!(matches!(index.resolve("near/src.md", "Doc"), Resolution::Resolved { path: ref p, ambiguous: true, .. } if p == "near/Doc.md"));
|
|
|
|
index.remove_note("near/Doc.md").unwrap();
|
|
assert_eq!(index.resolve("near/src.md", "Doc"), Resolution::Resolved { path: String::from("far/Doc.md"), ambiguous: false, subpath_ok: true });
|
|
}
|
|
|
|
#[test]
|
|
fn updating_a_note_replaces_its_links_aliases_and_tags() {
|
|
let mut index = Index::new();
|
|
index.update_note("n.md", "---\naliases: [Old]\ntags: [old]\n---\n[[target]]\n").unwrap();
|
|
index.update_note("target.md", "").unwrap();
|
|
index.update_note("src.md", "[[Old]]\n").unwrap();
|
|
assert_eq!(index.backlinks("n.md").len(), 1, "the alias link from src");
|
|
|
|
index.update_note("n.md", "---\naliases: [New]\ntags: [new]\n---\n").unwrap();
|
|
assert_eq!(index.backlinks("n.md").len(), 0);
|
|
assert!(matches!(index.resolve("src.md", "Old"), Resolution::Unresolved), "the old alias is gone");
|
|
assert!(index.resolve("src.md", "New").is_resolved());
|
|
assert_eq!(index.note("n.md").map(|n| n.tags.clone()), Some(vec![String::from("new")]));
|
|
assert_eq!(index.notes_with_tag("old", false), Vec::<&str>::new());
|
|
assert_eq!(index.notes_with_tag("new", false), vec!["n.md"]);
|
|
}
|
|
|
|
#[test]
|
|
fn rename_rekeys_without_rewriting_any_content() {
|
|
let mut index = Index::new();
|
|
index.update_note("old/Thing.md", "---\naliases: [Thing]\n---\n# Head\n\n[[Thing]] [[other]]\n").unwrap();
|
|
index.update_note("other.md", "[[Thing]]\n").unwrap();
|
|
let before = index.note("old/Thing.md").expect("indexed").clone();
|
|
|
|
index.rename_note("old/Thing.md", "new/Thing.md").unwrap();
|
|
|
|
let after = index.note("new/Thing.md").expect("re-keyed");
|
|
assert_eq!(after.path, "new/Thing.md");
|
|
assert_eq!(after.links, before.links, "content and link ranges are untouched");
|
|
assert_eq!(index.note("old/Thing.md"), None, "the old path is gone");
|
|
assert_eq!(index.notes(), vec!["new/Thing.md", "other.md"]);
|
|
assert_eq!(index.unresolved().len(), 0, "links by basename survive a rename");
|
|
assert_eq!(index.headings("new/Thing.md").map(<[_]>::len), Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn a_rename_into_an_existing_name_replaces_the_target_note() {
|
|
let mut index = Index::new();
|
|
index.update_note("a/Name.md", "first\n").unwrap();
|
|
index.update_note("b/Name.md", "second\n").unwrap();
|
|
index.update_note("src.md", "[[Name]]\n").unwrap();
|
|
|
|
index.rename_note("a/Name.md", "b/Name.md").unwrap();
|
|
assert_eq!(index.notes(), vec!["b/Name.md", "src.md"]);
|
|
assert_eq!(index.note("b/Name.md").map(|n| n.links.len()), Some(0), "the renamed note is indexed");
|
|
assert_eq!(index.note("a/Name.md"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn renames_move_the_folder_tiebreak() {
|
|
let mut index = Index::new();
|
|
index.update_note("m/Doc.md", "").unwrap();
|
|
index.update_note("z/Doc.md", "").unwrap();
|
|
index.update_note("m/src.md", "[[Doc]]\n").unwrap();
|
|
assert_eq!(index.resolve("m/src.md", "Doc").path(), Some("m/Doc.md"));
|
|
|
|
index.rename_note("m/Doc.md", "m2/Doc.md").unwrap();
|
|
let resolution = index.resolve("m/src.md", "Doc");
|
|
assert!(
|
|
matches!(resolution, Resolution::Resolved { ref path, ambiguous: true, .. } if path == "m2/Doc.md"),
|
|
"both Docs match now, the lexicographically smaller one wins: {resolution:?}"
|
|
);
|
|
|
|
index.update_note("m/src.md", "[[m2/Doc]]\n").unwrap();
|
|
assert_eq!(index.resolve("m/src.md", "m2/Doc").path(), Some("m2/Doc.md"), "an explicit path is never ambiguous");
|
|
}
|
|
|
|
#[test]
|
|
fn renaming_an_unknown_note_is_an_error() {
|
|
let mut index = Index::new();
|
|
index.update_note("n.md", "").unwrap();
|
|
assert_eq!(index.rename_note("gone.md", "other.md"), Err(Error::NotIndexed(String::from("gone.md"))));
|
|
assert!(matches!(index.rename_note("n.md", "../evil.md"), Err(Error::OutsideVault(_))));
|
|
assert_eq!(index.notes(), vec!["n.md"], "a failed rename changes nothing");
|
|
}
|
|
|
|
#[test]
|
|
fn remove_note_reports_unknown_paths_and_detaches_the_note() {
|
|
let mut index = Index::new();
|
|
index.update_note("n.md", "[[n]]\n").unwrap();
|
|
assert_eq!(index.remove_note("missing.md"), Err(Error::NotIndexed(String::from("missing.md"))));
|
|
assert_eq!(index.note("n.md").map(|n| n.links.len()), Some(1));
|
|
|
|
index.remove_note("N.MD").expect("matched case-insensitively");
|
|
assert_eq!(index.notes(), Vec::<&str>::new());
|
|
assert!(matches!(index.resolve("n.md", "n"), Resolution::Unresolved));
|
|
}
|