truth-mirror 0.7.2

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

use assert_cmd::Command;
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",
    );

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
        .assert()
        .success();
}

#[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] {
        Command::cargo_bin("truth-mirror")
            .unwrap()
            .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");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
        .assert()
        .failure()
        .stderr(predicate::str::contains("missing CLAIM"));
}

// Finding 2: when a CLAIM line is present but evidence is missing/invalid, the
// gate must report the underlying ClaimError (naming the exact problem) rather
// than the generic "completion wording lacks evidence" hint.
#[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",
    );

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args(["gate", "--commit-msg", commit_msg.to_str().unwrap()])
        .assert()
        .failure()
        .stderr(predicate::str::contains("missing evidence pointer"));
}

// CompletionWithoutEvidence is still raised when there is NO CLAIM line at all
// and body prose contains a completion word.
#[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");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .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();

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .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();

    Command::cargo_bin("truth")
        .unwrap()
        .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();

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .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();

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .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",
    );

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args([
            "gate",
            "--commit-msg",
            commit_msg.to_str().unwrap(),
            "--diff-file",
            diff.to_str().unwrap(),
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("TODO-as-done"));
}