drep-ai 2.5.0

A local commit gate: runs the linters your repo configures, and sends changed code to an LLM for review
Documentation
//! The Codex invocation is an isolation contract, not merely a command line.

use std::ffi::OsString;
use std::path::Path;

use crate::analysis::response_contract::output_schema;
use crate::config::ReasoningEffort;
use crate::llm::codex::command::{
    ChildEnvironment, allowed_nonsensitive_name, instructions_text, invocation_args,
    sensitive_name, toml_override,
};

#[test]
fn every_argument_is_present_in_the_load_bearing_order() {
    let args = invocation_args(
        "gpt-5.6-sol",
        Some(&ReasoningEffort::High),
        Path::new("/tmp/drep codex/instructions.md"),
        Path::new("/tmp/drep codex/schema.json"),
        Path::new("/tmp/drep codex/cwd"),
    )
    .expect("UTF-8 fixture paths");
    let args: Vec<String> = args
        .into_iter()
        .map(|arg| arg.into_string().expect("UTF-8 fixture"))
        .collect();

    assert_eq!(
        args,
        vec![
            "-c",
            "forced_login_method=\"chatgpt\"",
            "-c",
            "model_instructions_file=\"/tmp/drep codex/instructions.md\"",
            "-c",
            "model_reasoning_effort=\"high\"",
            "-c",
            "project_doc_max_bytes=0",
            "-c",
            "web_search=\"disabled\"",
            "--disable",
            "shell_tool",
            "--disable",
            "unified_exec",
            "--disable",
            "apps",
            "--disable",
            "multi_agent",
            "--disable",
            "hooks",
            "--disable",
            "memories",
            "-a",
            "never",
            "exec",
            "--ephemeral",
            "--ignore-user-config",
            "--ignore-rules",
            "--sandbox",
            "read-only",
            "--skip-git-repo-check",
            "-C",
            "/tmp/drep codex/cwd",
            "--model",
            "gpt-5.6-sol",
            "--output-schema",
            "/tmp/drep codex/schema.json",
            "--json",
            "-",
        ]
    );
}

#[test]
fn absent_reasoning_effort_leaves_the_cli_default_unset() {
    let args = invocation_args(
        "gpt-5.6-sol",
        None,
        Path::new("/tmp/instructions.md"),
        Path::new("/tmp/schema.json"),
        Path::new("/tmp/cwd"),
    )
    .expect("paths");
    let args = args
        .iter()
        .map(|arg| arg.to_string_lossy())
        .collect::<Vec<_>>();

    assert!(
        !args
            .iter()
            .any(|arg| arg.contains("model_reasoning_effort")),
        "the CLI default must not be replaced: {args:?}"
    );
}

#[test]
fn private_files_replace_generic_instructions_and_pin_the_response_shape() {
    let instructions = instructions_text("Review Rust carefully.");
    assert!(instructions.starts_with("Review Rust carefully."));
    assert!(instructions.contains("Use only the code supplied on stdin"));
    assert!(instructions.contains("Do not use tools"));

    let schema = output_schema();
    assert_eq!(schema["additionalProperties"], false);
    assert_eq!(schema["required"], serde_json::json!(["issues", "summary"]));
    let issue = &schema["properties"]["issues"]["items"];
    assert_eq!(issue["additionalProperties"], false);
    assert_eq!(
        issue["required"],
        serde_json::json!([
            "line",
            "severity",
            "category",
            "message",
            "suggestion",
            "code_snippet",
            "compile_failure"
        ])
    );
    assert_eq!(
        issue["properties"]["severity"]["enum"],
        serde_json::json!(["critical", "high", "medium"])
    );
}

#[test]
fn toml_overrides_encode_quotes_and_backslashes_with_toml_itself() {
    let path = r#"C:\reviewer's \"prompt\".md"#;
    let encoded = toml_override("model_instructions_file", path);
    let decoded: toml::Value = toml::from_str(&encoded).expect("override is valid TOML");
    assert_eq!(decoded["model_instructions_file"].as_str(), Some(path));
}

#[test]
fn child_environment_is_an_allowlist_and_never_forwards_api_keys() {
    let environment = ChildEnvironment::from_iter(
        [
            ("PATH", "/bin"),
            ("HOME", "/home/test"),
            ("CODEX_HOME", "/home/test/.codex"),
            ("HTTPS_PROXY", "http://proxy"),
            ("SSL_CERT_FILE", "/certs/ca.pem"),
            ("LANG", "en_US.UTF-8"),
            ("OPENAI_API_KEY", "openai-secret"),
            ("KIMI_API_KEY", "kimi-secret"),
            ("DREP_AUTH_PATH", "/real/auth.toml"),
            ("UNRELATED_SECRET", "secret"),
        ]
        .map(|(name, value)| (OsString::from(name), OsString::from(value))),
    );

    assert_eq!(environment.get("PATH"), Some("/bin"));
    assert_eq!(environment.get("HOME"), Some("/home/test"));
    assert_eq!(environment.get("CODEX_HOME"), Some("/home/test/.codex"));
    assert_eq!(environment.get("HTTPS_PROXY"), Some("http://proxy"));
    assert_eq!(environment.get("SSL_CERT_FILE"), Some("/certs/ca.pem"));
    assert_eq!(environment.get("LANG"), Some("en_US.UTF-8"));
    for forbidden in [
        "OPENAI_API_KEY",
        "KIMI_API_KEY",
        "DREP_AUTH_PATH",
        "UNRELATED_SECRET",
    ] {
        assert_eq!(environment.get(forbidden), None, "forwarded {forbidden}");
    }
}

#[test]
fn each_secret_name_pattern_is_independently_forbidden() {
    assert!(sensitive_name("DREP_ANYTHING"));
    assert!(sensitive_name("VENDOR_API_KEY"));
    assert!(sensitive_name("GITHUB_TOKEN"));
    assert!(sensitive_name("client_secret"));
    assert!(sensitive_name("DB_PASSWORD"));
    assert!(sensitive_name("AWS_CREDENTIALS_FILE"));
    assert!(!sensitive_name("PATH"));
}

#[test]
fn windows_allowlist_matches_names_and_locale_prefixes_independently() {
    assert!(allowed_nonsensitive_name("Path", true));
    assert!(allowed_nonsensitive_name("lc_messages", true));
    assert!(!allowed_nonsensitive_name("UNRELATED", true));
}