truth-mirror 0.8.0

Truthfulness gate and adversarial reviewer harness for AI coding agents.
Documentation
use assert_cmd::Command;
use predicates::prelude::*;
use truth_mirror::ledger::{LedgerEntry, LedgerStore, ReviewerConfig, Verdict};

fn seed_rejection(state_dir: &std::path::Path, sha: &str) {
    let store = LedgerStore::new(state_dir);
    store
        .append_entry(&LedgerEntry::new_at(
            sha,
            Verdict::Reject,
            "CLAIM: rejected | verified: cargo test | evidence: tests:cargo-test",
            vec!["tests:cargo-test".to_owned()],
            ReviewerConfig::new("claude", "claude-opus-4-1", false),
            vec!["unsupported claim".to_owned()],
            100,
        ))
        .unwrap();
}

#[test]
fn ledger_list_shows_unresolved_rejection() {
    let temp = tempfile::tempdir().unwrap();
    seed_rejection(temp.path(), "abc123");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "ledger",
            "list",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("abc123"))
        .stdout(predicate::str::contains("REJECT"));
}

#[test]
fn ledger_show_prints_entry_details() {
    let temp = tempfile::tempdir().unwrap();
    seed_rejection(temp.path(), "abc123");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "ledger",
            "show",
            "abc123",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("unsupported claim"));
}

#[test]
fn ledger_resolve_clears_unresolved_stats() {
    let temp = tempfile::tempdir().unwrap();
    seed_rejection(temp.path(), "abc123");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "ledger",
            "resolve",
            "abc123",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("resolved abc123"));

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "ledger",
            "stats",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("unresolved=0"))
        .stdout(predicate::str::contains("resolved=1"));
}

#[test]
fn ledger_waive_records_reason() {
    let temp = tempfile::tempdir().unwrap();
    seed_rejection(temp.path(), "abc123");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "ledger",
            "waive",
            "abc123",
            "--reason",
            "Ramiro approved exception",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("waived abc123"));

    let markdown = std::fs::read_to_string(temp.path().join("ledger.md")).unwrap();
    assert!(markdown.contains("Ramiro approved exception"));
}

#[test]
fn reinject_claude_is_quiet_when_grok_env_and_skill_present() {
    let temp = tempfile::tempdir().unwrap();
    seed_rejection(temp.path(), "abc123");
    // Owned Grok skill present → Claude reinject is suppressed under Grok env.
    let skill = temp.path().join(".grok/skills/truth-mirror/SKILL.md");
    std::fs::create_dir_all(skill.parent().unwrap()).unwrap();
    std::fs::write(
        &skill,
        format!(
            "{}\nrun truth-mirror reinject --agent grok\n",
            truth_mirror::surface::GROK_SKILL_MANAGED_MARKER
        ),
    )
    .unwrap();

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(temp.path())
        .env("GROK_SESSION_ID", "sess-test")
        .env("GROK_HOOK_EVENT", "user_prompt_submit")
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "reinject",
            "--agent",
            "claude",
        ])
        .assert()
        .success()
        .stdout(predicate::str::is_empty());
}

#[test]
fn reinject_claude_warns_under_grok_without_skill() {
    let temp = tempfile::tempdir().unwrap();
    seed_rejection(temp.path(), "abc123");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(temp.path())
        .env("GROK_SESSION_ID", "sess-test")
        .env_remove("GROK_HOOK_EVENT")
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "reinject",
            "--agent",
            "claude",
        ])
        .assert()
        .success()
        .stderr(predicate::str::contains("without"))
        .stdout(predicate::str::contains("Claude"))
        .stdout(predicate::str::contains("abc123"));
}

#[test]
fn reinject_grok_still_prints_under_grok_env() {
    let temp = tempfile::tempdir().unwrap();
    seed_rejection(temp.path(), "abc123");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .env("GROK_SESSION_ID", "sess-test")
        .env("GROK_HOOK_EVENT", "user_prompt_submit")
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "reinject",
            "--agent",
            "grok",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("Grok"))
        .stdout(predicate::str::contains("abc123"));
}

#[test]
fn reinject_claude_still_prints_without_grok_env() {
    let temp = tempfile::tempdir().unwrap();
    seed_rejection(temp.path(), "abc123");

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .env_remove("GROK_SESSION_ID")
        .env_remove("GROK_HOOK_EVENT")
        .args([
            "--state-dir",
            temp.path().to_str().unwrap(),
            "reinject",
            "--agent",
            "claude",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("Claude"))
        .stdout(predicate::str::contains("abc123"));
}