use std::{fs, path::Path};
use assert_cmd::Command;
fn git(repo: &Path, args: &[&str]) {
let status = std::process::Command::new("git")
.args(args)
.current_dir(repo)
.status()
.unwrap();
assert!(status.success(), "git {args:?} failed");
}
#[cfg(unix)]
fn make_executable(path: &Path) {
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).unwrap();
}
#[cfg(not(unix))]
fn make_executable(_path: &Path) {}
fn repo_with_unresolved_rejection() -> (tempfile::TempDir, std::path::PathBuf) {
let temp = tempfile::tempdir().unwrap();
let repo = temp.path().join("repo");
let bin = temp.path().join("bin");
fs::create_dir_all(&repo).unwrap();
fs::create_dir_all(&bin).unwrap();
git(&repo, &["init"]);
git(&repo, &["config", "user.email", "t@t.invalid"]);
git(&repo, &["config", "user.name", "T"]);
fs::write(repo.join("f.txt"), "hi\n").unwrap();
git(&repo, &["add", "f.txt"]);
git(
&repo,
&[
"commit",
"-m",
"feat: add f",
"-m",
"CLAIM: add f | verified: cargo test | evidence: tests:x",
],
);
let sha = {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&repo)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_owned()
};
let fake_codex = bin.join("codex");
fs::write(
&fake_codex,
"#!/usr/bin/env python3\nprint('VERDICT: REJECT')\nprint('FINDINGS:')\nprint('- unsupported')\n",
)
.unwrap();
make_executable(&fake_codex);
let path = format!(
"{}:{}",
bin.display(),
std::env::var("PATH").unwrap_or_default()
);
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(&repo)
.env("PATH", path)
.args([
"--state-dir",
".truth-mirror",
"review",
&sha,
"--watched-agent",
"claude",
"--watched-model",
"model-a",
"--reviewer-harness",
"codex",
"--reviewer-model",
"model-b",
])
.assert()
.success();
(temp, repo)
}
#[test]
fn pre_tool_use_blocks_mutating_tool_when_enforced_and_unresolved() {
let (_temp, repo) = repo_with_unresolved_rejection();
fs::write(
repo.join(".truth-mirror/config.toml"),
"[enforcement]\nblock_tools_after_unresolved = 1\n",
)
.unwrap();
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(&repo)
.args([
"--state-dir",
".truth-mirror",
"gate",
"--pre-tool-use",
"--tool",
"Edit",
])
.assert()
.failure();
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(&repo)
.args([
"--state-dir",
".truth-mirror",
"gate",
"--pre-tool-use",
"--tool",
"Read",
])
.assert()
.success();
}
#[test]
fn pre_tool_use_allows_when_enforcement_disabled_by_default() {
let (_temp, repo) = repo_with_unresolved_rejection();
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(&repo)
.args([
"--state-dir",
".truth-mirror",
"gate",
"--pre-tool-use",
"--tool",
"Edit",
])
.assert()
.success();
}
#[test]
fn pre_tool_use_fails_closed_on_unparseable_hook_payload() {
let (_temp, repo) = repo_with_unresolved_rejection();
fs::write(
repo.join(".truth-mirror/config.toml"),
"[enforcement]\nblock_tools_after_unresolved = 1\n",
)
.unwrap();
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(&repo)
.args(["--state-dir", ".truth-mirror", "gate", "--pre-tool-use"])
.write_stdin("{\"unexpected\":\"schema\"}")
.assert()
.failure();
}
#[test]
fn pre_tool_use_honors_global_config_flag() {
let (_temp, repo) = repo_with_unresolved_rejection();
fs::write(
repo.join("enforce.toml"),
"[enforcement]\nblock_tools_after_unresolved = 1\n",
)
.unwrap();
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(&repo)
.args([
"--state-dir",
".truth-mirror",
"gate",
"--pre-tool-use",
"--tool",
"Edit",
])
.assert()
.success();
Command::cargo_bin("truth-mirror")
.unwrap()
.current_dir(&repo)
.args([
"--state-dir",
".truth-mirror",
"--config",
"enforce.toml",
"gate",
"--pre-tool-use",
"--tool",
"Edit",
])
.assert()
.failure();
}