use std::path::{Path, PathBuf};
use std::process::{Command, Output};
struct Sandbox {
_dir: tempfile::TempDir,
repo: PathBuf,
home: PathBuf,
}
fn sandbox() -> Sandbox {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path().join("repo");
let home = dir.path().join("home");
std::fs::create_dir_all(repo.join(".git")).unwrap();
std::fs::create_dir_all(&home).unwrap();
Sandbox {
_dir: dir,
repo,
home,
}
}
impl Sandbox {
fn omh(&self, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_omh"))
.args(args)
.current_dir(&self.repo)
.env("HOME", &self.home)
.output()
.expect("the binary under test must run")
}
fn local_store(&self) -> PathBuf {
self.home
.join(".omh/notes")
.join(self.repo.file_name().unwrap())
.join("local")
}
fn seed(&self, at: &str, body: &str) {
let path = self.local_store().join(at);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
fn git_init(&self) {
std::fs::remove_dir_all(self.repo.join(".git")).unwrap();
let out = Command::new("git")
.arg("-C")
.arg(&self.repo)
.args(["init", "-q", "-b", "main"])
.output()
.expect("git must be installed to run this test");
assert!(out.status.success(), "git init failed");
}
fn team_store(&self) -> PathBuf {
self.repo.join(".omh/notes")
}
}
fn note(key: &str, body: &str) -> String {
format!(
"---\nkey: {key}\ntype: surprise\nsource: audit\nrecorded: 2026-08-10\n---\n\n# T\n\n{body}"
)
}
const WHOLE: &str =
"## Expected\na\n\n## Observed\nb\n\n## Evidence\nc\n\n## Answers\n\n- what happens here\n";
#[test]
fn lint_fails_the_command_when_the_schema_refused_something() {
let sb = sandbox();
sb.seed("broken.md", ¬e("broken", "## Expected\na\n"));
let out = sb.omh(&["memory", "lint"]);
assert!(
!out.status.success(),
"a store with refusals must fail the command"
);
let printed = String::from_utf8_lossy(&out.stdout);
assert!(
printed.contains("refused"),
"the report is the product and prints before the exit code: {printed}"
);
}
#[test]
fn lint_passes_a_store_that_only_has_warnings() {
let sb = sandbox();
sb.seed("fine.md", ¬e("fine", WHOLE));
let out = sb.omh(&["memory", "lint"]);
assert!(
out.status.success(),
"warnings must not fail the command: {}",
String::from_utf8_lossy(&out.stdout)
);
assert!(String::from_utf8_lossy(&out.stdout).contains("warning"));
}
#[test]
#[ignore = "needs a container runtime; run with --include-ignored"]
fn init_delivers_the_note_rules_to_a_repo_that_already_had_agents_md() {
let sb = sandbox();
let agents = sb.repo.join(".omh/profile/AGENTS.md");
std::fs::create_dir_all(agents.parent().unwrap()).unwrap();
let human = "# Project rules\n\n## House style\n\nTabs, and no adverbs.\n";
std::fs::write(&agents, human).unwrap();
assert!(sb.omh(&["init"]).status.success());
let body = std::fs::read_to_string(&agents).unwrap();
assert!(body.starts_with(human), "a human's file must survive whole");
assert!(body.contains("## Memory"), "the rules must arrive");
}
#[test]
fn rm_refuses_an_at_that_names_no_note() {
let sb = sandbox();
sb.seed("solo.md", ¬e("solo", WHOLE));
let out = sb.omh(&["memory", "rm", "solo", "--at", "elsewhere.md"]);
assert!(!out.status.success());
assert!(
sb.local_store().join("solo.md").exists(),
"a note the caller did not name was removed"
);
}
#[test]
fn remember_refuses_a_key_template_that_leaves_the_store() {
let sb = sandbox();
std::fs::create_dir_all(sb.repo.join(".omh")).unwrap();
std::fs::write(
sb.repo.join(".omh/keys.toml"),
"[keys]\nsurprise = \"../../escaped/{{slug}}\"\ntopic = \"{{slug}}\"\nstub = \"docs/{{path}}\"\n",
)
.unwrap();
let out = sb.omh(&[
"memory",
"remember",
"--expected",
"a",
"--observed",
"the mount failed",
"--evidence",
"c",
]);
assert!(!out.status.success());
assert!(
!escaped_notes(sb.home.parent().unwrap()),
"a note was written outside the store"
);
}
fn escaped_notes(under: &Path) -> bool {
let mut stack = vec![under.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "md")
&& !path.components().any(|c| c.as_os_str() == "notes")
{
return true;
}
}
}
false
}
#[test]
fn promote_fails_the_command_and_moves_nothing_when_a_key_is_blocked() {
let sb = sandbox();
sb.git_init();
sb.seed("private.md", ¬e("private", WHOLE));
sb.seed(
"candidate.md",
¬e(
"candidate",
&format!("{WHOLE}\n## Related\n\n- [[private]]\n"),
),
);
let out = sb.omh(&["memory", "promote", "candidate"]);
assert!(
!out.status.success(),
"a refused promotion must fail the command"
);
let said = String::from_utf8_lossy(&out.stderr);
assert!(
said.contains("private"),
"the blocker names what to fix, on stderr: {said}"
);
assert!(
sb.local_store().join("candidate.md").exists(),
"and the note is still in the gitignored layer"
);
assert!(
!sb.team_store().join("candidate.md").exists(),
"and nothing was committed-layer written"
);
}
#[test]
fn promote_moves_the_note_and_says_it_is_not_shared_yet() {
let sb = sandbox();
sb.git_init();
sb.seed("fine.md", ¬e("fine", WHOLE));
let out = sb.omh(&["memory", "promote", "fine"]);
assert!(
out.status.success(),
"a clean note promotes: {}",
String::from_utf8_lossy(&out.stderr)
);
let printed = String::from_utf8_lossy(&out.stdout);
assert!(
printed.contains("not shared until committed"),
"moving the file is not sharing it: {printed}"
);
assert!(
sb.team_store().join("fine.md").exists(),
"the note is in the committed layer"
);
assert!(
!sb.local_store().join("fine.md").exists(),
"and no longer in the gitignored one"
);
}
fn hash_object(repo: &Path, rel: &str) -> String {
let out = Command::new("git")
.arg("-C")
.arg(repo)
.args(["hash-object", "--"])
.arg(rel)
.output()
.expect("git must be installed to run this test");
assert!(out.status.success(), "git hash-object failed");
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
fn note_expiring(key: &str, trigger: &str) -> String {
format!(
"---\nkey: {key}\ntype: surprise\nsource: audit\nrecorded: 2026-08-10\n\
invalidated_by: {trigger}\n---\n\n# T\n\n{WHOLE}"
)
}
#[test]
fn stale_fails_the_command_when_a_note_is_out_of_date() {
let sb = sandbox();
sb.git_init();
std::fs::write(sb.repo.join("t.txt"), "before\n").unwrap();
sb.seed("pinned.md", ¬e_expiring("pinned", "file:t.txt@0000000"));
let out = sb.omh(&["memory", "stale"]);
assert_eq!(
out.status.code(),
Some(1),
"a stale store must fail: {}",
String::from_utf8_lossy(&out.stdout)
);
let printed = String::from_utf8_lossy(&out.stdout);
assert!(
printed.contains("stale"),
"the report is the product: {printed}"
);
}
#[test]
fn stale_exits_zero_when_every_note_is_current() {
let sb = sandbox();
sb.git_init();
std::fs::write(sb.repo.join("t.txt"), "before\n").unwrap();
let real = hash_object(&sb.repo, "t.txt");
sb.seed(
"pinned.md",
¬e_expiring("pinned", &format!("file:t.txt@{real}")),
);
let out = sb.omh(&["memory", "stale"]);
assert_eq!(
out.status.code(),
Some(0),
"nothing is stale: {}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn stale_reports_a_separate_code_when_it_cannot_tell() {
let sb = sandbox();
sb.git_init();
sb.seed("sym.md", ¬e_expiring("sym", "symbol:GUEST_HOME"));
let out = sb.omh(&["memory", "stale"]);
assert_eq!(
out.status.code(),
Some(2),
"cannot-tell has its own code: {}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn stale_never_files_what_it_cannot_tell_under_stale() {
let sb = sandbox();
sb.git_init();
sb.seed("sym.md", ¬e_expiring("sym", "symbol:GUEST_HOME"));
let printed = String::from_utf8_lossy(&sb.omh(&["memory", "stale"]).stdout).to_string();
let cannot = printed.find("omh cannot tell").expect(&printed);
let key = printed.find("sym").expect(&printed);
assert!(
printed.find("stale:").is_none(),
"nothing is known to be stale here: {printed}"
);
assert!(
key > cannot,
"the note belongs under that heading: {printed}"
);
}