mod common;
use std::path::{Path, PathBuf};
use common::{dbmd, write_file};
fn scratch_store() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path().to_path_buf();
write_file(
&store,
"DB.md",
"---\ntype: db-md\nscope: test\nowner: T\n---\n# T\n",
);
for (path, summary) in [
("records/widgets/a.md", "Widget A"),
("records/widgets/b.md", "Widget B"),
] {
dbmd()
.args([
"write",
path,
"--type",
"widget",
"--summary",
summary,
"--dir",
])
.arg(&store)
.assert()
.success();
}
(dir, store)
}
#[test]
fn rm_unlinked_removes_file_and_index_row() {
let (_tmp, store) = scratch_store();
let assert = dbmd()
.args(["--json", "rm", "records/widgets/a.md", "--dir"])
.arg(&store)
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let out: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(
out,
serde_json::json!({
"removed": "records/widgets/a.md",
"backlinks": [],
"forced": false,
})
);
assert!(!store.join("records/widgets/a.md").exists());
let jsonl = std::fs::read_to_string(store.join("records/widgets/index.jsonl")).unwrap();
assert!(
!jsonl.contains("a.md"),
"catalog row must be dropped: {jsonl}"
);
assert!(jsonl.contains("b.md"), "sibling row must survive: {jsonl}");
dbmd()
.args(["validate", "--all"])
.arg(&store)
.assert()
.success();
}
#[test]
fn rm_last_record_removes_index_artifacts() {
let (_tmp, store) = scratch_store();
for path in ["records/widgets/a.md", "records/widgets/b.md"] {
dbmd()
.args(["rm", path, "--dir"])
.arg(&store)
.assert()
.success();
}
assert!(!store.join("records/widgets/index.md").exists());
assert!(!store.join("records/widgets/index.jsonl").exists());
dbmd()
.args(["validate", "--all"])
.arg(&store)
.assert()
.success();
}
#[test]
fn rm_linked_refuses_and_lists_backlinks() {
let (_tmp, store) = scratch_store();
dbmd()
.args([
"link",
"records/widgets/b.md",
"records/widgets/a.md",
"--dir",
])
.arg(&store)
.assert()
.success();
let assert = dbmd()
.args(["--json", "rm", "records/widgets/a.md", "--dir"])
.arg(&store)
.assert()
.failure()
.code(5); let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
let err: serde_json::Value = serde_json::from_str(&stderr).unwrap();
assert_eq!(err["error"]["code"], serde_json::json!("RM_LINKED"));
assert_eq!(
err["error"]["details"]["backlinks"],
serde_json::json!(["records/widgets/b.md"])
);
assert!(
store.join("records/widgets/a.md").exists(),
"refusal must not delete"
);
}
#[test]
fn rm_force_deletes_and_validate_flags_broken_links() {
let (_tmp, store) = scratch_store();
dbmd()
.args([
"link",
"records/widgets/b.md",
"records/widgets/a.md",
"--dir",
])
.arg(&store)
.assert()
.success();
let assert = dbmd()
.args(["--json", "rm", "records/widgets/a.md", "--force", "--dir"])
.arg(&store)
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let out: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(
out,
serde_json::json!({
"removed": "records/widgets/a.md",
"backlinks": ["records/widgets/b.md"],
"forced": true,
})
);
let assert = dbmd()
.args(["--json", "validate", "--all"])
.arg(&store)
.assert()
.failure()
.code(6); let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("WIKI_LINK_BROKEN"),
"forced delete must surface as WIKI_LINK_BROKEN: {stdout}"
);
}
#[test]
fn rm_frozen_page_refused() {
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path();
write_file(
store,
"DB.md",
"---\ntype: db-md\nscope: test\nowner: T\n---\n\n# T\n\n## Policies\n\n### Frozen pages\n- `records/widgets/a.md` — signed off.\n",
);
dbmd()
.args([
"write",
"records/widgets/a.md",
"--type",
"widget",
"--summary",
"A",
"--dir",
])
.arg(store)
.assert()
.failure(); write_file(
store,
"records/widgets/a.md",
"---\ntype: widget\nsummary: A\ncreated: 2026-01-01T00:00:00Z\nupdated: 2026-01-01T00:00:00Z\n---\n",
);
let assert = dbmd()
.args(["--json", "rm", "records/widgets/a.md", "--dir"])
.arg(store)
.assert()
.failure()
.code(4); let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
let err: serde_json::Value = serde_json::from_str(&stderr).unwrap();
assert_eq!(
err["error"]["code"],
serde_json::json!("POLICY_FROZEN_PAGE")
);
assert!(store.join("records/widgets/a.md").exists());
}
#[test]
fn rm_reserved_meta_refused() {
let (_tmp, store) = scratch_store();
for path in [
"DB.md",
"log.md",
"records/widgets/index.md",
"records/widgets/index.jsonl",
] {
let assert = dbmd()
.args(["--json", "rm", path, "--dir"])
.arg(&store)
.assert()
.failure()
.code(4); let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
let err: serde_json::Value = serde_json::from_str(&stderr).unwrap();
assert_eq!(
err["error"]["code"],
serde_json::json!("RM_RESERVED_META"),
"{path}"
);
}
assert!(store.join("DB.md").exists());
assert!(store.join("records/widgets/index.md").exists());
}
#[test]
fn rm_shape_guards() {
let (_tmp, store) = scratch_store();
let assert = dbmd()
.args(["--json", "rm", "records/widgets", "--dir"])
.arg(&store)
.assert()
.failure()
.code(4);
let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
let err: serde_json::Value = serde_json::from_str(&stderr).unwrap();
assert_eq!(err["error"]["code"], serde_json::json!("RM_NOT_A_FILE"));
write_file(&store, "scratch/x.md", "loose bytes\n");
let assert = dbmd()
.args(["--json", "rm", "scratch/x.md", "--dir"])
.arg(&store)
.assert()
.failure()
.code(4);
let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
let err: serde_json::Value = serde_json::from_str(&stderr).unwrap();
assert_eq!(err["error"]["code"], serde_json::json!("RM_NOT_CONTENT"));
assert!(store.join("scratch/x.md").exists());
dbmd()
.args(["rm", "records/widgets/missing.md", "--dir"])
.arg(&store)
.assert()
.failure()
.code(1); }
#[test]
fn rm_works_from_inside_the_store() {
let (_tmp, store) = scratch_store();
dbmd()
.current_dir(&store)
.args(["rm", "records/widgets/a.md"])
.assert()
.success();
assert!(!Path::new(&store).join("records/widgets/a.md").exists());
}