use std::path::PathBuf;
use std::process::Command;
fn corpus_a() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/corpora/corpus-a-canonical")
.canonicalize()
.expect("corpus-a-canonical must exist")
}
fn run(args: &[&str]) -> (i32, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_dbmd"))
.args(args)
.output()
.expect("failed to spawn dbmd");
(
output.status.code().unwrap_or(-1),
String::from_utf8(output.stdout).expect("stdout utf8"),
String::from_utf8(output.stderr).expect("stderr utf8"),
)
}
fn run_ok(args: &[&str]) -> String {
let (code, out, err) = run(args);
assert_eq!(code, 0, "`dbmd {args:?}` exited {code}; stderr:\n{err}");
out
}
fn lines(s: &str) -> Vec<String> {
s.lines().map(|l| l.to_string()).collect()
}
#[test]
fn backlinks_lists_incoming_content_links_sorted_excluding_indexes() {
let dir = corpus_a();
let out = run_ok(&[
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--dir",
dir.to_str().unwrap(),
]);
assert_eq!(
lines(&out),
vec![
"records/companies/northstar",
"records/meetings/2026/04/2026-04-15-northstar-quarterly-review",
"records/meetings/2026/05/2026-05-22-northstar-renewal-call",
"wiki/people/sarah-chen",
"wiki/projects/northstar-renewal",
]
);
}
#[test]
fn backlinks_json_is_a_sorted_string_array() {
let dir = corpus_a();
let out = run_ok(&[
"--json",
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--dir",
dir.to_str().unwrap(),
]);
let arr: Vec<String> = serde_json::from_str(out.trim()).expect("backlinks json array");
assert_eq!(
arr,
vec![
"records/companies/northstar".to_string(),
"records/meetings/2026/04/2026-04-15-northstar-quarterly-review".to_string(),
"records/meetings/2026/05/2026-05-22-northstar-renewal-call".to_string(),
"wiki/people/sarah-chen".to_string(),
"wiki/projects/northstar-renewal".to_string(),
]
);
}
#[test]
fn backlinks_honors_limit() {
let dir = corpus_a();
let out = run_ok(&[
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--dir",
dir.to_str().unwrap(),
"--limit",
"2",
]);
assert_eq!(
lines(&out),
vec![
"records/companies/northstar",
"records/meetings/2026/04/2026-04-15-northstar-quarterly-review",
]
);
}
#[test]
fn backlinks_of_a_target_nobody_links_is_empty_exit_zero() {
let dir = corpus_a();
let (code, out, _err) = run(&[
"graph",
"backlinks",
"sources/emails/2026/05/2026-05-12-marcus-intro.md",
"--dir",
dir.to_str().unwrap(),
]);
assert_eq!(code, 0);
assert!(out.trim().is_empty(), "expected no backlinks, got:\n{out}");
}
#[test]
fn backlinks_type_filter_scopes_to_the_linking_files_type() {
let dir = corpus_a();
let out = run_ok(&[
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--type",
"meeting",
"--dir",
dir.to_str().unwrap(),
]);
assert_eq!(
lines(&out),
vec![
"records/meetings/2026/04/2026-04-15-northstar-quarterly-review",
"records/meetings/2026/05/2026-05-22-northstar-renewal-call",
]
);
}
#[test]
fn backlinks_in_layer_filter_scopes_to_the_linking_files_layer() {
let dir = corpus_a();
let d = dir.to_str().unwrap();
let wiki = run_ok(&[
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--in",
"wiki",
"--dir",
d,
]);
assert_eq!(
lines(&wiki),
vec!["wiki/people/sarah-chen", "wiki/projects/northstar-renewal"]
);
let records = run_ok(&[
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--in",
"records",
"--dir",
d,
]);
assert_eq!(
lines(&records),
vec![
"records/companies/northstar",
"records/meetings/2026/04/2026-04-15-northstar-quarterly-review",
"records/meetings/2026/05/2026-05-22-northstar-renewal-call",
]
);
}
#[test]
fn backlinks_type_and_in_compose() {
let dir = corpus_a();
let d = dir.to_str().unwrap();
let both = run_ok(&[
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--type",
"meeting",
"--in",
"records",
"--dir",
d,
]);
assert_eq!(
lines(&both),
vec![
"records/meetings/2026/04/2026-04-15-northstar-quarterly-review",
"records/meetings/2026/05/2026-05-22-northstar-renewal-call",
]
);
let mismatch = run_ok(&[
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--type",
"meeting",
"--in",
"wiki",
"--dir",
d,
]);
assert!(
mismatch.trim().is_empty(),
"no meeting linker lives under wiki/, got:\n{mismatch}"
);
}
#[test]
fn forwardlinks_type_filter_narrows_targets_to_that_type() {
let dir = corpus_a();
let out = run_ok(&[
"graph",
"forwardlinks",
"wiki/projects/northstar-renewal.md",
"--type",
"contact",
"--dir",
dir.to_str().unwrap(),
]);
assert_eq!(
lines(&out),
vec![
"records/contacts/elena-rodriguez",
"records/contacts/sarah-chen",
]
);
}
#[test]
fn forwardlinks_in_layer_filter_narrows_targets_to_that_layer() {
let dir = corpus_a();
let out = run_ok(&[
"graph",
"forwardlinks",
"wiki/projects/northstar-renewal.md",
"--in",
"sources",
"--dir",
dir.to_str().unwrap(),
]);
assert_eq!(
lines(&out),
vec![
"sources/docs/2026-03-15-northstar-msa",
"sources/emails/2026/05/2026-05-22-elena-renewal",
]
);
}
#[test]
fn forwardlinks_returns_frontmatter_and_body_targets_sorted_deduped() {
let dir = corpus_a();
let out = run_ok(&[
"graph",
"forwardlinks",
"wiki/projects/northstar-renewal.md",
"--dir",
dir.to_str().unwrap(),
]);
assert_eq!(
lines(&out),
vec![
"records/companies/northstar",
"records/contacts/elena-rodriguez",
"records/contacts/sarah-chen",
"records/meetings/2026/04/2026-04-15-northstar-quarterly-review",
"records/meetings/2026/05/2026-05-22-northstar-renewal-call",
"sources/docs/2026-03-15-northstar-msa",
"sources/emails/2026/05/2026-05-22-elena-renewal",
"wiki/synthesis/2026-renewal-plan",
]
);
}
#[test]
fn forwardlinks_and_backlinks_round_trip_on_the_same_key() {
let dir = corpus_a();
let d = dir.to_str().unwrap();
let fwd = run_ok(&[
"graph",
"forwardlinks",
"wiki/projects/northstar-renewal.md",
"--dir",
d,
]);
assert!(lines(&fwd).contains(&"records/contacts/sarah-chen".to_string()));
let back = run_ok(&[
"graph",
"backlinks",
"records/contacts/sarah-chen.md",
"--dir",
d,
]);
assert!(back.lines().any(|l| l == "wiki/projects/northstar-renewal"));
}
#[test]
fn neighborhood_one_hop_hydrates_summaries_in_json() {
let dir = corpus_a();
let out = run_ok(&[
"--json",
"graph",
"neighborhood",
"records/contacts/sarah-chen.md",
"--hops",
"1",
"--dir",
dir.to_str().unwrap(),
]);
let v: serde_json::Value = serde_json::from_str(out.trim()).expect("neighborhood json");
assert_eq!(v["seed"], "records/contacts/sarah-chen");
let nodes = v["nodes"].as_array().expect("nodes array");
let mut paths: Vec<&str> = nodes.iter().map(|n| n["path"].as_str().unwrap()).collect();
paths.sort_unstable();
assert_eq!(
paths,
vec![
"records/companies/northstar",
"records/meetings/2026/04/2026-04-15-northstar-quarterly-review",
"records/meetings/2026/05/2026-05-22-northstar-renewal-call",
"wiki/people/sarah-chen",
"wiki/projects/northstar-renewal",
]
);
for n in nodes {
assert_eq!(n["hops"], 1, "node {n} should be one hop out");
assert!(
!n["summary"].as_str().unwrap().is_empty(),
"node {n} must carry the reached file's summary"
);
assert!(n["type"].is_string(), "node {n} must carry a type");
assert_eq!(
n["via"], "records/contacts/sarah-chen",
"the one-hop edge must originate at the seed"
);
}
let company = nodes
.iter()
.find(|n| n["path"] == "records/companies/northstar")
.expect("company node present");
assert_eq!(company["type"], "company");
assert!(company["summary"].as_str().unwrap().contains("175-seat"));
}
#[test]
fn neighborhood_text_is_tab_separated_path_hops_summary() {
let dir = corpus_a();
let out = run_ok(&[
"graph",
"neighborhood",
"records/contacts/sarah-chen.md",
"--hops",
"1",
"--dir",
dir.to_str().unwrap(),
]);
let rows = lines(&out);
assert_eq!(rows.len(), 5, "five one-hop neighbors");
for row in &rows {
let cols: Vec<&str> = row.split('\t').collect();
assert_eq!(
cols.len(),
3,
"row `{row}` must be path<TAB>hops<TAB>summary"
);
assert_eq!(cols[1], "1", "hop column is 1 for one-hop neighbors");
assert!(!cols[2].is_empty(), "summary column is non-empty");
}
}
#[test]
fn neighborhood_in_layer_filter_narrows_to_that_layer() {
let dir = corpus_a();
let out = run_ok(&[
"graph",
"neighborhood",
"records/contacts/sarah-chen.md",
"--hops",
"1",
"--in",
"wiki",
"--dir",
dir.to_str().unwrap(),
]);
let mut paths: Vec<String> = out
.lines()
.map(|l| l.split('\t').next().unwrap().to_string())
.collect();
paths.sort();
assert_eq!(
paths,
vec![
"wiki/people/sarah-chen".to_string(),
"wiki/projects/northstar-renewal".to_string(),
]
);
}
#[test]
fn neighborhood_type_filter_narrows_to_that_type() {
let dir = corpus_a();
let out = run_ok(&[
"--json",
"graph",
"neighborhood",
"records/contacts/sarah-chen.md",
"--hops",
"1",
"--type",
"meeting",
"--dir",
dir.to_str().unwrap(),
]);
let v: serde_json::Value = serde_json::from_str(out.trim()).unwrap();
let nodes = v["nodes"].as_array().unwrap();
assert!(!nodes.is_empty(), "at least one meeting neighbor");
for n in nodes {
assert_eq!(n["type"], "meeting", "type filter keeps only meetings: {n}");
}
}
#[test]
fn orphans_lists_files_with_no_edges_either_direction() {
let dir = corpus_a();
let out = run_ok(&["graph", "orphans", "--dir", dir.to_str().unwrap()]);
assert_eq!(
lines(&out),
vec![
"sources/emails/2026/04/2026-04-28-aws-invoice-available.md",
"sources/emails/2026/05/2026-05-12-marcus-intro.md",
]
);
}
#[test]
fn orphans_in_layer_scopes_candidates() {
let dir = corpus_a();
let (code, out, err) = run(&[
"graph",
"orphans",
"--in",
"records",
"--dir",
dir.to_str().unwrap(),
]);
assert_eq!(code, 0, "stderr:\n{err}");
assert!(
out.trim().is_empty(),
"no record-layer orphans in the canonical corpus, got:\n{out}"
);
let sources = run_ok(&[
"graph",
"orphans",
"--in",
"sources",
"--dir",
dir.to_str().unwrap(),
]);
assert_eq!(
lines(&sources),
vec![
"sources/emails/2026/04/2026-04-28-aws-invoice-available.md",
"sources/emails/2026/05/2026-05-12-marcus-intro.md",
]
);
}
#[test]
fn orphans_json_is_a_string_array() {
let dir = corpus_a();
let out = run_ok(&["--json", "graph", "orphans", "--dir", dir.to_str().unwrap()]);
let arr: Vec<String> = serde_json::from_str(out.trim()).expect("orphans json array");
assert_eq!(
arr,
vec![
"sources/emails/2026/04/2026-04-28-aws-invoice-available.md".to_string(),
"sources/emails/2026/05/2026-05-12-marcus-intro.md".to_string(),
]
);
}
#[test]
fn tree_layer_scope_groups_type_folders_then_files_indented() {
let dir = corpus_a();
let out = run_ok(&["tree", "--layer", "wiki", "--dir", dir.to_str().unwrap()]);
assert_eq!(
lines(&out),
vec![
"wiki",
" wiki/people",
" wiki/people/elena-rodriguez.md",
" wiki/people/sarah-chen.md",
" wiki/projects",
" wiki/projects/northstar-renewal.md",
" wiki/synthesis",
" wiki/synthesis/2026-renewal-plan.md",
]
);
}
#[test]
fn tree_json_mirrors_the_layer_type_folder_file_structure() {
let dir = corpus_a();
let out = run_ok(&[
"--json",
"tree",
"--layer",
"wiki",
"--dir",
dir.to_str().unwrap(),
]);
let v: serde_json::Value = serde_json::from_str(out.trim()).expect("tree json");
let layers = v["layers"].as_array().unwrap();
assert_eq!(layers.len(), 1, "only the wiki layer");
assert_eq!(layers[0]["layer"], "wiki");
let folders = layers[0]["type_folders"].as_array().unwrap();
let folder_paths: Vec<&str> = folders
.iter()
.map(|f| f["path"].as_str().unwrap())
.collect();
assert_eq!(
folder_paths,
vec!["wiki/people", "wiki/projects", "wiki/synthesis"]
);
let people_files: Vec<&str> = folders[0]["files"]
.as_array()
.unwrap()
.iter()
.map(|f| f.as_str().unwrap())
.collect();
assert_eq!(
people_files,
vec![
"wiki/people/elena-rodriguez.md",
"wiki/people/sarah-chen.md"
]
);
}
#[test]
fn tree_type_filter_keeps_only_the_named_type_folder() {
let dir = corpus_a();
let out = run_ok(&["tree", "--type", "contacts", "--dir", dir.to_str().unwrap()]);
assert_eq!(
lines(&out),
vec![
"records",
" records/contacts",
" records/contacts/david-kim.md",
" records/contacts/elena-rodriguez.md",
" records/contacts/marcus-okafor.md",
" records/contacts/sarah-chen.md",
]
);
}
#[test]
fn stats_json_counts_match_the_corpus_shape() {
let dir = corpus_a();
let out = run_ok(&["--json", "stats", dir.to_str().unwrap()]);
let v: serde_json::Value = serde_json::from_str(out.trim()).expect("stats json");
assert_eq!(v["total_files"], 515);
assert_eq!(v["files_per_layer"]["sources"], 6);
assert_eq!(v["files_per_layer"]["records"], 505);
assert_eq!(v["files_per_layer"]["wiki"], 4);
assert_eq!(v["broken_link_count"], 0);
assert_eq!(v["orphan_count"], 2);
let top = v["top_types"].as_array().unwrap();
assert_eq!(top[0][0], "expense");
assert_eq!(top[0][1], 490);
assert!(v["custom_types_present"].as_array().unwrap().is_empty());
let recognized: Vec<&str> = v["recognized_types_present"]
.as_array()
.unwrap()
.iter()
.map(|t| t.as_str().unwrap())
.collect();
for t in ["contact", "company", "email", "expense", "wiki-page"] {
assert!(
recognized.contains(&t),
"{t} should be a recognized type present"
);
}
}
#[test]
fn stats_text_reports_totals_and_per_layer_lines() {
let dir = corpus_a();
let out = run_ok(&["stats", dir.to_str().unwrap()]);
assert!(out.contains("files: 515"), "totals line:\n{out}");
assert!(out.contains("sources: 6"), "per-layer sources:\n{out}");
assert!(out.contains("records: 505"), "per-layer records:\n{out}");
assert!(out.contains("wiki: 4"), "per-layer wiki:\n{out}");
assert!(out.contains("broken links: 0"), "broken-link line:\n{out}");
assert!(out.contains("orphans: 2"), "orphan line:\n{out}");
}
#[test]
fn outline_lists_only_h2_plus_sections_with_levels_and_body_lines() {
let dir = corpus_a();
let out = run_in(
&dir,
&["--json", "outline", "wiki/projects/northstar-renewal.md"],
);
let v: serde_json::Value = serde_json::from_str(out.trim()).expect("outline json");
assert_eq!(v["file"], "wiki/projects/northstar-renewal.md");
let sections = v["sections"].as_array().unwrap();
let got: Vec<(&str, u64, u64)> = sections
.iter()
.map(|s| {
(
s["heading"].as_str().unwrap(),
s["level"].as_u64().unwrap(),
s["line"].as_u64().unwrap(),
)
})
.collect();
assert_eq!(
got,
vec![("Timeline", 2, 7), ("Commercials", 2, 20)],
"only ##+ headings; the # title is not a section"
);
}
#[test]
fn outline_text_indents_by_heading_depth() {
let dir = corpus_a();
let out = run_in(&dir, &["outline", "wiki/projects/northstar-renewal.md"]);
assert_eq!(lines(&out), vec!["Timeline", "Commercials"]);
}
#[test]
fn outline_of_a_file_with_no_h2_sections_is_empty_exit_zero() {
let dir = corpus_a();
let (code, out, err) = run_in_status(&dir, &["outline", "records/contacts/sarah-chen.md"]);
assert_eq!(code, 0, "stderr:\n{err}");
assert!(
out.trim().is_empty(),
"no ## sections in a contact record:\n{out}"
);
}
fn run_in(dir: &std::path::Path, args: &[&str]) -> String {
let (code, out, err) = run_in_status(dir, args);
assert_eq!(
code, 0,
"`dbmd {args:?}` (cwd {dir:?}) exited {code}; stderr:\n{err}"
);
out
}
fn run_in_status(dir: &std::path::Path, args: &[&str]) -> (i32, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_dbmd"))
.args(args)
.current_dir(dir)
.output()
.expect("failed to spawn dbmd");
(
output.status.code().unwrap_or(-1),
String::from_utf8(output.stdout).expect("stdout utf8"),
String::from_utf8(output.stderr).expect("stderr utf8"),
)
}