use std::path::{Path, PathBuf};
use std::process::{Command, Output};
struct Sandbox {
_dir: tempfile::TempDir,
repo: PathBuf,
home: PathBuf,
bin: PathBuf,
}
fn sandbox() -> Sandbox {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path().join("repo");
let home = dir.path().join("home");
let bin = dir.path().join("bin");
std::fs::create_dir_all(repo.join(".git")).unwrap();
std::fs::create_dir_all(&home).unwrap();
std::fs::create_dir_all(&bin).unwrap();
Sandbox {
_dir: dir,
repo,
home,
bin,
}
}
impl Sandbox {
fn omh(&self, args: &[&str]) -> Output {
let path = match std::env::var("PATH") {
Ok(rest) => format!("{}:{rest}", self.bin.display()),
Err(_) => self.bin.display().to_string(),
};
Command::new(env!("CARGO_BIN_EXE_omh"))
.args(args)
.current_dir(&self.repo)
.env("HOME", &self.home)
.env("PATH", path)
.output()
.expect("the binary under test must run")
}
fn fake_docker(&self) -> PathBuf {
let log = self.bin.join("docker.log");
let shim = self.bin.join("docker");
std::fs::write(
&shim,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {}\n\
if [ \"$1\" = inspect ]; then echo true; fi\n\
if [ \"$1\" = ps ]; then cat {} 2>/dev/null; fi\nexit 0\n",
log.display(),
self.bin.join("containers").display()
),
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o755)).unwrap();
}
std::fs::create_dir_all(self.repo.join(".omh")).unwrap();
std::fs::write(
self.repo.join(".omh/settings.toml"),
"runtime = \"docker\"\n",
)
.unwrap();
log
}
fn docker_calls(&self, log: &Path) -> Vec<String> {
std::fs::read_to_string(log)
.unwrap_or_default()
.lines()
.map(str::to_string)
.collect()
}
fn seed_base(&self) {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("base");
let dst = self.home.join(".omh/base");
std::fs::create_dir_all(&dst).unwrap();
for entry in std::fs::read_dir(src).unwrap().flatten() {
std::fs::copy(entry.path(), dst.join(entry.file_name())).unwrap();
}
}
fn settings(&self) -> String {
std::fs::read_to_string(self.repo.join(".omh/settings.toml")).unwrap_or_default()
}
fn catalogue(&self, entries: &[&str]) {
for entry in entries {
let p = self.home.join(".omh").join(entry);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, "x").unwrap();
}
}
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]
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/memory.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}"
);
}
impl Sandbox {
fn session(&self, id: &str) -> PathBuf {
self.git_init();
let origin = self._dir.path().join("origin.git");
Command::new("git")
.args(["init", "-q", "--bare"])
.arg(&origin)
.output()
.expect("git must be installed to run this test");
let git = |args: &[&str]| {
let out = Command::new("git")
.arg("-C")
.arg(&self.repo)
.args(args)
.output()
.expect("git must be installed to run this test");
assert!(out.status.success(), "git {args:?}: {out:?}");
};
git(&["config", "user.email", "t@example.com"]);
git(&["config", "user.name", "t"]);
git(&["commit", "-q", "--allow-empty", "-m", "root"]);
git(&["remote", "add", "origin", origin.to_str().unwrap()]);
let worktree = self
.home
.join(".omh/worktrees")
.join(self.repo.file_name().unwrap())
.join(id);
std::fs::create_dir_all(worktree.parent().unwrap()).unwrap();
git(&[
"worktree",
"add",
"-q",
worktree.to_str().unwrap(),
"-b",
&format!("omh/{id}"),
]);
worktree
}
}
#[test]
fn committing_with_no_session_says_so_rather_than_inventing_one() {
let sb = sandbox();
let out = sb.omh(&["s", "commit", "-m", "anything"]);
assert!(!out.status.success(), "there is nothing to commit to");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("no sessions"), "got: {err}");
}
#[test]
fn work_committed_from_the_host_is_what_diff_then_reports() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("feature.rs"), "fn main() {}").unwrap();
let out = sb.omh(&["s", "commit", "-m", "Add the feature"]);
assert!(
out.status.success(),
"commit failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let printed = String::from_utf8_lossy(&sb.omh(&["s", "diff", "s01"]).stdout).to_string();
assert!(printed.contains("feature.rs"), "got: {printed}");
}
#[test]
fn a_session_id_that_is_a_path_is_refused() {
let sb = sandbox();
sb.session("s01");
let out = sb.omh(&["-s", "../escape", "s", "commit", "-m", "x"]);
assert!(!out.status.success(), "`../escape` is not a session id");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("not a path"),
"refused for the wrong reason: {err}"
);
}
#[test]
fn a_session_that_has_committed_but_never_pushed_is_not_reported_as_clean() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("feature.rs"), "fn main() {}").unwrap();
assert!(sb
.omh(&["s", "commit", "-m", "Add the feature"])
.status
.success());
let printed = String::from_utf8_lossy(&sb.omh(&["s", "ls"]).stdout).to_string();
assert!(printed.contains("to push"), "got: {printed}");
}
#[test]
fn s_ls_renders_each_state_a_session_can_sit_in() {
let sb = sandbox();
let worktree = sb.session("s01");
let ls = || String::from_utf8_lossy(&sb.omh(&["s", "ls"]).stdout).to_string();
std::fs::write(worktree.join("a.rs"), "fn a() {}").unwrap();
assert!(ls().contains("1 uncommitted"), "got: {}", ls());
assert!(sb.omh(&["s", "commit", "-m", "Add a"]).status.success());
assert!(ls().contains("1 to push"), "got: {}", ls());
assert!(sb.omh(&["s", "push", "feat/a"]).status.success());
assert!(ls().contains("→ feat/a"), "got: {}", ls());
}
#[test]
fn a_session_omh_cannot_read_is_never_rendered_as_clean() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("a.rs"), "fn a() {}").unwrap();
std::fs::write(worktree.join(".git"), "gitdir: /nowhere/that/exists").unwrap();
let printed = String::from_utf8_lossy(&sb.omh(&["s", "ls"]).stdout).to_string();
assert!(
printed.contains("s01"),
"the session is still listed: {printed}"
);
assert!(
printed.contains('?'),
"omh cannot tell, and must say so rather than imply clean: {printed}"
);
}
#[test]
fn the_push_command_carries_its_name_and_refuses_without_one() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("a.rs"), "fn a() {}").unwrap();
assert!(sb.omh(&["s", "commit", "-m", "Add a"]).status.success());
let bare = sb.omh(&["s", "push"]);
assert!(!bare.status.success(), "a session id is not a branch name");
assert!(String::from_utf8_lossy(&bare.stderr).contains("not a branch name"));
assert!(sb.omh(&["s", "push", "feat/a"]).status.success());
let printed = String::from_utf8_lossy(&sb.omh(&["s", "push", "feat/a"]).stdout).to_string();
assert!(printed.contains("origin/feat/a"), "got: {printed}");
}
#[test]
fn a_session_that_does_not_exist_is_named_in_the_refusal() {
let sb = sandbox();
sb.session("s01");
let out = sb.omh(&["-s", "s99", "s", "commit", "-m", "x"]);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("s99"), "the refusal must name it: {err}");
}
#[test]
#[ignore]
fn a_dry_run_discloses_the_repos_hooks_and_records_nothing() {
let sb = sandbox();
sb.git_init();
std::fs::write(sb.repo.join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
assert!(
sb.omh(&["init"]).status.success(),
"init must set the repo up"
);
std::fs::write(
sb.repo.join(".omh/hooks/rust-test.json"),
r#"{ "on": "turn-end", "run": "cargo test" }"#,
)
.unwrap();
let out = sb.omh(&["--dry-run", "claude"]);
let said = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
said.contains("this repo's hooks") && said.contains("rust-test"),
"a launch has to name the executable content it was handed: {said}"
);
let snapshot = sb
.home
.join(".omh/run")
.join(sb.repo.file_name().unwrap())
.join("hooks.json");
assert!(
!snapshot.exists(),
"a dry run recorded {} — the next real launch would be silent about a change",
snapshot.display()
);
}
#[test]
#[ignore]
fn a_dry_run_names_the_catalogue_entries_this_repo_is_not_using() {
let sb = sandbox();
sb.git_init();
assert!(
sb.omh(&["init"]).status.success(),
"init must set the repo up"
);
std::fs::create_dir_all(sb.home.join(".omh/skills/refactor")).unwrap();
std::fs::write(sb.home.join(".omh/skills/refactor/SKILL.md"), "x").unwrap();
let said = String::from_utf8_lossy(&sb.omh(&["--dry-run", "claude"]).stderr).to_string();
assert!(
said.contains("skills/refactor"),
"a launch has to name what it is not doing: {said}"
);
assert!(
said.contains("omh use skills refactor"),
"and the command that fixes it: {said}"
);
}
#[test]
fn use_writes_the_committed_file_and_unuse_takes_a_name_back_out() {
let sb = sandbox();
sb.seed_base();
sb.catalogue(&["skills/review-diff/SKILL.md", "skills/refactor/SKILL.md"]);
assert!(sb.omh(&["use", "skills", "review-diff"]).status.success());
let written = sb.settings();
assert!(written.contains("review-diff"), "got: {written}");
assert!(
written.contains("refactor"),
"a capability that was following the whole catalogue must not be \
narrowed to one name by adding one: {written}"
);
assert!(
!sb.repo.join(".omh/settings.local.toml").exists(),
"the gitignored file is `omh repo set`'s, not this command's"
);
assert!(sb.omh(&["unuse", "skills", "refactor"]).status.success());
let written = sb.settings();
assert!(written.contains("review-diff"), "got: {written}");
assert!(!written.contains("refactor"), "taken back out: {written}");
}
#[test]
fn use_is_idempotent_and_unuse_refuses_a_name_this_repo_never_used() {
let sb = sandbox();
sb.seed_base();
sb.catalogue(&["skills/review-diff/SKILL.md"]);
sb.omh(&["use", "skills", "review-diff"]);
let before = sb.settings();
let out = sb.omh(&["use", "skills", "review-diff"]);
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("already used"));
assert_eq!(sb.settings(), before, "selecting it again touched the file");
let out = sb.omh(&["unuse", "skills", "nosuchthing"]);
assert!(!out.status.success(), "a typo must not report success");
assert!(String::from_utf8_lossy(&out.stderr).contains("nosuchthing"));
}
#[test]
fn use_refuses_a_feature_and_disable_refuses_an_entry() {
let sb = sandbox();
sb.seed_base();
sb.catalogue(&["skills/review-diff/SKILL.md"]);
let out = sb.omh(&["use", "mcp", "codegraph"]);
assert!(!out.status.success(), "codegraph is omh's, not yours");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("codegraph"), "must name it: {err}");
assert!(
err.contains("omh repo disable codegraph"),
"and point at the switch that does work: {err}"
);
let out = sb.omh(&["repo", "disable", "review-diff"]);
assert!(!out.status.success(), "a skill is not a feature");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("omh use"), "point back the other way: {err}");
}
#[test]
fn repo_disable_switches_a_feature_off_here_without_uninstalling_it() {
let sb = sandbox();
sb.seed_base();
let out = sb.omh(&["repo", "disable", "codegraph"]);
assert!(
out.status.success(),
"{:?}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
sb.settings().contains("codegraph = false"),
"{}",
sb.settings()
);
assert!(String::from_utf8_lossy(&out.stdout).contains("nothing was uninstalled"));
assert!(sb.omh(&["repo", "enable", "codegraph"]).status.success());
assert!(
sb.settings().contains("codegraph = true"),
"{}",
sb.settings()
);
}
#[test]
fn repo_set_is_gitignored_and_shared_says_it_is_not() {
let sb = sandbox();
sb.seed_base();
assert!(sb
.omh(&["repo", "set", "carry_in", "[\".env\"]"])
.status
.success());
let local = std::fs::read_to_string(sb.repo.join(".omh/settings.local.toml")).unwrap();
assert!(local.contains(".env"), "got: {local}");
let out = sb.omh(&["repo", "set", "--shared", "idle_timeout", "30m"]);
assert!(out.status.success());
assert!(sb.settings().contains("30m"), "{}", sb.settings());
assert!(
String::from_utf8_lossy(&out.stdout).contains("COMMITTED"),
"writing the committed file has to say so"
);
}
#[test]
fn config_set_writes_your_defaults() {
let sb = sandbox();
sb.seed_base();
assert!(sb
.omh(&["config", "set", "idle_timeout", "45m"])
.status
.success());
let personal = std::fs::read_to_string(sb.home.join(".omh/settings.toml")).unwrap();
assert!(personal.contains("45m"), "got: {personal}");
assert!(
!sb.repo.join(".omh/settings.local.toml").exists(),
"this is not a repo-scoped command any more"
);
}
#[test]
fn layer_still_works_and_names_what_replaced_it() {
let sb = sandbox();
sb.seed_base();
let out = sb.omh(&["config", "set", "--layer", "shared", "idle_timeout", "1h"]);
assert!(out.status.success(), "still works");
assert!(sb.settings().contains("1h"), "and writes where it said");
let said = String::from_utf8_lossy(&out.stderr);
assert!(said.contains("going away"), "got: {said}");
assert!(
said.contains("omh repo set --shared"),
"and names the form that replaces it: {said}"
);
}
#[test]
fn edit_refuses_a_name_that_climbs_out_of_the_catalogue() {
let sb = sandbox();
sb.seed_base();
let out = sb.omh(&["config", "edit", "skills", "../../../.ssh/id_rsa"]);
assert!(!out.status.success(), "traversal must not reach $EDITOR");
assert!(String::from_utf8_lossy(&out.stderr).contains("never a path"));
}
#[test]
fn bare_repo_reports_what_is_used_what_is_not_and_what_decided_it() {
let sb = sandbox();
sb.seed_base();
sb.catalogue(&["skills/review-diff/SKILL.md", "skills/refactor/SKILL.md"]);
sb.omh(&["use", "skills", "review-diff"]);
sb.omh(&["unuse", "skills", "refactor"]);
sb.omh(&["repo", "disable", "codegraph"]);
sb.omh(&["repo", "set", "carry_in", "[\".env\"]"]);
let out = sb.omh(&["repo"]);
assert!(
out.status.success(),
"{:?}",
String::from_utf8_lossy(&out.stderr)
);
let said = String::from_utf8_lossy(&out.stdout);
assert!(said.contains("review-diff"), "what is used: {said}");
assert!(said.contains("refactor"), "and what is not: {said}");
assert!(said.contains("codegraph"), "omh's features: {said}");
assert!(said.contains("off here"), "and their state: {said}");
assert!(said.contains("carry_in"), "settings: {said}");
assert!(
said.contains("local"),
"and which file decided each: {said}"
);
}
#[test]
fn writing_a_setting_keeps_what_you_wrote_around_it() {
let sb = sandbox();
sb.seed_base();
sb.catalogue(&["skills/review-diff/SKILL.md"]);
std::fs::create_dir_all(sb.repo.join(".omh")).unwrap();
std::fs::write(
sb.repo.join(".omh/settings.toml"),
"# why this repo carries an env file\ncarry_in = [\".env.local\"] # the app needs it\n",
)
.unwrap();
assert!(sb.omh(&["use", "skills", "review-diff"]).status.success());
let after = sb.settings();
assert!(
after.contains("# why this repo carries an env file"),
"the comment above a setting is part of the setting: {after}"
);
assert!(
after.contains("# the app needs it"),
"and so is the one beside it: {after}"
);
assert!(
after.contains("review-diff"),
"and the write happened: {after}"
);
}
#[test]
fn init_installs_every_directory_this_repo_ships() {
let sb = sandbox();
std::fs::write(sb.repo.join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_omh"))
.arg("init")
.current_dir(&sb.repo)
.env("HOME", &sb.home)
.env("PATH", "/nonexistent")
.output()
.expect("the binary under test must run");
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"));
for kind in ["adapters", "base", "editors", "stacks"] {
let src = repo_root.join(kind);
let shipped: Vec<String> = std::fs::read_dir(&src)
.unwrap_or_else(|e| panic!("this repo must ship {kind}: {e}"))
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".toml"))
.collect();
assert!(!shipped.is_empty(), "{kind} ships nothing");
for name in shipped {
let landed = sb.home.join(".omh").join(kind).join(&name);
assert!(
landed.exists(),
"{kind}/{name} is in the repo and not in ~/.omh — embedded but \
never installed, or never embedded. stdout: {} stderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
}
}
#[test]
fn a_missing_container_runtime_records_no_resolution() {
let sb = sandbox();
std::fs::write(sb.repo.join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_omh"))
.arg("init")
.current_dir(&sb.repo)
.env("HOME", &sb.home)
.env("PATH", "/nonexistent")
.output()
.expect("the binary under test must run");
let settings = sb.settings();
assert!(
!settings.contains("[provision]"),
"an unmeasured resolution must not be written: {settings}\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(settings.contains("[use]"), "got: {settings}");
}
#[test]
fn a_missing_container_runtime_still_leaves_the_repo_configured() {
let sb = sandbox();
sb.git_init();
std::fs::write(sb.repo.join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
sb.catalogue(&["skills/review-diff/SKILL.md"]);
let out = Command::new(env!("CARGO_BIN_EXE_omh"))
.arg("init")
.current_dir(&sb.repo)
.env("HOME", &sb.home)
.env("PATH", "/nonexistent")
.output()
.expect("the binary under test must run");
let gitignore = std::fs::read_to_string(sb.repo.join(".omh/.gitignore")).unwrap_or_default();
assert!(
gitignore.contains("settings.local.toml"),
"a tracked settings.local.toml is how a machine-local override gets \
committed to the team's repo. stdout: {} stderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
sb.settings().contains("[use]"),
"and the selection is what switches this repo's own hooks on: {}",
sb.settings()
);
}
#[test]
#[ignore]
fn init_writes_the_selection_expanded() {
let sb = sandbox();
sb.git_init();
std::fs::write(sb.repo.join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
sb.catalogue(&["skills/review-diff/SKILL.md"]);
assert!(sb.omh(&["init"]).status.success());
let written = sb.settings();
assert!(written.contains("[use]"), "got: {written}");
assert!(written.contains("review-diff"), "your catalogue: {written}");
let hooks: Vec<String> = std::fs::read_dir(sb.repo.join(".omh/hooks"))
.expect("init creates the hooks directory")
.flatten()
.map(|e| {
e.file_name()
.to_string_lossy()
.trim_end_matches(".json")
.to_string()
})
.collect();
for h in &hooks {
assert!(
written.contains(h.as_str()),
"init wrote {h} and then left it out of the selection: {written}"
);
}
assert!(
written.contains("\"rust-test\"") && written.contains("\"rust-format\""),
"the detected stack's hooks are unconditional: {written}"
);
assert!(
!written.contains("go-test") && !written.contains("python-test"),
"and an ecosystem this repo is not must not be selected into it: {written}"
);
assert!(
!written.contains("codegraph") || !written.contains("mcp = [\"codegraph"),
"omh's own are `[omh]`'s, not `[use]`'s: {written}"
);
assert!(
written.contains("# carry_in"),
"init's own explanation has to survive its own write: {written}"
);
assert!(sb.omh(&["unuse", "skills", "review-diff"]).status.success());
assert!(sb.omh(&["init"]).status.success());
assert!(
!sb.settings().contains("review-diff"),
"init writes the list once; `omh use --all` is how you ask for a resync"
);
}
#[test]
fn use_writes_every_repo_layer_that_already_declares_the_capability() {
let sb = sandbox();
sb.seed_base();
sb.catalogue(&["skills/review-diff/SKILL.md", "skills/refactor/SKILL.md"]);
std::fs::create_dir_all(sb.repo.join(".omh")).unwrap();
std::fs::write(
sb.repo.join(".omh/settings.local.toml"),
"[use]\nskills = [\"review-diff\", \"refactor\"]\n",
)
.unwrap();
let out = sb.omh(&["unuse", "skills", "refactor"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let local = std::fs::read_to_string(sb.repo.join(".omh/settings.local.toml")).unwrap();
assert!(
!local.contains("refactor"),
"the layer that decides has to be the layer that changed: {local}"
);
assert!(
local.contains("review-diff"),
"and only that name went: {local}"
);
assert!(
sb.settings().contains("review-diff") && !sb.settings().contains("refactor"),
"the committed file is still the one a teammate gets: {}",
sb.settings()
);
assert!(
String::from_utf8_lossy(&out.stdout).contains("settings.local.toml"),
"and it says both files were written: {}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn a_local_file_that_declares_nothing_stays_that_way() {
let sb = sandbox();
sb.seed_base();
sb.catalogue(&["skills/review-diff/SKILL.md"]);
std::fs::create_dir_all(sb.repo.join(".omh")).unwrap();
std::fs::write(
sb.repo.join(".omh/settings.local.toml"),
"carry_in = [\".env\"]\n",
)
.unwrap();
assert!(sb.omh(&["use", "skills", "review-diff"]).status.success());
let local = std::fs::read_to_string(sb.repo.join(".omh/settings.local.toml")).unwrap();
assert!(
!local.contains("[use]"),
"nothing was declared there: {local}"
);
}
#[test]
fn a_feature_switch_reaches_the_layer_that_decides() {
let sb = sandbox();
sb.seed_base();
std::fs::create_dir_all(sb.repo.join(".omh")).unwrap();
std::fs::write(
sb.repo.join(".omh/settings.local.toml"),
"[omh]\ncodegraph = false\n",
)
.unwrap();
assert!(sb.omh(&["repo", "enable", "codegraph"]).status.success());
let local = std::fs::read_to_string(sb.repo.join(".omh/settings.local.toml")).unwrap();
assert!(
local.contains("codegraph = true"),
"the local switch is what decides, so it is what has to move: {local}"
);
}
#[test]
fn rm_takes_the_session_container_down_with_the_worktree() {
let sb = sandbox();
let log = sb.fake_docker();
let worktree = sb.home.join(".omh/worktrees/repo/s01");
std::fs::create_dir_all(&worktree).unwrap();
let run = sb.home.join(".omh/run/repo/s01");
std::fs::create_dir_all(&run).unwrap();
let out = sb.omh(&["s", "rm", "s01"]);
assert!(
out.status.success(),
"rm failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(!worktree.exists(), "the worktree must be gone");
assert!(
!run.exists(),
"the session's staging and its last-used marker outlived it"
);
let calls = sb.docker_calls(&log);
assert!(
calls
.iter()
.any(|c| c.starts_with("rm ") && c.contains("omh-repo-s01")),
"the container outlived the worktree it mounts: {calls:?}"
);
}
#[test]
fn s_ls_names_what_removed_sessions_left_behind() {
let sb = sandbox();
let _log = sb.fake_docker();
std::fs::write(sb.bin.join("containers"), "omh-repo-s03\n").unwrap();
let launched = sb.home.join(".omh/run/repo/s02");
std::fs::create_dir_all(&launched).unwrap();
std::fs::write(launched.join("last-used"), "").unwrap();
std::fs::create_dir_all(sb.home.join(".omh/run/repo/doctor")).unwrap();
let out = sb.omh(&["s", "ls"]);
let printed = String::from_utf8_lossy(&out.stdout);
assert!(
printed.contains("s02"),
"a run directory left behind: {printed}"
);
assert!(
printed.contains("s03"),
"a container left behind: {printed}"
);
assert!(
!printed.contains("doctor"),
"scratch staging is not a removed session: {printed}"
);
}
impl Sandbox {
fn seed_adapters(&self) {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("adapters");
let dst = self.home.join(".omh/adapters");
std::fs::create_dir_all(&dst).unwrap();
for entry in std::fs::read_dir(src).unwrap().flatten() {
std::fs::copy(entry.path(), dst.join(entry.file_name())).unwrap();
}
}
fn harness_hooks(&self, body: &str) -> PathBuf {
let p = self.home.join("their-settings.json");
std::fs::write(&p, body).unwrap();
p
}
fn repo_hook(&self, name: &str) -> Option<String> {
std::fs::read_to_string(self.repo.join(".omh/hooks").join(format!("{name}.json"))).ok()
}
}
#[test]
fn importing_hooks_writes_them_into_this_repo() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
let theirs = fx.harness_hooks(
r#"{"hooks":{
"Stop":[{"matcher":"","hooks":[{"type":"command","command":"cargo test"}]}],
"PostToolUse":[{"matcher":"Edit|Write|MultiEdit","hooks":[{"type":"command","command":"cargo fmt"}]}]}}"#,
);
let out = fx.omh(&[
"import",
"hooks",
"claude",
"--from",
theirs.to_str().unwrap(),
]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let test = fx
.repo_hook("turn-end-cargo")
.expect("the turn-end hook must be in this repo");
assert!(test.contains("cargo test"), "got: {test}");
assert!(
test.contains("\"on\": \"turn-end\""),
"in omh's words, not Claude's: {test}"
);
let fmt = fx
.repo_hook("after-tool-cargo")
.expect("the after-tool hook must be in this repo");
assert!(
fmt.contains("cargo fmt") && fmt.contains("edit"),
"got: {fmt}"
);
assert!(
!fx.home.join(".omh/hooks/turn-end-cargo.json").exists(),
"a repo's hook must not be installed into the catalogue"
);
}
#[test]
fn importing_leaves_the_harness_config_untouched() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
let body = r#"{"hooks":{"Stop":[{"matcher":"","hooks":[{"type":"command","command":"cargo test"}]}]}}"#;
let theirs = fx.harness_hooks(body);
fx.omh(&[
"import",
"hooks",
"claude",
"--from",
theirs.to_str().unwrap(),
]);
assert_eq!(
std::fs::read_to_string(&theirs).unwrap(),
body,
"the harness's own config must be byte-for-byte what it was"
);
}
#[test]
fn imported_hooks_are_selected_or_they_land_dead() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
std::fs::create_dir_all(fx.repo.join(".omh")).unwrap();
std::fs::write(fx.repo.join(".omh/settings.toml"), "[use]\nhooks = []\n").unwrap();
let theirs = fx.harness_hooks(
r#"{"hooks":{"Stop":[{"matcher":"","hooks":[{"type":"command","command":"cargo test"}]}]}}"#,
);
let out = fx.omh(&[
"import",
"hooks",
"claude",
"--from",
theirs.to_str().unwrap(),
]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
fx.settings().contains("turn-end-cargo"),
"an imported hook must reach `[use]`, or it never runs: {}",
fx.settings()
);
}
#[test]
fn importing_refuses_a_name_omh_ships() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
let theirs = fx.harness_hooks(
r#"{"hooks":{"Stop":[{"matcher":"","hooks":[{"type":"command","command":"graph-refresh --now"}]}]}}"#,
);
let out = fx.omh(&[
"import",
"hooks",
"claude",
"--from",
theirs.to_str().unwrap(),
]);
let said = String::from_utf8_lossy(&out.stdout).to_string();
assert!(out.status.success(), "importing is not fatal: {said}");
let why = fx.omh(&["why", "codegraph"]);
assert!(
why.status.success(),
"importing left this repo unable to launch: {}",
String::from_utf8_lossy(&why.stderr)
);
}
#[test]
fn what_omh_cannot_import_is_reported_rather_than_dropped() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
let theirs = fx.harness_hooks(
r#"{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[
{"type":"command","command":"guard","if":"tool.name == 'Bash'"}]}]}}"#,
);
let out = fx.omh(&[
"import",
"hooks",
"claude",
"--from",
theirs.to_str().unwrap(),
]);
let said = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "{said}");
assert!(said.contains("left"), "the residue is reported: {said}");
assert!(
fx.repo_hook("before-tool-guard").is_none(),
"and a hook whose permission gate omh cannot express is not written \
without it"
);
}
impl Sandbox {
fn theirs(&self, at: &str, body: &str) -> PathBuf {
let p = self.home.join(at);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(&p, body).unwrap();
p
}
fn mine(&self, at: &str) -> Option<String> {
std::fs::read_to_string(self.home.join(".omh").join(at)).ok()
}
}
#[test]
fn importing_a_skill_puts_it_in_your_catalogue() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
fx.theirs(
".claude/skills/review-diff/SKILL.md",
"---\nname: review-diff\ndescription: read a diff\n---\n\nbody\n",
);
fx.theirs(".claude/skills/review-diff/notes/extra.md", "more\n");
let out = fx.omh(&["import", "skills", "claude"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
fx.mine("skills/review-diff/SKILL.md")
.is_some_and(|s| s.contains("read a diff")),
"the skill must be in your catalogue"
);
assert!(
fx.mine("skills/review-diff/notes/extra.md").is_some(),
"a skill is a directory and arrives whole, not just its SKILL.md"
);
assert!(
!fx.repo.join(".omh/skills").exists(),
"a skill is yours across every project, not this repo's"
);
}
#[test]
fn importing_rules_takes_yours_and_not_this_projects() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
fx.theirs(".claude/CLAUDE.md", "always write the test first\n");
std::fs::write(fx.repo.join("CLAUDE.md"), "this project uses tabs\n").unwrap();
let out = fx.omh(&["import", "rules", "claude"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let imported = fx.mine("rules/claude.md").expect("your own rules");
assert!(
imported.contains("always write the test first"),
"got: {imported}"
);
assert!(
!imported.contains("tabs"),
"the project's own rules are composed already — importing them delivers \
the same prose twice: {imported}"
);
}
#[cfg(unix)]
#[test]
fn importing_refuses_a_skill_that_reaches_outside_itself() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
let secret = fx.theirs("secrets/id_rsa", "PRIVATE KEY\n");
fx.theirs(".claude/skills/sneaky/SKILL.md", "---\nname: sneaky\n---\n");
std::os::unix::fs::symlink(&secret, fx.home.join(".claude/skills/sneaky/borrowed.pem"))
.unwrap();
let out = fx.omh(&["import", "skills", "claude"]);
let said = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "one bad entry is not fatal: {said}");
assert!(said.contains("skipped"), "and it is reported: {said}");
assert!(
fx.mine("skills/sneaky/borrowed.pem").is_none()
&& fx.mine("skills/sneaky/SKILL.md").is_none(),
"an entry omh cannot copy whole is not copied in part"
);
}
#[cfg(unix)]
#[test]
fn importing_refuses_an_entry_whose_name_is_a_path() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
fx.theirs(".claude/commands/.hidden.md", "not an entry\n");
fx.theirs(".claude/commands/real.md", "an entry\n");
let out = fx.omh(&["import", "commands", "claude"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
fx.mine("commands/real.md").is_some(),
"the good one arrives"
);
assert!(
fx.mine("commands/.hidden.md").is_none(),
"a dotfile is not a catalogue entry"
);
}
#[test]
fn importing_twice_changes_nothing_the_second_time() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
fx.theirs(".claude/commands/review.md", "theirs\n");
fx.omh(&["import", "commands", "claude"]);
std::fs::write(fx.home.join(".omh/commands/review.md"), "mine, edited\n").unwrap();
let out = fx.omh(&["import", "commands", "claude"]);
assert!(out.status.success());
assert_eq!(
fx.mine("commands/review.md").as_deref(),
Some("mine, edited\n"),
"an import must not replace what you have since written"
);
}
#[cfg(unix)]
#[test]
fn a_copy_that_fails_part_way_leaves_nothing_behind() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
eprintln!("skipped: root reads an unreadable file, so this proves nothing");
return;
}
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
fx.theirs(".claude/skills/big/SKILL.md", "---\nname: big\n---\n");
let locked = fx.theirs(".claude/skills/big/zz-locked.md", "secret\n");
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
let out = fx.omh(&["import", "skills", "claude"]);
let said = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "one bad entry is not fatal: {said}");
assert!(said.contains("skipped"), "and is reported: {said}");
assert!(
!fx.home.join(".omh/skills/big").exists(),
"a half-copied entry must not survive: it is mounted into every sandbox \
exactly as a whole one is"
);
}
#[test]
fn a_hook_init_derives_later_is_selected_too() {
let fx = sandbox();
fx.seed_base();
fx.seed_adapters();
std::fs::create_dir_all(fx.repo.join(".omh")).unwrap();
std::fs::write(fx.repo.join(".omh/settings.toml"), "[use]\nhooks = []\n").unwrap();
std::fs::write(
fx.repo.join("package.json"),
r#"{"scripts":{"test":"vitest run"}}"#,
)
.unwrap();
std::fs::write(fx.repo.join("pnpm-lock.yaml"), "").unwrap();
let out = fx.omh(&["init"]);
let said = String::from_utf8_lossy(&out.stdout);
assert!(
fx.repo.join(".omh/hooks/pnpm-test.json").exists(),
"the hook is derived: {said}"
);
assert!(
fx.settings().contains("pnpm-test"),
"and named in `[use]`, or no session will ever run it: {}",
fx.settings()
);
}