truth-mirror 0.4.0

Truthfulness gate and adversarial reviewer harness for AI coding agents.
Documentation
use assert_cmd::Command;
use predicates::prelude::*;

#[test]
fn install_hooks_dry_run_prints_plan_without_writing_hooks() {
    let temp = tempfile::tempdir().unwrap();
    std::process::Command::new("git")
        .arg("init")
        .current_dir(temp.path())
        .status()
        .unwrap();

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(temp.path())
        .args(["install-hooks", "--dry-run", "--claude", "--codex", "--pi"])
        .assert()
        .success()
        .stdout(predicate::str::contains("truth-mirror hook plan"))
        .stdout(predicate::str::contains("commit-msg"))
        .stdout(predicate::str::contains("pre-push"))
        .stdout(predicate::str::contains(
            "surface: claude -> .claude/settings.json",
        ))
        .stdout(predicate::str::contains(
            "surface: codex -> .codex/hooks.json",
        ))
        .stdout(predicate::str::contains(
            "surface: pi -> .pi/extensions/truth-mirror.js",
        ));

    assert!(!temp.path().join(".truth-mirror/hooks/commit-msg").exists());
    assert!(!temp.path().join(".claude/settings.json").exists());
    assert!(!temp.path().join(".codex/hooks.json").exists());
    assert!(!temp.path().join(".pi/extensions/truth-mirror.js").exists());
}

fn git_init(dir: &std::path::Path) {
    std::process::Command::new("git")
        .arg("init")
        .current_dir(dir)
        .status()
        .unwrap();
}

#[test]
fn install_hooks_writes_selected_agent_surfaces() {
    let temp = tempfile::tempdir().unwrap();
    git_init(temp.path());

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(temp.path())
        .args(["install-hooks", "--claude", "--codex", "--pi"])
        .assert()
        .success();

    let claude = std::fs::read_to_string(temp.path().join(".claude/settings.json")).unwrap();
    assert!(claude.contains("UserPromptSubmit"));
    assert!(claude.contains("truth-mirror reinject --agent claude"));

    // Codex uses the same nested schema as Claude (verified against Codex 0.142.4).
    let codex: serde_json::Value = serde_json::from_str(
        &std::fs::read_to_string(temp.path().join(".codex/hooks.json")).unwrap(),
    )
    .unwrap();
    assert_eq!(
        codex
            .pointer("/hooks/UserPromptSubmit/0/hooks/0/command")
            .and_then(serde_json::Value::as_str),
        Some("truth-mirror reinject --agent codex")
    );
    assert_eq!(
        codex
            .pointer("/hooks/UserPromptSubmit/0/hooks/0/type")
            .and_then(serde_json::Value::as_str),
        Some("command")
    );

    // Pi uses a project-local extension file, not a hooks.json.
    assert!(!temp.path().join(".pi/hooks.json").exists());
    let pi = std::fs::read_to_string(temp.path().join(".pi/extensions/truth-mirror.js")).unwrap();
    assert!(pi.contains("truth-mirror"));
    assert!(pi.contains("reinject"));
    assert!(pi.contains("pi.on(\"context\""));
}

#[test]
fn install_hooks_only_touches_selected_agents() {
    let temp = tempfile::tempdir().unwrap();
    git_init(temp.path());

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(temp.path())
        .args(["install-hooks", "--claude"])
        .assert()
        .success();

    assert!(temp.path().join(".claude/settings.json").exists());
    assert!(!temp.path().join(".codex/hooks.json").exists());
    assert!(!temp.path().join(".pi/hooks.json").exists());
}

#[test]
fn install_is_non_clobbering_and_uninstall_reverses_only_our_entries() {
    let temp = tempfile::tempdir().unwrap();
    git_init(temp.path());
    std::fs::create_dir_all(temp.path().join(".claude")).unwrap();
    std::fs::write(
        temp.path().join(".claude/settings.json"),
        r#"{"model":"sonnet","hooks":{"PreToolUse":[{"matcher":"Bash"}]}}"#,
    )
    .unwrap();

    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(temp.path())
        .args(["install-hooks", "--claude"])
        .assert()
        .success();

    let after_install = std::fs::read_to_string(temp.path().join(".claude/settings.json")).unwrap();
    assert!(after_install.contains("sonnet"));
    assert!(after_install.contains("PreToolUse"));
    assert!(after_install.contains("truth-mirror reinject --agent claude"));

    // Bare uninstall clears all agent surfaces but must preserve foreign config.
    Command::cargo_bin("truth-mirror")
        .unwrap()
        .current_dir(temp.path())
        .args(["install-hooks", "--uninstall"])
        .assert()
        .success();

    let after_uninstall =
        std::fs::read_to_string(temp.path().join(".claude/settings.json")).unwrap();
    assert!(after_uninstall.contains("sonnet"));
    assert!(after_uninstall.contains("PreToolUse"));
    assert!(!after_uninstall.contains("truth-mirror reinject"));
}