truth-mirror 0.13.3

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,
        r#"#!/usr/bin/env python3
import json
print(json.dumps({
    "verdict": "REJECT",
    "summary": "The claim is not substantiated.",
    "findings": [{
        "severity": "high",
        "title": "claim not substantiated",
        "body": "The cited evidence does not prove the claim.",
        "file": "file.txt",
        "line_start": 1,
        "line_end": 1,
        "confidence": 95,
        "recommendation": "Add evidence that directly proves the claim."
    }],
    "next_steps": ["Re-run the verification command."],
    "memory_skill": {
        "kind": "anti_pattern_skill",
        "learning_source": "claim not substantiated",
        "reasoning": "The rejection identifies a reusable unsupported-claim pattern."
    }
}))
"#,
    )
    .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"));
}

#[cfg(unix)]
#[test]
fn watch_until_empty_exits_without_processing_when_lock_is_foreign_held() {
    // 0.9.2 field failure: a second watcher proceeded "without lock
    // ownership", double-processed a petition, and crashed the first watcher.
    // A looping watch that cannot take the lock must exit with a clear
    // message and NEVER touch the queue.
    let temp = tempfile::tempdir().unwrap();
    let repo = temp.path().join("repo");
    fs::create_dir(&repo).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-lock-e2e",
        ],
    );
    let sha = git_stdout(&repo, &["rev-parse", "HEAD"]);

    // A queued commit the watcher would drain if it (wrongly) proceeded.
    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();

    // A live foreign process holds the watcher lock (empty start token:
    // liveness degrades to pid-alive, which the sleeper satisfies).
    let mut sleeper = std::process::Command::new("sleep")
        .arg("30")
        .spawn()
        .unwrap();
    fs::write(
        state.join("watcher.lock"),
        format!(
            "{{\"identity\":{{\"pid\":{},\"start_token\":\"\"}},\"created_at_unix\":1}}",
            sleeper.id()
        ),
    )
    .unwrap();

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(&repo)
        .args([
            "--state-dir",
            ".truth-mirror",
            "watch",
            "--until-empty",
            "--grace",
            "1",
            "--poll-secs",
            "1",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("watcher lock"));

    let _ = sleeper.kill();
    let _ = sleeper.wait();

    // The queue item was NOT consumed and no run record was created.
    let queue = fs::read_to_string(state.join("review-queue.jsonl")).unwrap();
    assert!(
        queue.contains(&sha),
        "the refused watcher must not process queue items, got: {queue:?}"
    );
    assert!(
        !state.join("runs").exists(),
        "the refused watcher must not create run records"
    );
    // The incumbent's lock was left intact.
    assert!(state.join("watcher.lock").exists());
}

#[cfg(unix)]
#[test]
fn watch_once_refuses_foreign_held_lock_without_processing() {
    // Round-2 contract: --once races concurrent drains exactly like a loop
    // does (same queue items, same run rows, same queue file rewrite), so it
    // takes the same single-flight lock — and fails loudly without it instead
    // of draining.
    let temp = tempfile::tempdir().unwrap();
    let repo = temp.path().join("repo");
    fs::create_dir(&repo).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-once-lock-e2e",
        ],
    );
    let sha = git_stdout(&repo, &["rev-parse", "HEAD"]);

    // A queued commit the drain would process if it (wrongly) proceeded.
    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();

    // A live foreign process holds the watcher lock (empty start token:
    // liveness degrades to pid-alive, which the sleeper satisfies).
    let mut sleeper = std::process::Command::new("sleep")
        .arg("30")
        .spawn()
        .unwrap();
    fs::write(
        state.join("watcher.lock"),
        format!(
            "{{\"identity\":{{\"pid\":{},\"start_token\":\"\"}},\"created_at_unix\":1}}",
            sleeper.id()
        ),
    )
    .unwrap();

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(&repo)
        .args(["--state-dir", ".truth-mirror", "watch", "--once"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("watcher lock"));

    let _ = sleeper.kill();
    let _ = sleeper.wait();

    // The queue item was NOT consumed and no run record was created.
    let queue = fs::read_to_string(state.join("review-queue.jsonl")).unwrap();
    assert!(
        queue.contains(&sha),
        "the refused --once drain must not process queue items, got: {queue:?}"
    );
    assert!(
        !state.join("runs").exists(),
        "the refused --once drain must not create run records"
    );
    // The incumbent's lock was left intact.
    assert!(state.join("watcher.lock").exists());
}