mod common;
use std::{fs, path::Path};
use common::truth_mirror_bin;
use predicates::prelude::*;
fn write(path: &Path, contents: &str) {
fs::write(path, contents).unwrap();
}
#[test]
fn commit_msg_gate_accepts_evidenced_claim() {
let temp = tempfile::tempdir().unwrap();
let commit_msg = temp.path().join("COMMIT_EDITMSG");
write(
&commit_msg,
"feat: claim parser\n\nCLAIM: add parser | verified: cargo test | evidence: tests:cargo-test\n",
);
truth_mirror_bin("truth-mirror")
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.assert()
.success();
}
fn valid_commit_message(temp: &tempfile::TempDir) -> std::path::PathBuf {
let commit_msg = temp.path().join("COMMIT_EDITMSG");
write(
&commit_msg,
"feat: hermetic config loading\n\nCLAIM: load config | verified: cargo test | evidence: tests:gate-cli\n",
);
commit_msg
}
#[test]
fn commit_msg_gate_loads_without_home_or_xdg_config() {
let temp = tempfile::tempdir().unwrap();
let commit_msg = valid_commit_message(&temp);
truth_mirror_bin("truth-mirror")
.env_remove("XDG_CONFIG_HOME")
.env_remove("HOME")
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.assert()
.success();
}
#[test]
fn commit_msg_gate_does_not_materialize_the_system_config() {
let temp = tempfile::tempdir().unwrap();
let config_home = temp.path().join("xdg-config");
let commit_msg = valid_commit_message(&temp);
fs::create_dir(&config_home).unwrap();
truth_mirror_bin("truth-mirror")
.env("XDG_CONFIG_HOME", &config_home)
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.assert()
.success();
assert!(!config_home.join("truth-mirror/config.toml").exists());
}
#[cfg(unix)]
#[test]
fn commit_msg_gate_ignores_an_unwritable_system_config_directory() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().unwrap();
let config_home = temp.path().join("read-only-xdg-config");
let commit_msg = valid_commit_message(&temp);
fs::create_dir(&config_home).unwrap();
fs::set_permissions(&config_home, fs::Permissions::from_mode(0o500)).unwrap();
let output = truth_mirror_bin("truth-mirror")
.env("XDG_CONFIG_HOME", &config_home)
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.output()
.unwrap();
fs::set_permissions(&config_home, fs::Permissions::from_mode(0o700)).unwrap();
assert!(
output.status.success(),
"gate should ignore an unavailable system config directory:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn commit_msg_gate_rejects_a_malformed_existing_system_config() {
let temp = tempfile::tempdir().unwrap();
let config_home = temp.path().join("xdg-config");
let commit_msg = valid_commit_message(&temp);
let system_dir = config_home.join("truth-mirror");
fs::create_dir_all(&system_dir).unwrap();
write(&system_dir.join("config.toml"), "enabled = [\n");
truth_mirror_bin("truth-mirror")
.env("XDG_CONFIG_HOME", &config_home)
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("failed to parse config"));
}
#[test]
fn disabled_config_still_runs_commit_gate() {
let temp = tempfile::tempdir().unwrap();
let state = temp.path().join("state");
let commit_msg = temp.path().join("COMMIT_EDITMSG");
fs::create_dir_all(&state).unwrap();
write(&state.join("config.toml"), "enabled = false\n");
write(
&commit_msg,
"feat: claim parser\n\nCLAIM: add parser | verified: cargo test | evidence: tests:cargo-test\n",
);
truth_mirror_bin("truth-mirror")
.args([
"--state-dir",
state.to_str().unwrap(),
"gate",
"--commit-msg",
commit_msg.to_str().unwrap(),
])
.assert()
.success()
.stdout(predicate::str::is_empty())
.stderr(predicate::str::is_empty());
}
#[test]
fn commit_msg_gate_accepts_duplicate_empty_verification_aliases_in_any_order() {
let temp = tempfile::tempdir().unwrap();
let first = temp.path().join("FIRST_MSG");
let second = temp.path().join("SECOND_MSG");
write(
&first,
"feat: claim parser\n\nCLAIM: fix parser | verified: cargo test | verification: | evidence: tests:x\n",
);
write(
&second,
"feat: claim parser\n\nCLAIM: fix parser | verification: | verified: cargo test | evidence: tests:x\n",
);
for commit_msg in [first, second] {
truth_mirror_bin("truth-mirror")
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.assert()
.success();
}
}
#[test]
fn commit_msg_gate_rejects_missing_claim() {
let temp = tempfile::tempdir().unwrap();
let commit_msg = temp.path().join("COMMIT_EDITMSG");
write(&commit_msg, "feat: claim parser\n");
truth_mirror_bin("truth-mirror")
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("missing CLAIM"));
}
#[test]
fn commit_msg_gate_rejects_claim_with_missing_evidence_directly() {
let temp = tempfile::tempdir().unwrap();
let commit_msg = temp.path().join("COMMIT_EDITMSG");
write(
&commit_msg,
"feat: done parser\n\nCLAIM: complete parser | verified: cargo test\n",
);
truth_mirror_bin("truth-mirror")
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("missing evidence pointer"));
}
#[test]
fn commit_msg_gate_rejects_completion_without_any_claim() {
let temp = tempfile::tempdir().unwrap();
let commit_msg = temp.path().join("COMMIT_EDITMSG");
write(&commit_msg, "feat: done — this is complete\n");
truth_mirror_bin("truth-mirror")
.args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains(
"completion wording lacks evidence",
))
.stderr(predicate::str::contains("example: CLAIM: change behavior"));
}
#[test]
fn pre_push_warns_about_queued_unreviewed_claims_without_blocking() {
let temp = tempfile::tempdir().unwrap();
let state = temp.path().join("state");
fs::create_dir(&state).unwrap();
fs::write(
state.join("review-queue.jsonl"),
"{\"commit_sha\":\"abc123\",\"enqueued_at_unix\":1}\n",
)
.unwrap();
truth_mirror_bin("truth-mirror")
.args([
"--state-dir",
state.to_str().unwrap(),
"gate",
"--pre-push",
"abc123",
])
.assert()
.success()
.stderr(predicate::str::contains("1 unreviewed claim(s) queued"))
.stderr(predicate::str::contains("truth-mirror --state-dir"))
.stderr(predicate::str::contains(state.to_string_lossy().as_ref()))
.stderr(predicate::str::contains("watch --once"));
}
#[test]
fn truth_alias_pre_push_warning_uses_alias_prefix_and_drain_command() {
let temp = tempfile::tempdir().unwrap();
let state = temp.path().join("state");
fs::create_dir(&state).unwrap();
fs::write(
state.join("review-queue.jsonl"),
"{\"commit_sha\":\"abc123\",\"enqueued_at_unix\":1}\n",
)
.unwrap();
truth_mirror_bin("truth")
.args([
"--state-dir",
state.to_str().unwrap(),
"gate",
"--pre-push",
"abc123",
])
.assert()
.success()
.stderr(predicate::str::contains("truth: warning"))
.stderr(predicate::str::contains("truth --state-dir"))
.stderr(predicate::str::contains("truth-mirror: warning").not());
}
#[test]
fn pre_push_warning_preserves_config_in_drain_guidance() {
let temp = tempfile::tempdir().unwrap();
let state = temp.path().join("state");
let config = temp.path().join("truth.toml");
fs::create_dir(&state).unwrap();
fs::write(&config, "").unwrap();
fs::write(
state.join("review-queue.jsonl"),
"{\"commit_sha\":\"abc123\",\"enqueued_at_unix\":1}\n",
)
.unwrap();
truth_mirror_bin("truth-mirror")
.args([
"--config",
config.to_str().unwrap(),
"--state-dir",
state.to_str().unwrap(),
"gate",
"--pre-push",
"abc123",
])
.assert()
.success()
.stderr(predicate::str::contains("truth-mirror --config"))
.stderr(predicate::str::contains(config.to_string_lossy().as_ref()))
.stderr(predicate::str::contains("watch --once"));
}
#[test]
fn pre_push_queue_warning_parse_error_does_not_block_push() {
let temp = tempfile::tempdir().unwrap();
let state = temp.path().join("state");
fs::create_dir(&state).unwrap();
fs::write(state.join("review-queue.jsonl"), "not json\n").unwrap();
truth_mirror_bin("truth-mirror")
.args([
"--state-dir",
state.to_str().unwrap(),
"gate",
"--pre-push",
"abc123",
])
.assert()
.success()
.stderr(predicate::str::contains(
"could not read async review queue",
));
}
#[test]
fn commit_msg_gate_rejects_fake_marker_in_diff_file() {
let temp = tempfile::tempdir().unwrap();
let commit_msg = temp.path().join("COMMIT_EDITMSG");
let diff = temp.path().join("staged.diff");
write(
&commit_msg,
"feat: claim parser\n\nCLAIM: add parser | verified: cargo test | evidence: tests:cargo-test\n",
);
write(
&diff,
"diff --git a/src/lib.rs b/src/lib.rs\n+ TODO-as-done\n",
);
truth_mirror_bin("truth-mirror")
.args([
"gate",
"--commit-msg",
commit_msg.to_str().unwrap(),
"--diff-file",
diff.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains("TODO-as-done"));
}