use assert_cmd::Command;
use predicates::prelude::*;
#[test]
fn install_hooks_dry_run_prints_plan_without_writing_hooks() {
let temp = tempfile::tempdir().unwrap();
std::process::Command::new("git")
.arg("init")
.current_dir(temp.path())
.status()
.unwrap();
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(temp.path())
.args(["install-hooks", "--dry-run", "--claude", "--codex", "--pi"])
.assert()
.success()
.stdout(predicate::str::contains("truth-mirror hook plan"))
.stdout(predicate::str::contains("commit-msg"))
.stdout(predicate::str::contains("pre-push"))
.stdout(predicate::str::contains(
"surface: claude -> .claude/settings.json",
))
.stdout(predicate::str::contains(
"surface: codex -> .codex/hooks.json",
))
.stdout(predicate::str::contains(
"surface: pi -> .pi/extensions/truth-mirror.js",
));
assert!(!temp.path().join(".truth-mirror/hooks/commit-msg").exists());
assert!(!temp.path().join(".claude/settings.json").exists());
assert!(!temp.path().join(".codex/hooks.json").exists());
assert!(!temp.path().join(".pi/extensions/truth-mirror.js").exists());
}
fn git_init(dir: &std::path::Path) {
std::process::Command::new("git")
.arg("init")
.current_dir(dir)
.status()
.unwrap();
}
#[test]
fn install_hooks_writes_selected_agent_surfaces() {
let temp = tempfile::tempdir().unwrap();
git_init(temp.path());
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(temp.path())
.args(["install-hooks", "--claude", "--codex", "--pi"])
.assert()
.success();
let claude = std::fs::read_to_string(temp.path().join(".claude/settings.json")).unwrap();
assert!(claude.contains("UserPromptSubmit"));
assert!(claude.contains("truth-mirror reinject --agent claude"));
let codex: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(temp.path().join(".codex/hooks.json")).unwrap(),
)
.unwrap();
assert_eq!(
codex
.pointer("/hooks/UserPromptSubmit/0/hooks/0/command")
.and_then(serde_json::Value::as_str),
Some("truth-mirror reinject --agent codex")
);
assert_eq!(
codex
.pointer("/hooks/UserPromptSubmit/0/hooks/0/type")
.and_then(serde_json::Value::as_str),
Some("command")
);
assert!(!temp.path().join(".pi/hooks.json").exists());
let pi = std::fs::read_to_string(temp.path().join(".pi/extensions/truth-mirror.js")).unwrap();
assert!(pi.contains("truth-mirror"));
assert!(pi.contains("reinject"));
assert!(pi.contains("pi.on(\"context\""));
}
#[test]
fn install_hooks_only_touches_selected_agents() {
let temp = tempfile::tempdir().unwrap();
git_init(temp.path());
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(temp.path())
.args(["install-hooks", "--claude"])
.assert()
.success();
assert!(temp.path().join(".claude/settings.json").exists());
assert!(!temp.path().join(".codex/hooks.json").exists());
assert!(!temp.path().join(".pi/hooks.json").exists());
}
#[test]
fn install_is_non_clobbering_and_uninstall_reverses_only_our_entries() {
let temp = tempfile::tempdir().unwrap();
git_init(temp.path());
std::fs::create_dir_all(temp.path().join(".claude")).unwrap();
std::fs::write(
temp.path().join(".claude/settings.json"),
r#"{"model":"sonnet","hooks":{"PreToolUse":[{"matcher":"Bash"}]}}"#,
)
.unwrap();
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(temp.path())
.args(["install-hooks", "--claude"])
.assert()
.success();
let after_install = std::fs::read_to_string(temp.path().join(".claude/settings.json")).unwrap();
assert!(after_install.contains("sonnet"));
assert!(after_install.contains("PreToolUse"));
assert!(after_install.contains("truth-mirror reinject --agent claude"));
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(temp.path())
.args(["install-hooks", "--uninstall"])
.assert()
.success();
let after_uninstall =
std::fs::read_to_string(temp.path().join(".claude/settings.json")).unwrap();
assert!(after_uninstall.contains("sonnet"));
assert!(after_uninstall.contains("PreToolUse"));
assert!(!after_uninstall.contains("truth-mirror reinject"));
}