mod common;
use std::path::{Path, PathBuf};
use common::{dbmd, write_db_md, write_file};
const NOTE_BODY: &str = "\nCarlos confirmed with [[records/contacts/sarah-chen]] and\n[[records/contacts/sarah-chen.md|Sarah]]; see [[records/decisions/ghost-call]].\n";
const NOTE_FM: &str = "type: note\ncreated: 2026-05-27T08:00:00Z\nupdated: 2026-06-01T09:30:00Z\nsummary: Carlos said the pivot is on\ntold_by: Carlos\n";
const CONTACT: &str = "---\ntype: contact\nname: Sarah Chen\ncreated: 2026-05-01T12:00:00Z\nupdated: 2026-06-02T10:00:00Z\nsummary: Design lead at Acme\ncompany: Acme\n---\n\nMet at the pivot call.\n";
const DECISION: &str = "---\ntype: decision\nmeta-type: conclusion\ncreated: 2026-06-03T08:00:00Z\nupdated: 2026-06-03T08:00:00Z\n---\n\n# Ship the pivot\n\nDecided with [[records/contacts/sarah-chen]].\n\n```\n[[records/contacts/fenced-not-a-link]]\n```\n";
const PLAIN: &str = "Just a plain note that predates the store discipline.\n";
fn seed_store(root: &Path) {
let note = format!("---\n{NOTE_FM}---\n{NOTE_BODY}");
write_file(root, "sources/notes/pivot-call.md", ¬e);
write_file(root, "sources/notes/plain.md", PLAIN);
write_file(root, "records/contacts/sarah-chen.md", CONTACT);
write_file(root, "records/decisions/pivot.md", DECISION);
write_file(root, "records/contacts/index.md", "# Contacts\n");
write_file(root, "records/contacts/index.jsonl", "");
}
fn fixture() -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::TempDir::new().expect("tempdir");
let root = tmp.path().to_path_buf();
write_db_md(&root);
seed_store(&root);
(tmp, root)
}
fn emit_json(root: &Path) -> serde_json::Value {
let out = dbmd().args(["--json", "emit"]).arg(root).assert().success();
let stdout = String::from_utf8(out.get_output().stdout.clone()).expect("utf8 stdout");
serde_json::from_str(stdout.trim()).expect("stdout is one JSON document")
}
fn file<'a>(dump: &'a serde_json::Value, path: &str) -> &'a serde_json::Value {
dump["files"]
.as_array()
.expect("files is an array")
.iter()
.find(|f| f["path"] == path)
.unwrap_or_else(|| panic!("no emitted file {path} in {dump}"))
}
fn sha256_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
use std::fmt::Write as _;
let digest = Sha256::digest(bytes);
let mut hex = String::with_capacity(64);
for b in digest.iter() {
let _ = write!(hex, "{b:02x}");
}
hex
}
#[test]
fn dump_envelope_has_store_files_and_summary_counts() {
let (_tmp, root) = fixture();
let dump = emit_json(&root);
assert_eq!(dump["store"], root.to_string_lossy().as_ref());
assert!(dump["files"].is_array());
let paths: Vec<&str> = dump["files"]
.as_array()
.unwrap()
.iter()
.map(|f| f["path"].as_str().expect("path is a string"))
.collect();
assert_eq!(
paths,
vec![
"DB.md",
"records/contacts/sarah-chen.md",
"records/decisions/pivot.md",
"sources/notes/pivot-call.md",
"sources/notes/plain.md",
]
);
assert_eq!(dump["summary"]["files"], 5);
assert_eq!(dump["summary"]["sources"], 2);
assert_eq!(dump["summary"]["records"], 2);
}
#[test]
fn layers_classify_source_record_and_null_for_db_md() {
let (_tmp, root) = fixture();
let dump = emit_json(&root);
assert_eq!(
file(&dump, "sources/notes/pivot-call.md")["layer"],
"source"
);
assert_eq!(
file(&dump, "records/contacts/sarah-chen.md")["layer"],
"record"
);
let db = file(&dump, "DB.md");
assert!(db["layer"].is_null(), "DB.md carries no layer: {db}");
assert_eq!(db["type"], "db-md");
assert_eq!(db["frontmatter"]["scope"], "company");
assert_eq!(db["title"], "Test store");
assert!(db["meta_type"].is_null());
}
#[test]
fn frontmatter_is_verbatim_and_derived_fields_are_typed() {
let (_tmp, root) = fixture();
let dump = emit_json(&root);
let note = file(&dump, "sources/notes/pivot-call.md");
assert_eq!(note["frontmatter"]["type"], "note");
assert_eq!(note["frontmatter"]["told_by"], "Carlos");
assert_eq!(note["frontmatter"]["created"], "2026-05-27T08:00:00Z");
assert_eq!(note["type"], "note");
assert_eq!(note["summary"], "Carlos said the pivot is on");
assert_eq!(note["created"], "2026-05-27T08:00:00+00:00");
assert_eq!(note["updated"], "2026-06-01T09:30:00+00:00");
assert!(note["meta_type"].is_null());
let contact = file(&dump, "records/contacts/sarah-chen.md");
assert_eq!(contact["frontmatter"]["company"], "Acme");
assert_eq!(
contact["title"], "Sarah Chen",
"title from the `name` field"
);
}
#[test]
fn meta_type_defaults_to_fact_for_records_and_keeps_declared_values() {
let (_tmp, root) = fixture();
let dump = emit_json(&root);
assert_eq!(
file(&dump, "records/contacts/sarah-chen.md")["meta_type"],
"fact"
);
assert_eq!(
file(&dump, "records/decisions/pivot.md")["meta_type"],
"conclusion"
);
}
#[test]
fn missing_summary_is_null_and_title_falls_back_to_first_h1() {
let (_tmp, root) = fixture();
let dump = emit_json(&root);
let decision = file(&dump, "records/decisions/pivot.md");
assert!(
decision["summary"].is_null(),
"no summary field ⇒ null, never an invented one: {decision}"
);
assert_eq!(decision["title"], "Ship the pivot");
}
#[test]
fn body_is_the_verbatim_text_after_the_frontmatter_block() {
let (_tmp, root) = fixture();
let dump = emit_json(&root);
assert_eq!(
file(&dump, "sources/notes/pivot-call.md")["body"],
NOTE_BODY
);
let plain = file(&dump, "sources/notes/plain.md");
assert_eq!(plain["body"], PLAIN);
assert_eq!(
plain["frontmatter"],
serde_json::json!({}),
"no frontmatter block ⇒ empty object"
);
assert!(plain["type"].is_null());
assert_eq!(plain["layer"], "source");
}
#[test]
fn links_are_normalized_md_paths_deduped_with_dangling_kept() {
let (_tmp, root) = fixture();
let dump = emit_json(&root);
assert_eq!(
file(&dump, "sources/notes/pivot-call.md")["links"],
serde_json::json!([
"records/contacts/sarah-chen.md",
"records/decisions/ghost-call.md",
])
);
assert_eq!(
file(&dump, "records/decisions/pivot.md")["links"],
serde_json::json!(["records/contacts/sarah-chen.md"])
);
}
#[test]
fn sha256_is_the_hex_digest_of_the_raw_file_bytes() {
let (_tmp, root) = fixture();
let dump = emit_json(&root);
assert_eq!(
file(&dump, "records/contacts/sarah-chen.md")["sha256"],
sha256_hex(CONTACT.as_bytes())
);
assert_eq!(
file(&dump, "sources/notes/plain.md")["sha256"],
sha256_hex(PLAIN.as_bytes())
);
let note_bytes = std::fs::read(root.join("sources/notes/pivot-call.md")).unwrap();
assert_eq!(
file(&dump, "sources/notes/pivot-call.md")["sha256"],
sha256_hex(¬e_bytes)
);
}
#[test]
fn text_mode_prints_the_emitted_paths_one_per_line() {
let (_tmp, root) = fixture();
let out = dbmd().arg("emit").arg(&root).assert().success();
let stdout = String::from_utf8(out.get_output().stdout.clone()).unwrap();
assert_eq!(
stdout,
"DB.md\nrecords/contacts/sarah-chen.md\nrecords/decisions/pivot.md\nsources/notes/pivot-call.md\nsources/notes/plain.md\n"
);
}
#[test]
fn a_non_store_dir_fails_with_not_a_store_exit_3() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let out = dbmd()
.args(["--json", "emit"])
.arg(tmp.path())
.assert()
.failure()
.code(3);
let stderr = String::from_utf8(out.get_output().stderr.clone()).unwrap();
let err: serde_json::Value = serde_json::from_str(stderr.trim()).expect("structured error");
assert_eq!(err["error"]["code"], "NOT_A_STORE");
}