mod common;
use std::process::Command as StdCommand;
use std::sync::{Arc, Barrier};
use common::{copy_store_to_temp, corpus_a, dbmd, write_db_md, write_file};
fn long_summary() -> String {
format!(
"Director of Operations at Northstar; renewal champion who drove the 175-seat expansion and {}END_QUALIFIER",
"x".repeat(150)
)
}
#[test]
fn regression_write_preserves_long_explicit_summary() {
let tmp = tempfile::TempDir::new().unwrap();
let store = tmp.path();
write_db_md(store);
let summary = long_summary();
assert!(summary.chars().count() > 200, "fixture must exceed the cap");
dbmd()
.current_dir(store)
.args([
"write",
"records/contacts/sarah.md",
"--type",
"contact",
"--summary",
&summary,
])
.assert()
.success();
let written = std::fs::read_to_string(store.join("records/contacts/sarah.md")).unwrap();
assert!(
written.contains("END_QUALIFIER"),
"explicit --summary must not be truncated; the tail is missing:\n{written}"
);
assert!(
written.contains(&summary),
"the full explicit summary must round-trip verbatim:\n{written}"
);
}
#[test]
fn regression_fm_init_preserves_long_explicit_summary() {
let (_tmp, store) = copy_store_to_temp(&corpus_a());
write_file(
&store,
"records/contacts/nina-ray.md",
"---\nname: Nina Ray\nrole: Analyst\n---\n\n# Nina Ray\n",
);
let summary = long_summary();
dbmd()
.current_dir(&store)
.args([
"fm",
"init",
"records/contacts/nina-ray.md",
"--summary",
&summary,
])
.assert()
.success();
let written = std::fs::read_to_string(store.join("records/contacts/nina-ray.md")).unwrap();
assert!(
written.contains("END_QUALIFIER") && written.contains(&summary),
"fm init --summary must not truncate the agent's value:\n{written}"
);
}
#[test]
fn regression_concurrent_write_never_silently_clobbers() {
let bin = env!("CARGO_BIN_EXE_dbmd");
for round in 0..40 {
let tmp = tempfile::TempDir::new().unwrap();
let store = tmp.path().to_path_buf();
write_db_md(&store);
let rel = "records/contacts/sarah.md";
let barrier = Arc::new(Barrier::new(2));
let summaries = ["SUMMARY_A", "SUMMARY_B"];
let handles: Vec<_> = summaries
.iter()
.map(|s| {
let bin = bin.to_string();
let store = store.clone();
let barrier = Arc::clone(&barrier);
let summary = s.to_string();
std::thread::spawn(move || {
barrier.wait();
let out = StdCommand::new(&bin)
.current_dir(&store)
.args([
"write",
"records/contacts/sarah.md",
"--type",
"contact",
"--summary",
&summary,
])
.output()
.expect("spawn dbmd");
(summary, out.status.code())
})
})
.collect();
let results: Vec<(String, Option<i32>)> =
handles.into_iter().map(|h| h.join().unwrap()).collect();
let successes: Vec<&String> = results
.iter()
.filter(|(_, code)| *code == Some(0))
.map(|(s, _)| s)
.collect();
let collisions = results.iter().filter(|(_, code)| *code == Some(5)).count();
assert_eq!(
successes.len(),
1,
"round {round}: exactly one concurrent write may succeed, got {results:?}",
);
assert_eq!(
collisions, 1,
"round {round}: the losing write must report PATH_COLLISION (exit 5), got {results:?}",
);
let written = std::fs::read_to_string(store.join(rel)).unwrap();
let winner = successes[0];
assert!(
written.contains(&format!("summary: {winner}")),
"round {round}: surviving file must hold the winner's summary `{winner}`:\n{written}",
);
}
}
#[test]
fn regression_write_atomic_claim_still_refuses_existing_file_without_overwrite() {
let (_tmp, store) = copy_store_to_temp(&corpus_a());
let original =
"---\ntype: contact\nsummary: ORIGINAL\nname: Sarah\n---\n\n# Sarah\n\nOriginal body.\n";
write_file(&store, "records/contacts/collide.md", original);
dbmd()
.current_dir(&store)
.args([
"write",
"records/contacts/collide.md",
"--type",
"contact",
"--summary",
"A DIFFERENT SUMMARY",
])
.assert()
.code(5);
let after = std::fs::read_to_string(store.join("records/contacts/collide.md")).unwrap();
assert_eq!(
after, original,
"a refused write must not modify the existing file",
);
}
#[test]
fn regression_fm_init_refuses_unterminated_frontmatter_fence() {
let (_tmp, store) = copy_store_to_temp(&corpus_a());
let malformed = "---\nname: Tom\nrole: VP\n# Tom\nbody\n";
let path = write_file(&store, "records/contacts/tom.md", malformed);
dbmd()
.current_dir(&store)
.args(["fm", "init", "records/contacts/tom.md"])
.assert()
.failure()
.code(1);
let after = std::fs::read_to_string(&path).unwrap();
assert_eq!(
after, malformed,
"a refused fm init must not rewrite the malformed file",
);
}
#[test]
fn regression_fm_init_refuses_unterminated_fence_with_structured_code() {
let (_tmp, store) = copy_store_to_temp(&corpus_a());
write_file(
&store,
"records/contacts/tom-json.md",
"---\nname: Tom\nrole: VP\nbody\n",
);
let out = StdCommand::new(env!("CARGO_BIN_EXE_dbmd"))
.current_dir(&store)
.args(["--json", "fm", "init", "records/contacts/tom-json.md"])
.output()
.expect("spawn dbmd");
assert_eq!(out.status.code(), Some(1));
let err: serde_json::Value =
serde_json::from_str(String::from_utf8_lossy(&out.stderr).trim()).expect("json error");
assert_eq!(
err["error"]["code"], "FM_MALFORMED",
"unterminated fence must surface the FM_MALFORMED code, got {err}",
);
}
#[test]
fn regression_fm_init_still_imports_headerless_file() {
let (_tmp, store) = copy_store_to_temp(&corpus_a());
let raw = "# Tom Vega\n\nNew finance contact.\n";
write_file(&store, "records/contacts/tom-raw.md", raw);
dbmd()
.current_dir(&store)
.args(["fm", "init", "records/contacts/tom-raw.md"])
.assert()
.success();
let written = std::fs::read_to_string(store.join("records/contacts/tom-raw.md")).unwrap();
assert!(
written.starts_with("---\n"),
"frontmatter added:\n{written}"
);
assert!(
written.contains("type: contact"),
"type inferred:\n{written}"
);
assert!(
written.ends_with(raw),
"the raw headerless body must be preserved verbatim:\n{written}"
);
}