use super::normalize_slug;
use crate::graph::storage::GraphRead;
use crate::okf::build::build;
use crate::okf::build::tests_support::{edges_of, provisional_count, vault_build, write};
use crate::okf::model::BuildOptions;
use tempfile::tempdir;
#[test]
fn vault_path_links_resolve_by_path_not_by_last_segment() {
let dir = tempdir().unwrap();
write(dir.path(), "Docs/Guide.md", "---\nid: g1\n---\nvendor copy");
write(dir.path(), "notes/guide.md", "my notes");
write(dir.path(), "a.md", "See [g](notes/guide.md).");
let opts = BuildOptions::for_dialect(crate::okf::Dialect::Obsidian);
let out = build(dir.path(), &opts).unwrap();
assert_eq!(out.report.dangling, 0, "a path link is not a dangling link");
assert_eq!(provisional_count(&out.graph), 0);
let g = &out.graph;
let targets: Vec<String> = g
.graph
.edge_indices()
.filter(|&e| g.graph[e].connection_type_str(&g.interner) == "LINKS_TO")
.filter_map(|e| g.graph.edge_endpoints(e))
.filter_map(|(_, t)| {
g.node_view(t)
.map(|nd| nd.node_type_str(&g.interner).to_string())
})
.collect();
assert_eq!(
targets,
vec!["notes".to_string()],
"the link lands on notes/guide.md (label `notes`), not on Docs/Guide.md"
);
}
#[test]
fn vault_wikilinks_resolve_by_id_and_by_path() {
let dir = tempdir().unwrap();
write(dir.path(), "notes/meeting.md", "---\nid: mtg-1\n---\nleaf");
write(dir.path(), "wing/sub/target.md", "leaf");
write(dir.path(), "wing/other.md", "leaf");
write(
dir.path(),
"wing/a.md",
"[[mtg-1]] and [[wing/sub/target]] and [[sub/target]]",
);
let out = vault_build(dir.path());
assert_eq!(out.report.dangling, 0, "no rung fell through to a stub");
let targets: Vec<String> = edges_of(&out.graph)
.into_iter()
.filter(|(_, c, _, _)| c == "LINKS_TO")
.map(|(_, _, t, _)| t)
.collect();
assert_eq!(
targets,
vec!["mtg-1".to_string(), "target".to_string()],
"both spellings reach that note, and land on one edge: they sit in \
the same section with no anchor, so VAULT.md §5.4 makes them one \
relationship"
);
}
#[test]
fn vault_alias_resolves_a_link_and_is_not_a_stub() {
let dir = tempdir().unwrap();
write(
dir.path(),
"seismic.md",
"---\naliases:\n- Seismic interpretation\n---\nleaf",
);
write(dir.path(), "a.md", "see [[Seismic interpretation]]");
let out = vault_build(dir.path());
assert_eq!(out.report.dangling, 0);
assert_eq!(
edges_of(&out.graph),
vec![("a".into(), "LINKS_TO".into(), "seismic".into(), vec![])]
);
write(
dir.path(),
"seismic.md",
"---\naliases: Seismic interpretation\n---\nleaf",
);
assert_eq!(vault_build(dir.path()).report.dangling, 0);
}
#[test]
fn vault_alias_collisions_warn() {
let dir = tempdir().unwrap();
write(dir.path(), "atlas.md", "leaf");
write(
dir.path(),
"one.md",
"---\naliases:\n- atlas\n---\nclaims a stem",
);
write(dir.path(), "two.md", "---\naliases:\n- shared\n---\nleaf");
write(dir.path(), "three.md", "---\naliases:\n- shared\n---\nleaf");
let r = vault_build(dir.path()).report;
assert_eq!(r.warnings.len(), 2, "{:?}", r.warnings);
assert!(
r.warnings[0].contains("alias `atlas` on one.md") && r.warnings[0].contains("`atlas`"),
"{}",
r.warnings[0]
);
assert!(
r.warnings[1].contains("alias `shared`")
&& r.warnings[1].contains("`three`")
&& r.warnings[1].contains("`two`"),
"{}",
r.warnings[1]
);
}
#[test]
fn slug_normalization_unifies_separators() {
assert_eq!(
normalize_slug("Project_0-10 Shipped"),
"project-0-10-shipped"
);
assert_eq!(
normalize_slug("feedback_cypher_first"),
"feedback-cypher-first"
);
assert_eq!(normalize_slug("--A__B--"), "a-b");
}
#[test]
fn resolves_slug_and_title_variants_without_dangling() {
let dir = tempdir().unwrap();
write(
dir.path(),
"feedback_cypher_first.md",
"---\ntype: Note\ntitle: Cypher First\n---\nleaf",
);
write(
dir.path(),
"a.md",
"---\ntype: Note\n---\nsee [[feedback-cypher-first]] and [[Cypher First]]",
);
let opts = BuildOptions::for_dialect(crate::okf::model::Dialect::Loose);
let g = build(dir.path(), &opts).unwrap().graph;
assert_eq!(g.graph.node_indices().count(), 2);
assert_eq!(provisional_count(&g), 0);
}
#[test]
fn genuinely_missing_target_still_dangles() {
let dir = tempdir().unwrap();
write(
dir.path(),
"a.md",
"---\ntype: Note\n---\nsee [[truly-absent]]",
);
let opts = BuildOptions::for_dialect(crate::okf::model::Dialect::Loose);
let g = build(dir.path(), &opts).unwrap().graph;
assert_eq!(provisional_count(&g), 1);
}