use std::path::Path;
use std::process::{Command, Output};
const VOCABULARY: &str = r#"version: 1
namespace: "urn:ngm:class:"
types:
Class: { owl: "owl:Class", required: [resource, status] }
relations:
is-a: { owl: "rdfs:subClassOf" }
"#;
const EXISTING: &str = "---\ntype: Class\ntitle: Alpha\npublic: true\nresource: urn:ngm:class:alpha\nstatus: stable\n---\nbody\n";
const STAGED: &str = "---\ntype: Class\ntitle: New Thing\npublic: true\nresource: urn:ngm:class:new-thing\nstatus: draft\nis-a:\n- '[[Alpha]]'\n---\nElevated from a working note.\n";
const PAGES: &str = "knowledge";
fn write(path: &Path, text: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, text).unwrap();
}
fn ontomark(repo: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_ontomark"))
.arg("--repo")
.arg(repo)
.arg("--json")
.args(args)
.output()
.expect("the ontomark binary runs")
}
fn json(output: &Output) -> serde_json::Value {
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(&stdout).unwrap_or_else(|e| {
panic!(
"stdout is not one JSON document ({e}):\n{stdout}\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
)
})
}
#[test]
fn create_a_page_exactly_once() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path().join("repo");
write(&repo.join("ontology/vocabulary.yaml"), VOCABULARY);
write(&repo.join(PAGES).join("Alpha.md"), EXISTING);
let staged = dir.path().join("staged.md");
write(&staged, STAGED);
let staged_arg = staged.to_str().unwrap();
let target = repo.join("knowledge/New Thing.md");
let create = [
"create",
staged_arg,
"--expect",
"docs=1",
"--set",
"status=stable",
"--set",
"verified+={by: 'human:npub1test', at: '2026-09-22T00:00:00Z'}",
];
let dry = ontomark(&repo, &[&create[..], &["--dry-run"]].concat());
assert!(dry.status.success(), "{dry:?}");
assert_eq!(json(&dry)["dry_run"], true);
assert!(!target.exists(), "a dry run writes nothing");
let first = ontomark(&repo, &create);
assert!(first.status.success(), "{first:?}");
let outcome = json(&first);
assert_eq!(outcome["created"], true);
assert_eq!(outcome["dry_run"], false);
assert_eq!(outcome["path"], "knowledge/New Thing.md");
assert_eq!(outcome["iri"], "urn:ngm:class:new-thing");
assert_eq!(outcome["page"], "New Thing");
assert_eq!(outcome["docs"], 1);
assert_eq!(outcome["blocks"], 2);
let written = std::fs::read_to_string(&target).unwrap();
assert!(written.contains("status: stable"), "{written}");
assert!(written.contains("human:npub1test"), "{written}");
assert!(written.ends_with("---\nElevated from a working note.\n"));
let second = ontomark(&repo, &create);
assert_eq!(second.status.code(), Some(2), "{second:?}");
let refusal = json(&second);
assert_eq!(refusal["created"], false);
assert_eq!(refusal["code"], "EXISTS");
assert!(refusal["message"].as_str().is_some_and(|m| !m.is_empty()));
assert_eq!(refusal["blockers"], serde_json::json!([]));
assert_eq!(std::fs::read_to_string(&target).unwrap(), written);
}
#[test]
fn create_refuses_an_invalid_page_or_an_undeclared_radius_with_exit_2() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path().join("repo");
write(&repo.join("ontology/vocabulary.yaml"), VOCABULARY);
write(&repo.join(PAGES).join("Alpha.md"), EXISTING);
let staged = dir.path().join("staged.md");
write(&staged, &STAGED.replace("status: draft\n", ""));
let staged_arg = staged.to_str().unwrap();
let invalid = ontomark(&repo, &["create", staged_arg, "--expect", "docs=1"]);
assert_eq!(invalid.status.code(), Some(2), "{invalid:?}");
let refusal = json(&invalid);
assert_eq!(refusal["code"], "BLOCKED");
let blockers = refusal["blockers"].as_array().unwrap();
assert!(!blockers.is_empty(), "{refusal}");
assert!(blockers.iter().all(|b| b["code"].is_string()), "{refusal}");
let undeclared = ontomark(&repo, &["create", staged_arg, "--set", "status=stable"]);
assert_eq!(undeclared.status.code(), Some(2), "{undeclared:?}");
assert_eq!(json(&undeclared)["code"], "UNDECLARED_BLAST_RADIUS");
let clash = dir.path().join("clash.md");
write(
&clash,
&STAGED.replace("urn:ngm:class:new-thing", "urn:ngm:class:alpha"),
);
let collided = ontomark(
&repo,
&["create", clash.to_str().unwrap(), "--expect", "docs=1"],
);
assert_eq!(collided.status.code(), Some(2), "{collided:?}");
let refusal = json(&collided);
assert_eq!(refusal["code"], "BLOCKED");
assert!(
refusal["blockers"]
.as_array()
.unwrap()
.iter()
.any(|b| b["code"] == "IRI_COLLISION"),
"{refusal}"
);
assert!(!repo.join(PAGES).join("New Thing.md").exists());
}