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' \"$*\" >> {log}\n\
[ -f {refuses} ] && {{ echo 'cannot connect to the daemon' >&2; exit 1; }}\n\
if [ \"$1\" = exec ] && [ -f {exec_refuses} ]; then \
cat {exec_refuses} >&2; exit 1; fi\n\
if [ \"$1\" = inspect ]; then echo true; fi\n\
if [ \"$1\" = ps ]; then cat {containers} 2>/dev/null; fi\nexit 0\n",
log = log.display(),
refuses = self.bin.join("docker-refuses").display(),
exec_refuses = self.bin.join("docker-exec-refuses").display(),
containers = 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 fake_docker_with_nothing_built(&self, tags: &[&str], in_use: &[&str]) -> PathBuf {
let log = self.bin.join("docker.log");
let images = self.bin.join("images");
let containers = self.bin.join("containers");
std::fs::write(&images, format!("{}\n", tags.join("\n"))).unwrap();
std::fs::write(&containers, format!("{}\n", in_use.join("\n"))).unwrap();
let shim = self.bin.join("docker");
std::fs::write(
&shim,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\n\
case \"$1 $2\" in\n\
\"image inspect\") exit 1 ;;\n\
\"image rm\") echo \"Untagged: $3\"; echo 'Deleted: sha256:00'; exit 0 ;;\n\
esac\n\
# omh sends the Dockerfile on stdin (`-f -`) so nothing is\n\
# written to disk. A shim that exits without reading it leaves\n\
# omh writing into a pipe with no reader, and omh sets SIGPIPE\n\
# to SIG_DFL on purpose — so it dies of signal 13, silently,\n\
# whenever the Dockerfile loses the race with this exit. That\n\
# is what failed this test on the linux runner three times\n\
# across three branches, each time saying only `init failed`.\n\
if [ \"$1\" = build ]; then cat > /dev/null; fi\n\
if [ \"$1\" = images ]; then cat {images}; fi\n\
if [ \"$1\" = ps ]; then cat {containers}; fi\n\
if [ \"$1\" = inspect ]; then echo true; fi\nexit 0\n",
log = log.display(),
images = images.display(),
containers = 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 sandbox_repo_with_unkept_work(&self, id: &str, worktree: &std::path::Path) {
let shadow = self
.home
.join(".omh/shadow")
.join(self.repo.file_name().unwrap());
std::fs::create_dir_all(&shadow).unwrap();
let gitdir = shadow.join(format!("{id}.git"));
let git = |args: &[&str]| {
let out = Command::new("git")
.arg("--git-dir")
.arg(&gitdir)
.arg("--work-tree")
.arg(worktree)
.args(args)
.output()
.expect("git must be installed to run this test");
assert!(out.status.success(), "git {args:?}: {out:?}");
String::from_utf8_lossy(&out.stdout).trim().to_string()
};
Command::new("git")
.args(["init", "-q", "--bare"])
.arg(&gitdir)
.output()
.unwrap();
git(&["config", "user.email", "sandbox@omh.invalid"]);
git(&["config", "user.name", "omh sandbox"]);
git(&["commit", "-q", "--allow-empty", "--no-verify", "-m", "seed"]);
std::fs::write(
shadow.join(format!("{id}.seed")),
git(&["rev-parse", "HEAD"]),
)
.unwrap();
std::fs::write(worktree.join("agent.rs"), "fn agent() {}\n").unwrap();
git(&["add", "-A", "."]);
git(&["commit", "-q", "--no-verify", "-m", "the agent's own work"]);
}
fn head_of_branch(&self, branch: &str) -> String {
let out = Command::new("git")
.arg("-C")
.arg(&self.repo)
.args(["rev-parse", branch])
.output()
.expect("git must be installed to run this test");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
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 {
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:?}");
};
let first_session = Command::new("git")
.arg("-C")
.arg(&self.repo)
.args(["remote"])
.output()
.is_ok_and(|o| !o.status.success() || o.stdout.is_empty());
if first_session {
self.git_init();
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
}
}
fn omh_turn_hook_body(gitdir: &std::path::Path, worktree: &std::path::Path) -> String {
let g = format!(
"git -C {w} --git-dir={g} --work-tree={w}",
w = worktree.display(),
g = gitdir.display()
);
format!(
"{{ i={i}; GIT_INDEX_FILE=$i {g} read-tree HEAD && GIT_INDEX_FILE=$i {g} add -A \
&& t=$(GIT_INDEX_FILE=$i {g} write-tree) \
&& p=$({g} rev-parse -q --verify refs/omh/turn || true) \
&& if [ -n \"$p\" ] && [ \"$({g} rev-parse \"$p^{{tree}}\")\" = \"$t\" ]; then :; \
else c=$({g} commit-tree \"$t\" ${{p:+-p}} ${{p:+\"$p\"}} -m \"turn end\") \
&& {g} update-ref refs/omh/turn \"$c\"; fi; }} >/dev/null 2>&1 || true",
i = gitdir.join("omh-turn.index").display()
)
}
#[test]
fn rm_names_the_snapshots_it_takes_and_force_still_removes() {
let sb = sandbox();
let worktree = sb.session("s01");
sb.sandbox_repo_with_unkept_work("s01", &worktree);
let gitdir = sb
.home
.join(".omh/shadow")
.join(sb.repo.file_name().unwrap())
.join("s01.git");
std::fs::write(worktree.join("in-flight.rs"), "fn later() {}\n").unwrap();
Command::new("sh")
.arg("-c")
.arg(omh_turn_hook_body(&gitdir, &worktree))
.output()
.expect("sh must be installed");
let refused = sb.omh(&["s01", "rm"]);
assert!(!refused.status.success(), "unharvested work still refuses");
let said = String::from_utf8_lossy(&refused.stderr);
assert!(
said.contains("1 turn snapshot"),
"and the count is a real one, asked of the sandbox: {said}"
);
assert!(
said.contains("omh s01 log --turns"),
"with the command that reads them: {said}"
);
assert!(
sb.omh(&["s01", "rm", "--force"]).status.success(),
"and `--force` still means it"
);
}
#[test]
fn log_turns_reads_the_snapshots_and_the_default_view_does_not() {
let sb = sandbox();
let worktree = sb.session("s01");
sb.sandbox_repo_with_unkept_work("s01", &worktree);
let gitdir = sb
.home
.join(".omh/shadow")
.join(sb.repo.file_name().unwrap())
.join("s01.git");
std::fs::write(worktree.join("in-flight.rs"), "fn later() {}\n").unwrap();
let ran = Command::new("sh")
.arg("-c")
.arg(omh_turn_hook_body(&gitdir, &worktree))
.output()
.expect("sh must be installed");
assert!(ran.status.success(), "the hook never fails a turn: {ran:?}");
let turns = sb.omh(&["s01", "log", "--turns"]);
let printed = String::from_utf8_lossy(&turns.stdout);
assert!(
printed.contains("1 turn"),
"the snapshot is there to read: {printed}"
);
let plain = sb.omh(&["s01", "log"]);
let out = String::from_utf8_lossy(&plain.stdout);
let err = String::from_utf8_lossy(&plain.stderr);
assert!(plain.status.success(), "the default log works: {err}");
assert!(
out.contains("agent"),
"and lists the agent's own commit: {out}"
);
assert!(
!out.contains("turn end"),
"omh's own snapshot is not in the agent's list: {out}"
);
assert!(
!err.contains("on no branch") && !out.contains("on no branch"),
"and nothing calls it work the agent stranded: {out}{err}"
);
}
#[test]
fn a_log_for_a_sandbox_that_never_ran_says_nothing_yet() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("in-progress.rs"), "fn main() {}").unwrap();
let out = sb.omh(&["s01", "log"]);
let printed = String::from_utf8_lossy(&out.stdout);
let said = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"there is nothing wrong with an empty sandbox: {said}"
);
assert!(
printed.contains("no checkpoints"),
"it says so plainly: {printed}"
);
assert!(
printed.contains("uncommitted in the sandbox: 0 files"),
"nothing is staged for a harvest that has nothing to harvest: {printed}"
);
}
#[test]
fn a_patch_asked_for_by_a_program_is_a_field_named_for_what_it_holds() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("feature.rs"), "fn added() {}\n").unwrap();
let read = |args: &[&str]| -> serde_json::Value {
let out = sb.omh(args);
let printed = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
out.status.success(),
"{args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_str(&printed).unwrap_or_else(|e| panic!("not JSON: {e}: {printed}"))
};
let patch = read(&["s01", "diff", "-p", "--json"]);
assert_eq!(patch["changed"], serde_json::json!(true));
assert!(
patch["patch"]
.as_str()
.is_some_and(|s| s.contains("+fn added() {}")),
"the patch itself, under `patch`: {patch}"
);
assert!(
patch["summary"].is_null(),
"and not also under `summary`: {patch}"
);
let summary = read(&["s01", "diff", "--json"]);
assert!(
summary["summary"]
.as_str()
.is_some_and(|s| s.contains("feature.rs") && !s.contains("+fn added() {}")),
"a --stat, under `summary`: {summary}"
);
assert!(
summary["patch"].is_null(),
"nothing a script could hand to `git apply`: {summary}"
);
assert_eq!(
summary["session"],
serde_json::json!("s01"),
"the id, not a phrase: {summary}"
);
}
#[test]
fn the_flag_is_what_decides_whether_a_diff_is_a_summary_or_a_patch() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("feature.rs"), "fn added() {}\n").unwrap();
let summary = String::from_utf8_lossy(&sb.omh(&["s01", "diff"]).stdout).to_string();
let patch = String::from_utf8_lossy(&sb.omh(&["s01", "diff", "-p"]).stdout).to_string();
assert!(
summary.contains("feature.rs") && !summary.contains("+fn added() {}"),
"without the flag, the shape of the change: {summary}"
);
assert!(
patch.contains("+fn added() {}"),
"with it, the change: {patch}"
);
}
#[test]
fn edit_without_a_terminal_refuses_rather_than_pretending_to_curate() {
let sb = sandbox();
sb.session("s01");
let out = sb.omh(&["s01", "commit", "--keep", "--edit"]);
let said = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "there is nowhere to draw: {said}");
assert!(
said.contains("no terminal"),
"and it says why, rather than reporting a curation: {said}"
);
assert!(
said.contains("--keep 1,3-4") || said.contains("Drop `--edit`"),
"with something to do instead: {said}"
);
}
#[test]
fn a_refused_selection_leaves_the_branch_where_it_was() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("work.rs"), "fn work() {}\n").unwrap();
let before = sb.head_of_branch("omh/s01");
for selection in ["9", "0", "two", "4-2"] {
let out = sb.omh(&["s01", "commit", "--keep", selection]);
assert!(
!out.status.success(),
"`--keep {selection}` was accepted: {}",
String::from_utf8_lossy(&out.stdout)
);
}
assert_eq!(
sb.head_of_branch("omh/s01"),
before,
"the branch never moved"
);
}
#[test]
fn sessions_changing_the_same_file_are_named_together() {
let sb = sandbox();
let one = sb.session("s01");
let two = sb.session("s02");
for (worktree, extra) in [(&one, "only-in-s01.rs"), (&two, "only-in-s02.rs")] {
std::fs::write(worktree.join("shared.rs"), "fn shared() {}\n").unwrap();
std::fs::write(worktree.join(extra), "fn mine() {}\n").unwrap();
}
let printed = String::from_utf8_lossy(&sb.omh(&["s"]).stdout).to_string();
assert!(
printed.contains("s01 and s02 both change shared.rs"),
"the file both are changing, and who: {printed}"
);
assert!(
!printed.contains("only-in-s01.rs"),
"and nothing about what only one of them touches: {printed}"
);
let focused = String::from_utf8_lossy(&sb.omh(&["s01"]).stdout).to_string();
assert!(
focused.contains("s01 and s02 both change shared.rs"),
"a collision involving s01 survives the focus: {focused}"
);
assert!(
!String::from_utf8_lossy(&sb.omh(&["s"]).stderr).contains("both change"),
"it is the answer, not a warning"
);
let doc: serde_json::Value = serde_json::from_slice(&sb.omh(&["s", "--json"]).stdout).unwrap();
assert_eq!(
doc["overlaps"],
serde_json::json!([{"sessions": ["s01", "s02"], "paths": ["shared.rs"]}]),
"one answer, two renderings: {doc}"
);
assert_eq!(doc["unreadable"], serde_json::json!([]));
}
#[test]
fn a_session_omh_cannot_read_is_named_rather_than_left_out() {
let sb = sandbox();
let one = sb.session("s01");
let two = sb.session("s02");
std::fs::write(one.join("shared.rs"), "fn shared() {}\n").unwrap();
std::fs::write(two.join("shared.rs"), "fn shared() {}\n").unwrap();
std::fs::write(two.join(".git"), "gitdir: /nowhere-at-all\n").unwrap();
let out = sb.omh(&["s"]);
let printed = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
printed.contains("could not read what s02 is changing"),
"the session omh could not read is named: {printed}"
);
assert!(
printed.contains("incomplete"),
"and what that means for the rest: {printed}"
);
assert!(
!printed.contains("s01 and s02 both change"),
"omh does not invent a collision it could not check: {printed}"
);
}
#[test]
fn a_sandbox_repository_with_no_session_is_reported() {
let sb = sandbox();
let worktree = sb.session("s01");
sb.sandbox_repo_with_unkept_work("s01", &worktree);
let orphan = sb
.home
.join(".omh/shadow")
.join(sb.repo.file_name().unwrap())
.join("s09.git");
std::fs::create_dir_all(&orphan).unwrap();
let out = sb.omh(&["s"]);
let said = String::from_utf8_lossy(&out.stderr);
assert!(
said.contains("s09"),
"a repository nothing points at is named: {said}"
);
assert!(
!said.contains("s01"),
"and a session that is still here is not — every live session has a \
repository, so this filter is the only thing between a healthy checkout \
and being told to `rm` all of it: {said}"
);
assert!(
sb.omh(&["s09", "rm", "--force"]).status.success(),
"the hint `omh s` prints has to be a command that clears it"
);
assert!(!orphan.exists(), "and it did");
assert!(
!String::from_utf8_lossy(&sb.omh(&["s"]).stderr).contains("s09"),
"so a second listing no longer names it"
);
}
#[test]
fn removing_a_session_holding_unkept_work_is_refused_until_it_is_meant() {
let sb = sandbox();
let worktree = sb.session("s01");
sb.sandbox_repo_with_unkept_work("s01", &worktree);
let log = sb.fake_docker();
let run = sb
.home
.join(".omh/run")
.join(sb.repo.file_name().unwrap())
.join("s01");
std::fs::create_dir_all(&run).unwrap();
let gitdir = sb
.home
.join(".omh/shadow")
.join(sb.repo.file_name().unwrap())
.join("s01.git");
let out = sb.omh(&["s01", "rm"]);
let said = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"the agent's commit is on no branch: {said}"
);
assert!(
said.contains("s01 has 1 commit that no branch has"),
"it says what is at stake, in the singular: {said}"
);
assert!(said.contains("--force"), "and how to mean it: {said}");
assert!(
!sb.docker_calls(&log).iter().any(|c| c.starts_with("rm ")),
"the container was taken down on the way to refusing: {:?}",
sb.docker_calls(&log)
);
assert!(run.exists(), "so was the marker `omh s` reads");
assert!(gitdir.exists(), "and the repository the refusal is about");
assert!(worktree.exists());
let out = sb.omh(&["s01", "rm", "--force"]);
assert!(
out.status.success(),
"--force means it: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(!worktree.exists(), "the session is gone");
assert!(
!gitdir.exists(),
"…and so is the sandbox repository it was protecting"
);
}
#[test]
fn a_patch_with_nothing_in_it_says_so_rather_than_showing_a_blank_screen() {
let sb = sandbox();
sb.session("s01");
let out = sb.omh(&["s01", "diff", "-p"]);
let printed = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "an unchanged session is not an error");
assert!(
printed.contains("no changes"),
"the reader is told which comparison came up empty: {printed:?}"
);
}
#[test]
fn a_worktree_that_left_its_branch_is_refused_whichever_way_you_ask() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("feature.rs"), "fn added() {}\n").unwrap();
let healthy: Vec<bool> = [vec!["s01", "diff"], vec!["s01", "diff", "-p"]]
.iter()
.map(|args| sb.omh(args).status.success())
.collect();
assert_eq!(healthy, vec![true, true], "both work on a healthy session");
let head = Command::new("git")
.arg("-C")
.arg(&worktree)
.args(["rev-parse", "HEAD"])
.output()
.unwrap();
let head = String::from_utf8_lossy(&head.stdout).trim().to_string();
let out = Command::new("git")
.arg("-C")
.arg(&worktree)
.args(["checkout", "-q", "--detach", &head])
.output()
.unwrap();
assert!(out.status.success(), "detaching the worktree: {out:?}");
for args in [vec!["s01", "diff"], vec!["s01", "diff", "-p"]] {
let out = sb.omh(&args);
let said = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"`omh {}` handed over a review from a worktree that left its branch",
args.join(" ")
);
assert!(
said.contains("omh/s01"),
"and the refusal names the branch: {said}"
);
}
}
#[test]
fn a_base_given_with_a_checkpoint_is_refused() {
let sb = sandbox();
sb.session("s01");
let out = sb.omh(&["s01", "diff", "4", "--base", "main"]);
let said = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && said.contains("--base"),
"the flag cannot be honoured here and is not silently dropped: {said}"
);
}
#[test]
fn a_checkpoint_asked_for_before_the_sandbox_ran_says_so_plainly() {
let sb = sandbox();
sb.session("s01");
let out = sb.omh(&["s01", "diff", "1"]);
let said = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "there is no checkpoint 1: {said}");
assert!(
said.contains("has not committed anything"),
"and it says so in the words `log` uses: {said}"
);
assert!(
!said.contains("seed"),
"rather than naming a record the user has never heard of: {said}"
);
}
#[test]
fn the_session_named_first_is_the_one_the_command_acts_on() {
let sb = sandbox();
let one = sb.session("s01");
let two = sb.session("s02");
std::fs::write(one.join("only-in-s01.rs"), "fn main() {}").unwrap();
std::fs::write(two.join("only-in-s02.rs"), "fn main() {}").unwrap();
for (named, mine, theirs) in [
("s01", "only-in-s01.rs", "only-in-s02.rs"),
("s02", "only-in-s02.rs", "only-in-s01.rs"),
] {
let out = sb.omh(&[named, "diff"]);
let printed = String::from_utf8_lossy(&out.stdout);
assert!(
printed.contains(mine) && !printed.contains(theirs),
"`omh {named} diff` has to report {named}: {printed}{}",
String::from_utf8_lossy(&out.stderr)
);
}
}
#[test]
fn the_spellings_the_prefix_replaced_are_refused() {
let sb = sandbox();
sb.session("s01");
for line in [
vec!["s", "diff", "s01"],
vec!["s", "rm", "s01"],
vec!["s", "down", "s01"],
vec!["graph", "s01"],
] {
let out = sb.omh(&line);
let said = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && said.contains("'s01'"),
"`omh {}` names the session where it no longer goes: {said}",
line.join(" ")
);
}
}
#[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(&["s01", "diff"]).stdout).to_string();
assert!(printed.contains("feature.rs"), "got: {printed}");
}
#[test]
fn a_commit_over_conflict_markers_is_refused_by_the_command_itself() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(
worktree.join("tap.rs"),
"<<<<<<< main\nfn ours() {}\n=======\nfn theirs() {}\n>>>>>>> s01\n",
)
.unwrap();
let out = sb.omh(&["s01", "commit", "-m", "Add the tap"]);
assert!(!out.status.success(), "a half-resolved merge does not land");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("tap.rs:1"), "and it says where: {err}");
let out = sb.omh(&["s01", "commit", "-m", "Add the tap", "--force"]);
assert!(
out.status.success(),
"and the user can still mean it: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn keeping_a_sessions_own_commits_says_so_when_there_are_none() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("feature.rs"), "fn main() {}").unwrap();
let out = sb.omh(&["s", "commit", "--keep"]);
let said = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
said.contains("no sandbox repository") || said.contains("nothing to keep"),
"a session with no sandbox repository has nothing to keep, and has to \
say which: {said}"
);
assert!(
!said.contains("committed to"),
"and must not report a commit it did not make: {said}"
);
}
#[test]
fn a_message_and_keeping_the_agents_commits_are_refused_together() {
let sb = sandbox();
sb.session("s01");
let out = sb.omh(&["s", "commit", "-m", "squashed", "--keep"]);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("cannot be used with"), "got: {err}");
}
#[test]
fn diff_reports_a_sessions_work_before_anyone_commits_it() {
let sb = sandbox();
let worktree = sb.session("s01");
std::fs::write(worktree.join("feature.rs"), "fn main() {}").unwrap();
let out = sb.omh(&["s01", "diff"]);
assert!(
out.status.success(),
"diff failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let printed = String::from_utf8_lossy(&out.stdout);
assert!(
printed.contains("feature.rs"),
"uncommitted work is the only work there is yet: {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"]).stdout).to_string();
assert!(printed.contains("to push"), "got: {printed}");
}
#[test]
fn the_listing_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"]).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"]).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.stderr).contains("COMMITTED"),
"writing the committed file has to say so, where a redirect cannot hide it: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!String::from_utf8_lossy(&out.stdout).contains("COMMITTED"),
"and it is a diagnostic, so it does not land in the answer"
);
}
#[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(&["s01", "rm"]);
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 removing_a_session_that_committed_keeps_the_branch_for_review() {
let sb = sandbox();
let _log = sb.fake_docker();
let worktree = sb.session("s01");
std::fs::write(worktree.join("work.txt"), "agent output").unwrap();
for args in [vec!["add", "-A"], vec!["commit", "-q", "-m", "agent work"]] {
let out = Command::new("git")
.arg("-C")
.arg(&worktree)
.args(&args)
.output()
.expect("git must be installed to run this test");
assert!(out.status.success(), "git {args:?}: {out:?}");
}
let out = sb.omh(&["s01", "rm", "--json"]);
assert!(
out.status.success(),
"rm failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let alive = Command::new("git")
.arg("-C")
.arg(&sb.repo)
.args(["rev-parse", "--verify", "omh/s01"])
.output()
.unwrap();
assert!(
alive.status.success(),
"unreviewed work must outlive the session that made it"
);
let doc: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("`--json` is one document");
assert_eq!(doc["branch_kept"], serde_json::json!(true));
assert_eq!(
doc["commits"],
serde_json::json!(1),
"and the count reported is the one that decided it"
);
}
#[test]
fn a_session_named_on_its_own_is_that_session_alone() {
let sb = sandbox();
let _log = sb.fake_docker();
sb.session("s01");
sb.session("s02");
let focused = sb.omh(&["s01"]);
let printed = String::from_utf8_lossy(&focused.stdout);
assert!(
focused.status.success(),
"a session named on its own is a question, not an error: {}",
String::from_utf8_lossy(&focused.stderr)
);
assert!(printed.contains("s01"), "it is about s01: {printed}");
assert!(
!printed.contains("s02"),
"and only about s01 — a focus that quietly widens is the thing the \
selector exists to remove: {printed}"
);
let all = String::from_utf8_lossy(&sb.omh(&["s"]).stdout).to_string();
assert!(
all.contains("s01") && all.contains("s02"),
"`omh s` is still every session: {all}"
);
let three = sb.session("s03");
let four = sb.session("s04");
for worktree in [&three, &four] {
std::fs::write(worktree.join("elsewhere.rs"), "fn elsewhere() {}\n").unwrap();
}
let focused = String::from_utf8_lossy(&sb.omh(&["s01"]).stdout).to_string();
assert!(
!focused.contains("elsewhere.rs"),
"a collision between two other sessions is not s01's business: {focused}"
);
let missing = sb.omh(&["s99"]);
let err = String::from_utf8_lossy(&missing.stderr).to_string();
assert!(!missing.status.success(), "an unknown session is an error");
assert!(
err.contains("s99") && err.contains("omh s"),
"the refusal names the id and where the real ones are listed: {err}"
);
}
#[test]
fn the_focused_listing_is_one_session_in_the_document_too() {
let sb = sandbox();
let _log = sb.fake_docker();
sb.session("s01");
sb.session("s02");
let out = sb.omh(&["s01", "--json"]);
let doc: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("`omh s01 --json` is a document");
let sessions = doc["sessions"].as_array().expect("sessions is an array");
assert_eq!(sessions.len(), 1, "one session was asked for: {doc}");
assert_eq!(sessions[0]["id"], "s01", "and it is the one named: {doc}");
}
#[test]
fn the_retired_listing_verb_is_refused_by_name_rather_than_widening() {
let sb = sandbox();
let _log = sb.fake_docker();
sb.session("s01");
sb.session("s02");
let scoped = sb.omh(&["s01", "ls"]);
let out = String::from_utf8_lossy(&scoped.stdout).to_string();
let err = String::from_utf8_lossy(&scoped.stderr).to_string();
assert!(
!scoped.status.success(),
"a retired verb is refused, not answered: {out}"
);
assert!(
!out.contains("s02"),
"and refusing means it never listed every session on the way: {out}"
);
assert!(
err.contains("is the listing"),
"the refusal names what replaced the verb: {err}"
);
let bare = sb.omh(&["s", "ls"]); let err = String::from_utf8_lossy(&bare.stderr).to_string();
assert!(!bare.status.success(), "the retired verb is not a command");
assert!(
err.contains("is the listing"),
"a verb retired in favour of its own noun is one word away from what \
the user meant, so the error says the word rather than leaving them \
to read a usage line: {err}"
);
}
#[test]
fn a_session_that_vanishes_between_the_check_and_the_read_is_not_no_sessions() {
let sb = sandbox();
let _log = sb.fake_docker();
let worktree = sb.session("s01");
let stray = worktree.parent().unwrap().join("s02");
std::fs::write(&stray, "").unwrap();
let out = sb.omh(&["s02"]);
let printed = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
!printed.contains("no sessions"),
"omh looked, disagreed with itself, and reported an empty world: {printed}"
);
assert!(
!out.status.success(),
"and it exits non-zero, so a script cannot read the disagreement as \
an answer: {printed}"
);
let all = String::from_utf8_lossy(&sb.omh(&["s"]).stdout).to_string();
assert!(all.contains("s01"), "`omh s` still answers: {all}");
}
#[test]
fn the_long_spelling_of_the_selector_scopes_and_checks_the_same() {
let sb = sandbox();
let _log = sb.fake_docker();
sb.session("s01");
sb.session("s02");
let long = String::from_utf8_lossy(&sb.omh(&["s", "--session", "s01"]).stdout).to_string();
let prefix = String::from_utf8_lossy(&sb.omh(&["s01"]).stdout).to_string();
assert_eq!(
long, prefix,
"`omh s --session s01` and `omh s01` are one command spelled two ways"
);
let traversal = sb.omh(&["s", "--session", "../../etc"]);
let err = String::from_utf8_lossy(&traversal.stderr).to_string();
assert!(
!traversal.status.success(),
"a selector that is not an id is refused: {}",
String::from_utf8_lossy(&traversal.stdout)
);
assert!(
err.contains("not a path"),
"the refusal is about the shape of the name, not about what it \
happens to point at: {err}"
);
}
#[test]
fn a_launch_that_cannot_read_the_probe_removes_nothing() {
let sb = sandbox();
let log = sb.fake_docker();
sb.seed_catalogue(&["adapters", "base", "editors", "stacks"]);
sb.session("s01");
std::fs::write(sb.bin.join("containers"), "omh-repo-s01\n").unwrap();
std::fs::write(
sb.bin.join("docker-exec-refuses"),
"failed to connect to the docker API at unix:///var/run/docker.sock; check if \
the path is correct and if the daemon is running\n",
)
.unwrap();
let out = sb.omh(&["s01", "claude"]);
assert!(
!out.status.success(),
"the launch stops rather than guessing"
);
let said = String::from_utf8_lossy(&out.stderr);
assert!(
said.contains("could not tell whether s01's sandbox is still usable"),
"and says so: {said}"
);
assert!(
said.contains("omh s01 down"),
"with a way on that does not need the container entered: {said}"
);
let asked = std::fs::read_to_string(&log).unwrap_or_default();
assert!(
!asked.lines().any(|line| line.starts_with("rm -f")),
"and nothing was removed — this is the half that costs work: {asked}"
);
}
#[test]
fn a_runtime_that_cannot_be_reached_is_not_reported_as_a_stopped_sandbox() {
let sb = sandbox();
let _log = sb.fake_docker();
sb.session("s01");
std::fs::write(sb.bin.join("docker-refuses"), "").unwrap();
let out = sb.omh(&["s"]);
let printed = String::from_utf8_lossy(&out.stdout);
assert!(printed.contains("s01"), "the session is listed: {printed}");
assert!(
!printed.contains("stopped"),
"a question omh could not answer is not an answer: {printed}"
);
assert!(
printed.contains("up?"),
"it is rendered as the question it is: {printed}"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("could not tell whether s01's sandbox is running"),
"and the reason reaches stderr: {err}"
);
let json = sb.omh(&["s", "--json"]);
let doc: serde_json::Value =
serde_json::from_slice(&json.stdout).expect("`omh s --json` is a document");
assert_eq!(
doc["sessions"].as_array().map(Vec::len),
Some(1),
"one session in the document: {doc}"
);
assert_eq!(
doc["sessions"][0]["id"],
serde_json::json!("s01"),
"and it is s01: {doc}"
);
assert_eq!(
doc["sessions"][0]["running"],
serde_json::Value::Null,
"and a script is not told `false`: {doc}"
);
assert!(
doc["sessions"][0]["running_unknown"].is_string(),
"with the reason beside it, since `--json` never sees the warning: {doc}"
);
let sync = sb.omh(&["s01", "sync"]);
assert!(!sync.status.success(), "sync does not proceed on a guess");
let err = String::from_utf8_lossy(&sync.stderr);
assert!(
err.contains("could not tell whether s01 is running"),
"and says why it stopped: {err}"
);
}
#[test]
fn down_over_an_unreachable_runtime_still_names_the_session() {
let sb = sandbox();
let _log = sb.fake_docker();
sb.session("s01");
std::fs::write(sb.bin.join("docker-refuses"), "").unwrap();
let out = sb.omh(&["s01", "down", "--json"]);
assert!(!out.status.success(), "it is a failure, and exits like one");
let doc: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("down --json is a document");
assert_eq!(
doc["sessions"].as_array().map(Vec::len),
Some(1),
"the session is in the document: {doc}"
);
assert_eq!(doc["sessions"][0]["session"], serde_json::json!("s01"));
assert_eq!(
doc["sessions"][0]["stopped"],
serde_json::Value::Null,
"`null`, not `false` — omh never asked it to stop: {doc}"
);
assert!(
doc["sessions"][0]["why"].is_string(),
"with the runtime's reason beside it: {doc}"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("could not be asked") && !err.contains("would not stop"),
"and it does not claim the container refused to stop: {err}"
);
}
#[test]
fn the_listing_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"]);
let printed = String::from_utf8_lossy(&out.stderr);
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}"
);
}
#[test]
fn a_focused_listing_leaves_other_sessions_leftovers_to_the_wide_one() {
let sb = sandbox();
let _log = sb.fake_docker();
sb.session("s01");
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();
let focused = sb.omh(&["s01"]);
let aside = String::from_utf8_lossy(&focused.stderr).to_string();
assert!(
!aside.contains("s02") && !aside.contains("s03"),
"asked about s01, told about s02 and s03: {aside}"
);
let wide = String::from_utf8_lossy(&sb.omh(&["s"]).stderr).to_string();
assert!(
wide.contains("s02") && wide.contains("s03"),
"`omh s` is still where leftovers are named: {wide}"
);
}
#[test]
fn a_redirected_listing_collects_the_sessions_and_nothing_else() {
let sb = sandbox();
let _log = sb.fake_docker();
std::fs::write(sb.bin.join("containers"), "omh-repo-s03\n").unwrap();
let out = sb.omh(&["s"]);
let answer = String::from_utf8_lossy(&out.stdout);
let aside = String::from_utf8_lossy(&out.stderr);
assert!(
!answer.contains("rm"),
"a next step is not part of a redirected answer — got {answer:?}"
);
assert!(
!answer.contains("left something behind"),
"nor is a warning — got {answer:?}"
);
assert!(
aside.contains("left something behind") && aside.contains("rm"),
"both still reach the person watching — got {aside:?}"
);
}
#[test]
fn json_carries_leftovers_as_a_field_and_says_nothing_about_them() {
let sb = sandbox();
let _log = sb.fake_docker();
std::fs::write(sb.bin.join("containers"), "omh-repo-s03\n").unwrap();
let out = sb.omh(&["s", "--json"]);
let doc: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("`--json` is one document");
assert_eq!(
doc["leftovers"],
serde_json::json!(["s03"]),
"the fact is a field"
);
assert_eq!(
String::from_utf8_lossy(&out.stderr),
"",
"and the prose about it is gone"
);
}
impl Sandbox {
fn seed_adapters(&self) {
self.seed_catalogue(&["adapters"]);
}
fn seed_catalogue(&self, kinds: &[&str]) {
for kind in kinds {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join(kind);
let dst = self.home.join(".omh").join(kind);
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()
);
}
#[test]
fn every_json_answer_is_one_document_and_not_several() {
let sb = sandbox();
sb.seed_base();
sb.catalogue(&["skills/alpha/SKILL.md", "skills/beta/SKILL.md"]);
std::fs::create_dir_all(sb.repo.join(".omh")).unwrap();
std::fs::write(
sb.repo.join(".omh/settings.toml"),
"[use]\nskills = [\"alpha\"]\n\n[omh]\ncodegraph = true\n",
)
.unwrap();
std::fs::write(
sb.repo.join(".omh/settings.local.toml"),
"[use]\nskills = [\"alpha\"]\n\n[omh]\ncodegraph = true\n",
)
.unwrap();
for args in [
vec!["--json", "ls"],
vec!["--json", "repo"],
vec!["--json", "config"],
vec!["--json", "use", "skills", "beta"],
vec!["--json", "unuse", "skills", "beta"],
vec!["--json", "repo", "disable", "codegraph"],
vec!["--json", "s"],
vec!["--json", "memory", "ls"],
] {
let out = sb.omh(&args);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
if stdout.trim().is_empty() {
continue; }
let documents = stdout.lines().filter(|l| *l == "}").count();
assert_eq!(
documents,
1,
"`omh {}` emitted {documents} JSON documents, and a parser reads one:\n{stdout}",
args.join(" ")
);
}
}
#[test]
fn a_build_asks_docker_to_remove_the_tags_it_replaced() {
let sb = sandbox();
sb.git_init();
let log = sb.fake_docker_with_nothing_built(
&["omh/base:stale", "omh/base:latest", "omh/base:held"],
&["omh/base:held"],
);
let init = sb.omh(&["init"]);
assert!(
init.status.success(),
"init failed ({}), so no build ran and no reap followed it\n\
--- stderr ---\n{}\n--- stdout ---\n{}\n--- docker was asked ---\n{}",
init.status,
String::from_utf8_lossy(&init.stderr),
String::from_utf8_lossy(&init.stdout),
sb.docker_calls(&log).join("\n")
);
let removals: Vec<String> = sb
.docker_calls(&log)
.into_iter()
.filter(|c| c.starts_with("image rm "))
.collect();
assert!(
removals.iter().any(|c| c.ends_with("omh/base:stale")),
"the build never asked for the tag it replaced: {removals:?}"
);
assert!(
!removals.iter().any(|c| c.ends_with("omh/base:latest")),
"`latest` is the one removal that cannot be undone: {removals:?}"
);
assert!(
!removals.iter().any(|c| c.ends_with("omh/base:held")),
"a container still references it: {removals:?}"
);
}