mod common;
use common::{dbmd, write_db_md, write_file};
use dbmd_core::projection::projection_path_sha256;
use serde_json::Value;
const WRAPPER: &str = "\
---
type: pdf-source
created: 2026-06-17T09:00:00-05:00
updated: 2026-06-17T09:00:00-05:00
summary: \"Contract PDF wrapper\"
asset: sources/docs/2026/06/contract.pdf
---
# Contract
";
fn setup(dir: &std::path::Path) {
write_db_md(dir);
write_file(dir, "sources/docs/2026/06/contract.pdf.md", WRAPPER);
write_file(
dir,
"sources/docs/2026/06/contract.pdf",
"FAKE PDF BYTES 0123456789 abcdefghij",
);
}
fn json_stdout(out: &std::process::Output) -> Value {
serde_json::from_slice(&out.stdout).expect("stdout is valid JSON")
}
#[test]
fn scan_catalogs_then_verify_passes() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
let assert = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
assert_eq!(v["cataloged"], 1);
assert_eq!(v["hashed"], 1);
assert_eq!(v["preserved"], 0);
assert_eq!(v["wrote"], true);
let manifest = std::fs::read_to_string(tmp.path().join("assets.jsonl")).unwrap();
let rec: Value = serde_json::from_str(manifest.lines().next().unwrap()).unwrap();
assert_eq!(rec["path"], "sources/docs/2026/06/contract.pdf");
assert_eq!(rec["sha256"].as_str().unwrap().len(), 64);
assert_eq!(rec["media_type"], "application/pdf");
assert_eq!(rec["required"], true);
assert_eq!(rec["wrappers"][0], "sources/docs/2026/06/contract.pdf.md");
let assert = dbmd()
.args(["--json", "assets", "verify", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
assert_eq!(v["complete"], true);
assert_eq!(v["checked"], 1);
}
#[test]
fn scan_is_idempotent_no_change_on_second_run() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let assert = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
assert_eq!(
v["wrote"], false,
"a no-op rescan must not rewrite the manifest"
);
}
#[test]
fn refresh_updates_one_declared_asset_without_walking_unrelated_bytes() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let manifest_path = tmp.path().join("assets.jsonl");
let first = std::fs::read_to_string(&manifest_path).unwrap();
let unrelated = serde_json::json!({
"path": "sources/docs/2026/06/missing.bin",
"sha256": "a".repeat(64),
"bytes": 99,
"media_type": "application/octet-stream",
"wrappers": ["sources/docs/2026/06/missing.md"],
"required": true,
});
std::fs::write(
&manifest_path,
format!("{first}{}\n", serde_json::to_string(&unrelated).unwrap()),
)
.unwrap();
write_file(
tmp.path(),
"sources/docs/2026/06/contract.pdf",
"SANITIZED DERIVATIVE BYTES",
);
let assert = dbmd()
.args([
"--json",
"assets",
"refresh",
"sources/docs/2026/06/contract.pdf",
"--wrapper",
"sources/docs/2026/06/contract.pdf.md",
"--dir",
])
.arg(tmp.path())
.assert()
.success();
let report = json_stdout(assert.get_output());
assert_eq!(report["path"], "sources/docs/2026/06/contract.pdf");
assert_eq!(report["bytes"], 26);
assert_eq!(report["wrote"], true);
let rows = std::fs::read_to_string(&manifest_path).unwrap();
let parsed: Vec<Value> = rows
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0]["path"], "sources/docs/2026/06/contract.pdf");
assert_eq!(parsed[0]["bytes"], 26);
assert_eq!(
parsed[1], unrelated,
"unrelated missing bytes stay untouched"
);
let second = dbmd()
.args([
"--json",
"assets",
"refresh",
"sources/docs/2026/06/contract.pdf",
"--wrapper",
"sources/docs/2026/06/contract.pdf.md",
"--dir",
])
.arg(tmp.path())
.assert()
.success();
assert_eq!(json_stdout(second.get_output())["wrote"], false);
}
#[test]
fn append_only_supersession_keeps_original_optional_and_replacement_required() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let replacement = "sources/redacted/contract.txt";
let replacement_wrapper = "sources/redacted/contract.md";
write_file(tmp.path(), replacement, "portable $API_TOKEN reference\n");
write_file(
tmp.path(),
replacement_wrapper,
&format!(
"---\ntype: note\ncreated: 2026-08-25T00:00:00Z\nupdated: 2026-08-25T00:00:00Z\nsummary: Portable sanitized derivative\nasset: {replacement}\nsupersedes-asset: sources/docs/2026/06/contract.pdf\n---\n\n# Replacement\n"
),
);
let refreshed = dbmd()
.args([
"--json",
"assets",
"refresh",
replacement,
"--wrapper",
replacement_wrapper,
"--dir",
])
.arg(tmp.path())
.assert()
.success();
assert_eq!(
json_stdout(refreshed.get_output())["superseded_assets"],
serde_json::json!(["sources/docs/2026/06/contract.pdf"])
);
let manifest = tmp.path().join("assets.jsonl");
let rows: Vec<Value> = std::fs::read_to_string(&manifest)
.unwrap()
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["path"], "sources/docs/2026/06/contract.pdf");
assert_eq!(rows[0]["required"], false);
assert!(rows[0]["wrappers"]
.as_array()
.unwrap()
.iter()
.any(|wrapper| wrapper == replacement_wrapper));
assert_eq!(rows[1]["path"], replacement);
assert_eq!(rows[1]["required"], true);
let before_scan = std::fs::read(&manifest).unwrap();
let rescanned = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
assert_eq!(json_stdout(rescanned.get_output())["wrote"], false);
assert_eq!(std::fs::read(&manifest).unwrap(), before_scan);
std::fs::remove_file(tmp.path().join("sources/docs/2026/06/contract.pdf")).unwrap();
let absent_original_scan = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
assert_eq!(
json_stdout(absent_original_scan.get_output())["wrote"],
false,
"a fresh clone preserves the exact optional original row without its local-only bytes"
);
dbmd()
.args(["index", "rebuild", "--dir"])
.arg(tmp.path())
.assert()
.success();
dbmd()
.args(["validate", "--all"])
.arg(tmp.path())
.assert()
.success();
dbmd()
.args(["assets", "verify", "--dir"])
.arg(tmp.path())
.assert()
.success();
}
#[test]
fn conflicting_supersessions_fail_validation_and_keep_original_required() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
for (name, replacement) in [
("one", "sources/redacted/one.txt"),
("two", "sources/redacted/two.txt"),
] {
write_file(tmp.path(), replacement, name);
write_file(
tmp.path(),
&format!("sources/redacted/{name}.md"),
&format!(
"---\ntype: note\ncreated: 2026-08-25T00:00:00Z\nupdated: 2026-08-25T00:00:00Z\nsummary: Conflicting replacement {name}\nasset: {replacement}\nsupersedes-asset: sources/docs/2026/06/contract.pdf\n---\n"
),
);
}
let scan = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let scan = json_stdout(scan.get_output());
assert!(scan["warnings"]
.as_array()
.unwrap()
.iter()
.any(|warning| warning.as_str().unwrap().contains("conflicts")));
let original: Value = std::fs::read_to_string(tmp.path().join("assets.jsonl"))
.unwrap()
.lines()
.map(|line| serde_json::from_str::<Value>(line).unwrap())
.find(|row| row["path"] == "sources/docs/2026/06/contract.pdf")
.unwrap();
assert_eq!(original["required"], true);
dbmd()
.args(["index", "rebuild", "--dir"])
.arg(tmp.path())
.assert()
.success();
let validation = dbmd()
.args(["validate", "--all"])
.arg(tmp.path())
.assert()
.failure();
assert!(String::from_utf8_lossy(&validation.get_output().stdout)
.contains("ASSET_SUPERSESSION_INVALID"));
}
#[test]
fn refresh_refuses_a_wrapper_that_does_not_declare_the_exact_asset() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
write_file(
tmp.path(),
"sources/docs/2026/06/other.md",
"---\ntype: note\ncreated: 2026-06-17T09:00:00-05:00\nupdated: 2026-06-17T09:00:00-05:00\nsummary: other\n---\nbody\n",
);
let refused = dbmd()
.args([
"assets",
"refresh",
"sources/docs/2026/06/contract.pdf",
"--wrapper",
"sources/docs/2026/06/other.md",
"--dir",
])
.arg(tmp.path())
.assert()
.failure();
assert!(
String::from_utf8_lossy(&refused.get_output().stderr).contains("does not declare asset")
);
assert!(!tmp.path().join("assets.jsonl").exists());
}
#[test]
fn refresh_drops_a_missing_historical_wrapper_but_keeps_the_live_one() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let manifest_path = tmp.path().join("assets.jsonl");
let mut row: Value =
serde_json::from_str(std::fs::read_to_string(&manifest_path).unwrap().trim()).unwrap();
row["wrappers"] = serde_json::json!([
"sources/docs/2026/06/contract.pdf.md",
"sources/docs/2026/06/missing-wrapper.md"
]);
std::fs::write(
&manifest_path,
format!("{}\n", serde_json::to_string(&row).unwrap()),
)
.unwrap();
let refreshed = dbmd()
.args([
"--json",
"assets",
"refresh",
"sources/docs/2026/06/contract.pdf",
"--wrapper",
"sources/docs/2026/06/contract.pdf.md",
"--dir",
])
.arg(tmp.path())
.assert()
.success();
assert_eq!(
json_stdout(refreshed.get_output())["wrappers"],
serde_json::json!(["sources/docs/2026/06/contract.pdf.md"])
);
}
#[test]
fn refresh_refuses_an_existing_malformed_historical_wrapper() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let malformed = "sources/docs/2026/06/malformed.md";
write_file(tmp.path(), malformed, "---\nassets: [unterminated\n---\n");
let manifest_path = tmp.path().join("assets.jsonl");
let mut row: Value =
serde_json::from_str(std::fs::read_to_string(&manifest_path).unwrap().trim()).unwrap();
row["wrappers"] = serde_json::json!(["sources/docs/2026/06/contract.pdf.md", malformed]);
let before = format!("{}\n", serde_json::to_string(&row).unwrap());
std::fs::write(&manifest_path, &before).unwrap();
dbmd()
.args([
"assets",
"refresh",
"sources/docs/2026/06/contract.pdf",
"--wrapper",
"sources/docs/2026/06/contract.pdf.md",
"--dir",
])
.arg(tmp.path())
.assert()
.failure();
assert_eq!(
std::fs::read_to_string(&manifest_path).unwrap(),
before,
"a refused targeted refresh must leave the manifest byte-identical"
);
}
#[test]
fn scan_recompacts_duplicate_line_manifest() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let manifest = tmp.path().join("assets.jsonl");
let canonical = std::fs::read_to_string(&manifest).unwrap();
assert_eq!(canonical.lines().count(), 1);
std::fs::write(&manifest, format!("{canonical}{canonical}")).unwrap();
assert_eq!(
std::fs::read_to_string(&manifest).unwrap().lines().count(),
2
);
let assert = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
assert_eq!(
v["wrote"], true,
"a non-canonical (duplicate-line) manifest must be recompacted and reported as updated"
);
let after = std::fs::read_to_string(&manifest).unwrap();
assert_eq!(
after.lines().count(),
1,
"duplicate lines must collapse to the single canonical line"
);
assert_eq!(
after, canonical,
"scan must restore the exact canonical bytes"
);
let assert = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
assert_eq!(
v["wrote"], false,
"a recompacted, canonical manifest must rescan as no-change"
);
assert_eq!(
std::fs::read_to_string(&manifest).unwrap(),
canonical,
"the no-op rescan must leave the manifest byte-identical"
);
}
#[test]
fn verify_fails_and_status_reports_missing_required() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
std::fs::remove_file(tmp.path().join("sources/docs/2026/06/contract.pdf")).unwrap();
dbmd()
.args(["assets", "verify", "--dir"])
.arg(tmp.path())
.assert()
.failure();
let assert = dbmd()
.args(["--json", "assets", "status", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
assert_eq!(v["missing"], 1);
assert_eq!(v["required_missing"], 1);
}
#[test]
fn verify_projection_reports_exact_absence_but_never_hides_loss_or_corruption() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let asset = tmp.path().join("sources/docs/2026/06/contract.pdf");
std::fs::remove_file(&asset).unwrap();
write_file(
tmp.path(),
".projection",
"# private docs\nsources/docs/**\n",
);
let accepted = dbmd()
.args([
"--json",
"assets",
"verify",
"--projection-excludes",
".projection",
"--dir",
])
.arg(tmp.path())
.assert()
.success();
let report = json_stdout(accepted.get_output());
assert_eq!(report["complete"], false);
assert_eq!(report["projection_complete"], true);
assert_eq!(report["checked"], 0);
assert_eq!(
report["projected_missing"],
serde_json::json!(["sources/docs/2026/06/contract.pdf"])
);
write_file(
tmp.path(),
".projection",
"sources/docs/2026/06/other.pdf\n",
);
dbmd()
.args([
"assets",
"verify",
"--projection-excludes",
".projection",
"--dir",
])
.arg(tmp.path())
.assert()
.failure();
write_file(tmp.path(), "sources/docs/2026/06/contract.pdf", "CORRUPT");
write_file(tmp.path(), ".projection", "sources/docs/**\n");
dbmd()
.args([
"assets",
"verify",
"--projection-excludes",
".projection",
"--dir",
])
.arg(tmp.path())
.assert()
.failure();
}
#[test]
fn verify_projection_manifest_accepts_bounded_stdin_without_claiming_full_completeness() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
std::fs::remove_file(tmp.path().join("sources/docs/2026/06/contract.pdf")).unwrap();
let hash = projection_path_sha256("sources/docs/2026/06/contract.pdf");
let manifest =
format!("{{\"version\":1,\"algorithm\":\"sha256\",\"path_hashes\":[\"{hash}\"]}}");
let accepted = dbmd()
.args([
"--json",
"assets",
"verify",
"--projection-manifest",
"-",
"--dir",
])
.arg(tmp.path())
.write_stdin(manifest)
.assert()
.success();
let report = json_stdout(accepted.get_output());
assert_eq!(report["complete"], false);
assert_eq!(report["projection_complete"], true);
assert_eq!(
report["projected_missing"],
serde_json::json!(["sources/docs/2026/06/contract.pdf"])
);
}
#[test]
fn rescan_preserves_evicted_asset() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
std::fs::remove_file(tmp.path().join("sources/docs/2026/06/contract.pdf")).unwrap();
let assert = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
assert_eq!(v["cataloged"], 1);
assert_eq!(v["preserved"], 1);
assert_eq!(v["hashed"], 0);
assert!(std::fs::read_to_string(tmp.path().join("assets.jsonl"))
.unwrap()
.contains("contract.pdf"));
}
#[test]
fn traversal_asset_path_is_rejected_and_not_cataloged() {
let tmp = tempfile::TempDir::new().unwrap();
write_db_md(tmp.path());
write_file(
tmp.path(),
"sources/docs/2026/06/evil.md",
"---\ntype: pdf-source\ncreated: 2026-06-17T09:00:00-05:00\nupdated: \
2026-06-17T09:00:00-05:00\nsummary: \"evil\"\nasset: \
../../../../../../etc/passwd\n---\n\n# Evil\n",
);
let assert = dbmd()
.args(["--json", "assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
assert_eq!(v["cataloged"], 0, "a `..` path must never be cataloged");
assert!(
v["warnings"]
.as_array()
.unwrap()
.iter()
.any(|w| w.as_str().unwrap().contains("..")),
"the rejection is reported as a warning: {v}"
);
assert!(!tmp.path().join("assets.jsonl").exists());
}
#[test]
fn paths_omits_store_escaping_records() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
let manifest = tmp.path().join("assets.jsonl");
let mut text = std::fs::read_to_string(&manifest).unwrap();
text.push_str(
"{\"path\":\"../../../../../../etc/passwd\",\"sha256\":\"deadbeef\",\"bytes\":4096,\
\"media_type\":\"text/plain\",\"wrappers\":[\"sources/docs/2026/06/contract.pdf.md\"],\
\"required\":false}\n",
);
text.push_str(
"{\"path\":\"/etc/hosts\",\"sha256\":\"deadbeef\",\"bytes\":4096,\
\"media_type\":\"text/plain\",\"wrappers\":[\"sources/docs/2026/06/contract.pdf.md\"],\
\"required\":false}\n",
);
std::fs::write(&manifest, text).unwrap();
let assert = dbmd()
.args(["assets", "paths", "--dir"])
.arg(tmp.path())
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("sources/docs/2026/06/contract.pdf"),
"the legitimate in-store path is still emitted: {stdout:?}"
);
assert!(
!stdout.contains("etc/passwd") && !stdout.contains("etc/hosts"),
"no store-escaping path may leak from `assets paths`: {stdout:?}"
);
let assert = dbmd()
.args(["--json", "assets", "paths", "--dir"])
.arg(tmp.path())
.assert()
.success();
let v = json_stdout(assert.get_output());
let list = v.as_array().expect("paths --json is an array");
assert_eq!(
list,
&vec![Value::from("sources/docs/2026/06/contract.pdf")],
"JSON `paths` emits only the safe in-store path"
);
}
#[test]
fn validate_all_passes_on_a_byteless_fresh_clone() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
dbmd()
.args(["index", "rebuild", "--dir"])
.arg(tmp.path())
.assert()
.success();
std::fs::remove_file(tmp.path().join("sources/docs/2026/06/contract.pdf")).unwrap();
dbmd()
.arg("validate")
.arg(tmp.path())
.arg("--all")
.assert()
.success();
}
#[test]
fn undeclared_asset_is_flagged_by_validate_until_scanned() {
let tmp = tempfile::TempDir::new().unwrap();
setup(tmp.path());
dbmd()
.args(["index", "rebuild", "--dir"])
.arg(tmp.path())
.assert()
.success();
let assert = dbmd()
.args(["--json", "validate"])
.arg(tmp.path())
.arg("--all")
.assert()
.failure();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("ASSET_UNDECLARED"),
"validate --all flags the uncataloged declaration: {stdout}"
);
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
dbmd()
.arg("validate")
.arg(tmp.path())
.arg("--all")
.assert()
.success();
}
#[test]
fn optional_asset_excluded_from_default_verify() {
let tmp = tempfile::TempDir::new().unwrap();
write_db_md(tmp.path());
write_file(
tmp.path(),
"records/expenses/e1.md",
"---\ntype: expense\ncreated: 2026-06-17T09:00:00-05:00\nupdated: \
2026-06-17T09:00:00-05:00\nsummary: \"expense + optional receipt\"\nassets:\n - \
{ path: records/expenses/r1.png, required: false }\n---\n\n# Expense\n",
);
write_file(tmp.path(), "records/expenses/r1.png", "PNG BYTES");
dbmd()
.args(["assets", "scan", "--dir"])
.arg(tmp.path())
.assert()
.success();
std::fs::remove_file(tmp.path().join("records/expenses/r1.png")).unwrap();
dbmd()
.args(["assets", "verify", "--dir"])
.arg(tmp.path())
.assert()
.success();
dbmd()
.args(["assets", "verify", "--include-optional", "--dir"])
.arg(tmp.path())
.assert()
.failure();
}