truth-mirror 0.2.0

Truthfulness gate and adversarial reviewer harness for AI coding agents.
Documentation
use std::{fs, path::Path};

use assert_cmd::Command;
use predicates::prelude::*;

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");
}

fn git_stdout(repo: &Path, args: &[&str]) -> String {
    let output = std::process::Command::new("git")
        .args(args)
        .current_dir(repo)
        .output()
        .unwrap();
    assert!(output.status.success(), "git {args:?} failed");
    String::from_utf8(output.stdout).unwrap().trim().to_owned()
}

#[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) {}

#[test]
fn watch_once_drains_queued_commit_through_mocked_reviewer() {
    let temp = tempfile::tempdir().unwrap();
    let repo = temp.path().join("repo");
    let bin = temp.path().join("bin");
    fs::create_dir(&repo).unwrap();
    fs::create_dir(&bin).unwrap();

    git(&repo, &["init"]);
    git(&repo, &["config", "user.email", "truth@example.invalid"]);
    git(&repo, &["config", "user.name", "Truth Mirror Test"]);
    fs::write(repo.join("file.txt"), "hello\n").unwrap();
    git(&repo, &["add", "file.txt"]);
    git(
        &repo,
        &[
            "commit",
            "-m",
            "feat: add file",
            "-m",
            "CLAIM: add file | verified: cargo test | evidence: tests:watch-e2e",
        ],
    );
    let sha = git_stdout(&repo, &["rev-parse", "HEAD"]);

    // Simulate what the post-commit hook enqueues.
    let state = repo.join(".truth-mirror");
    fs::create_dir_all(&state).unwrap();
    fs::write(
        state.join("review-queue.jsonl"),
        format!("{{\"commit_sha\":\"{sha}\",\"enqueued_at_unix\":100}}\n"),
    )
    .unwrap();

    let fake_codex = bin.join("codex");
    fs::write(
        &fake_codex,
        "#!/usr/bin/env python3\nprint('VERDICT: REJECT')\nprint('FINDINGS:')\nprint('- claim not substantiated')\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",
            "watch",
            "--once",
            "--watched-agent",
            "claude",
            "--watched-model",
            "model-a",
            "--reviewer-harness",
            "codex",
            "--reviewer-model",
            "model-b",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("reviewed 1 commit"));

    let ledger = fs::read_to_string(state.join("ledger.jsonl")).unwrap();
    assert!(ledger.contains("\"verdict\":\"REJECT\""));
    assert!(ledger.contains(&sha));

    // Queue is compacted after draining.
    let queue = state.join("review-queue.jsonl");
    let remaining = fs::read_to_string(&queue).unwrap_or_default();
    assert!(
        remaining.trim().is_empty(),
        "queue should be empty after drain, got: {remaining}"
    );

    // Pre-push gate must now block on the unresolved rejection.
    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(&repo)
        .args(["--state-dir", ".truth-mirror", "ledger", "stats"])
        .assert()
        .success()
        .stdout(predicate::str::contains("reject=1"))
        .stdout(predicate::str::contains("unresolved=1"));
}

#[test]
fn watch_once_is_quiet_with_empty_queue() {
    let temp = tempfile::tempdir().unwrap();
    let repo = temp.path().join("repo");
    fs::create_dir(&repo).unwrap();
    git(&repo, &["init"]);

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(&repo)
        .args(["--state-dir", ".truth-mirror", "watch", "--once"])
        .assert()
        .success()
        .stdout(predicate::str::contains("reviewed 0 commit"));
}