use std::path::Path;
use dbmd_core::index::Index;
use dbmd_core::store::Store;
use dbmd_core::validate::{codes, validate_all, validate_working_set, Issue};
fn write(root: &Path, rel: &str, contents: &str) {
let abs = root.join(rel);
std::fs::create_dir_all(abs.parent().unwrap()).unwrap();
std::fs::write(abs, contents).unwrap();
}
fn fresh_store(dir: &Path) {
std::fs::write(
dir.join("DB.md"),
"---\ntype: db-md\nscope: company\nowner: Test\n---\n",
)
.unwrap();
for layer in ["sources", "records"] {
std::fs::create_dir_all(dir.join(layer)).unwrap();
}
}
fn open(dir: &Path) -> Store {
Store::open_strict(dir).expect("tempdir has a valid DB.md")
}
fn contact(summary: &str) -> String {
format!(
"---\ntype: contact\ncreated: 2026-05-22T10:00:00-07:00\nupdated: 2026-05-22T10:00:00-07:00\nsummary: \"{summary}\"\nname: A\n---\n\n# A\n"
)
}
fn has(issues: &[Issue], code: &str) -> bool {
issues.iter().any(|i| i.code == code)
}
#[test]
fn regression_index_summary_with_middle_dot_does_not_false_positive() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
fresh_store(root);
write(
root,
"records/companies/acme.md",
&contact("Acme · Q2 renewal"),
);
let store = open(root);
Index::rebuild_all(&store).unwrap();
let issues = validate_all(&store).unwrap();
assert!(
!has(&issues, codes::INDEX_SUMMARY_MISMATCH),
"a middle-dot summary on a freshly-rebuilt store must not desync: {issues:#?}"
);
assert!(
!issues.iter().any(Issue::is_error),
"clean store with a middle-dot summary should have no errors: {issues:#?}"
);
}
#[test]
fn regression_index_summary_strips_real_double_spaced_tag_suffix() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
fresh_store(root);
write(
root,
"records/contacts/a.md",
"---\ntype: contact\ncreated: 2026-05-22T10:00:00-07:00\nupdated: 2026-05-22T10:00:00-07:00\nsummary: \"clean summary\"\nname: A\ntags:\n - vip\n---\n\n# A\n",
);
let store = open(root);
Index::rebuild_all(&store).unwrap();
let issues = validate_all(&store).unwrap();
assert!(
!has(&issues, codes::INDEX_SUMMARY_MISMATCH),
"the renderer's ` · #tag` suffix must be stripped before compare: {issues:#?}"
);
}
#[test]
fn regression_working_set_validates_archived_changed_file() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
fresh_store(root);
write(
root,
"records/contacts/sarah-chen.md",
"---\ntype: contact\ncreated: 2026-05-22T10:00:00-07:00\nupdated: 2026-05-31T14:00:00-07:00\nsummary: \"changed in May, never validated\"\nname: Sarah\n---\n\nSee [[tom]].\n",
);
write(root, "records/contacts/tom.md", &contact("created in June"));
write(
root,
"log.md",
"---\ntype: log\n---\n\n## [2026-06-01 08:00] create | records/contacts/tom\n",
);
write(
root,
"log/2026-05.md",
"## [2026-05-30 09:00] validate\nPASS\n\n## [2026-05-31 14:00] update | records/contacts/sarah-chen\nedited\n",
);
let store = open(root);
let issues = validate_working_set(&store, None).unwrap();
assert!(
issues.iter().any(|i| i.code == codes::WIKI_LINK_SHORT_FORM
&& i.file == Path::new("records/contacts/sarah-chen.md")),
"the archived May change must be validated, surfacing its short-form link: {issues:#?}"
);
}
#[test]
fn regression_working_set_cutoff_reads_archived_validate_entry() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
fresh_store(root);
write(
root,
"records/contacts/before.md",
"---\ntype: contact\ncreated: 2026-05-20T10:00:00-07:00\nupdated: 2026-05-20T10:00:00-07:00\nsummary: \"changed before validate\"\nname: B\n---\n\nSee [[ghost]].\n",
);
write(
root,
"records/contacts/after.md",
"---\ntype: contact\ncreated: 2026-06-02T10:00:00-07:00\nupdated: 2026-06-02T10:00:00-07:00\nsummary: \"changed after validate\"\nname: A\n---\n\nSee [[phantom]].\n",
);
write(
root,
"log.md",
"---\ntype: log\n---\n\n## [2026-06-02 10:00] update | records/contacts/after\n",
);
write(
root,
"log/2026-05.md",
"## [2026-05-20 10:00] update | records/contacts/before\nx\n\n## [2026-05-30 09:00] validate\nPASS\n",
);
let store = open(root);
let issues = validate_working_set(&store, None).unwrap();
assert!(
issues
.iter()
.any(|i| i.file == Path::new("records/contacts/after.md")),
"post-validate change must be in the working set: {issues:#?}"
);
assert!(
!issues
.iter()
.any(|i| i.file == Path::new("records/contacts/before.md")),
"pre-validate change (before the archived cutoff) must be excluded: {issues:#?}"
);
}
#[test]
fn regression_index_summary_internal_whitespace_does_not_false_positive() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
fresh_store(root);
write(
root,
"records/companies/acme.md",
&contact("Partner; our operating co"),
);
write(root, "records/companies/beta.md", &contact("Ops\tlead co"));
let store = open(root);
Index::rebuild_all(&store).unwrap();
let issues = validate_all(&store).unwrap();
assert!(
!has(&issues, codes::INDEX_SUMMARY_MISMATCH),
"internal-whitespace summaries on a freshly-rebuilt store must not desync: {issues:#?}"
);
assert!(
!issues.iter().any(Issue::is_error),
"a clean store with internal-whitespace summaries must have no errors: {issues:#?}"
);
}
#[test]
fn regression_db_md_folders_section_is_recognized() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
std::fs::write(
root.join("DB.md"),
"---\ntype: db-md\nscope: company\nowner: Test\n---\n\n# Store\n\n## Folders\n\n- records/contacts|Contacts — people we have met\n",
)
.unwrap();
for layer in ["sources", "records"] {
std::fs::create_dir_all(root.join(layer)).unwrap();
}
write(root, "records/contacts/a.md", &contact("a contact"));
let store = open(root);
Index::rebuild_all(&store).unwrap();
let issues = validate_all(&store).unwrap();
assert!(
!has(&issues, codes::DB_MD_UNKNOWN_SECTION),
"`## Folders` is a recognized DB.md section and must not be flagged: {issues:#?}"
);
std::fs::write(
root.join("DB.md"),
"---\ntype: db-md\nscope: company\nowner: Test\n---\n\n# Store\n\n## Bogus\n\n- nope\n",
)
.unwrap();
let store2 = open(root);
let issues2 = validate_all(&store2).unwrap();
assert!(
has(&issues2, codes::DB_MD_UNKNOWN_SECTION),
"an unrecognized DB.md section must still warn: {issues2:#?}"
);
}
#[test]
fn regression_validate_working_set_does_not_escape_store_via_log_object() {
let host = tempfile::TempDir::new().unwrap();
let root = host.path().join("mid").join("store");
std::fs::create_dir_all(&root).unwrap();
fresh_store(&root);
std::fs::write(
root.parent().unwrap().join("leaky.md"),
"---\ntype: contact\ncreated: TOP-SECRET\nsummary: secret\nname: X\n---\n\n# x\n",
)
.unwrap();
write(&root, "records/contacts/real.md", &contact("real one"));
write(
&root,
"log.md",
"---\ntype: log\n---\n\n## [2026-06-01 08:00] create | records/contacts/real\n## [2026-06-01 08:01] update | records/../../leaky\n",
);
let store = open(&root);
let issues = validate_working_set(&store, None).unwrap();
assert!(
!issues
.iter()
.any(|i| i.file.to_string_lossy().contains("leaky")),
"validate must not read/report a file outside the store via a `..` log object: {issues:#?}"
);
}
#[cfg(unix)]
#[test]
fn regression_validate_all_follows_symlinked_content_file() {
use std::os::unix::fs::symlink;
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
fresh_store(root);
let real = root.join("external/bio.md");
std::fs::create_dir_all(real.parent().unwrap()).unwrap();
std::fs::write(
&real,
"---\ntype: profile\nmeta-type: conclusion\ncreated: 2026-05-22T10:00:00-07:00\nupdated: 2026-05-22T10:00:00-07:00\nsummary: \"bio\"\n---\n\nSee [[records/contacts/does-not-exist]].\n",
)
.unwrap();
std::fs::create_dir_all(root.join("records/profiles")).unwrap();
symlink(&real, root.join("records/profiles/bio.md")).unwrap();
let store = open(root);
let ws = validate_working_set(&store, None).unwrap();
let all = validate_all(&store).unwrap();
assert!(
ws.iter().any(|i| i.code == codes::WIKI_LINK_BROKEN),
"the loop default must flag the symlinked-in file's broken link: {ws:#?}"
);
assert!(
all.iter().any(|i| i.code == codes::WIKI_LINK_BROKEN),
"`validate --all` must also follow the symlink and flag it (superset contract): {all:#?}"
);
}
#[test]
fn regression_working_set_stale_index_entry_is_not_wiki_link_broken() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
fresh_store(root);
write(root, "records/contacts/a.md", &contact("a"));
write(root, "records/contacts/c.md", &contact("c"));
let store = open(root);
Index::rebuild_all(&store).unwrap();
std::fs::remove_file(root.join("records/contacts/c.md")).unwrap();
write(
root,
"log.md",
"---\ntype: log\n---\n\n## [2026-06-01 08:00] delete | records/contacts/c\n",
);
let store = open(root);
let ws = validate_working_set(&store, None).unwrap();
assert!(
!ws.iter().any(|i| i.code == codes::WIKI_LINK_BROKEN
&& i.file.file_name().and_then(|n| n.to_str()) == Some("index.md")),
"a stale index.md entry must NOT be WIKI_LINK_BROKEN in the working set: {ws:#?}"
);
let all = validate_all(&store).unwrap();
assert!(
has(&all, codes::INDEX_STALE_ENTRY),
"`validate --all` must report the stale index entry as INDEX_STALE_ENTRY: {all:#?}"
);
}
#[test]
fn loose_only_layer_validates_clean_after_rebuild() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fresh_store(root);
write(
root,
"records/loose-note.md",
"---\ntype: note\ncreated: 2026-06-01T08:00:00-07:00\nupdated: 2026-06-01T08:00:00-07:00\nsummary: \"A loose note\"\n---\n\n# A\n",
);
let store = open(root);
Index::rebuild_all(&store).unwrap();
let all = validate_all(&store).unwrap();
assert!(
!has(&all, codes::INDEX_MISSING),
"a freshly-rebuilt loose-only store must not report INDEX_MISSING: {all:#?}"
);
assert!(
!all.iter()
.any(|i| i.severity == dbmd_core::validate::Severity::Error),
"a freshly-rebuilt loose-only store must validate clean (no errors): {all:#?}"
);
}
#[test]
fn loose_only_layer_missing_jsonl_is_still_flagged() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fresh_store(root);
write(
root,
"records/loose-note.md",
"---\ntype: note\ncreated: 2026-06-01T08:00:00-07:00\nupdated: 2026-06-01T08:00:00-07:00\nsummary: \"A loose note\"\n---\n\n# A\n",
);
let store = open(root);
Index::rebuild_all(&store).unwrap();
std::fs::remove_file(root.join("records/index.jsonl")).unwrap();
let all = validate_all(&store).unwrap();
assert!(
has(&all, codes::INDEX_JSONL_MISSING),
"a loose file with no layer index.jsonl must report INDEX_JSONL_MISSING: {all:#?}"
);
}