truth-mirror 0.9.1

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

// `truth-mirror ledger resolve <sha>` and `ledger waive <sha> --reason <r>`
// moved to the new top-level `truth-mirror resolve` subcommand. The original
// self-service path is gone: only petitions and the TTY-gated human waive
// remain. Coverage for the new flow lives in tests/resolve_cli.rs.

// `truth-mirror ledger waive <sha> --reason` was the old human path; it now
// lives at the top-level `truth-mirror resolve --waive` and is TTY-gated. The
// non-TTY refusal + TTY happy path are tested in tests/resolve_cli.rs.

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