nils-codex-cli 1.21.9

CLI crate for nils-codex-cli in the nils-cli workspace.
Documentation
use codex_cli::agent;

use std::io::{BufReader, Cursor};

use nils_test_support::{EnvGuard, GlobalStateLock, StubBinDir, prepend_path};
use pretty_assertions::assert_eq;
use serde_json::Value;

fn write_codex_stub(stub: &StubBinDir) -> tempfile::NamedTempFile {
    let args_log = tempfile::NamedTempFile::new().expect("args log");
    stub.write_exe(
        "codex",
        r#"#!/bin/bash
set -euo pipefail
out="${CODEX_TEST_ARGV_LOG:?missing CODEX_TEST_ARGV_LOG}"
: > "$out"
for a in "$@"; do
  echo "$a" >> "$out"
done
"#,
    );
    args_log
}

fn read_args(log: &tempfile::NamedTempFile) -> Vec<String> {
    std::fs::read_to_string(log.path())
        .expect("read args")
        .lines()
        .map(|s| s.to_string())
        .collect()
}

#[test]
fn agent_prompt_requires_dangerous_mode() {
    let lock = GlobalStateLock::new();
    let stub = StubBinDir::new();
    let args_log = write_codex_stub(&stub);
    let _path = prepend_path(&lock, stub.path());

    let _danger = EnvGuard::remove(&lock, "CODEX_ALLOW_DANGEROUS_ENABLED");
    let _ephemeral = EnvGuard::remove(&lock, "CODEX_CLI_EPHEMERAL_ENABLED");
    let _model = EnvGuard::set(&lock, "CODEX_CLI_MODEL", "m");
    let _reasoning = EnvGuard::set(&lock, "CODEX_CLI_REASONING", "low");
    let args_log_path = args_log.path().to_string_lossy().to_string();
    let _argv_log = EnvGuard::set(&lock, "CODEX_TEST_ARGV_LOG", &args_log_path);

    let mut stdin = BufReader::new(Cursor::new(""));
    let mut stdout: Vec<u8> = Vec::new();
    let mut stderr: Vec<u8> = Vec::new();
    let code = agent::prompt_with_io(
        &["hello".into()],
        agent::exec::ExecOptions::default(),
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert_eq!(code, 1);
    assert!(
        String::from_utf8_lossy(&stderr)
            .contains("codex-tools:prompt: disabled (set CODEX_ALLOW_DANGEROUS_ENABLED=true)")
    );
    assert!(read_args(&args_log).is_empty());
}

#[test]
fn agent_prompt_execs_codex_with_expected_args() {
    let lock = GlobalStateLock::new();
    let stub = StubBinDir::new();
    let args_log = write_codex_stub(&stub);
    let _path = prepend_path(&lock, stub.path());

    let _danger = EnvGuard::set(&lock, "CODEX_ALLOW_DANGEROUS_ENABLED", "true");
    let _ephemeral = EnvGuard::remove(&lock, "CODEX_CLI_EPHEMERAL_ENABLED");
    let _model = EnvGuard::set(&lock, "CODEX_CLI_MODEL", "gpt-test");
    let _reasoning = EnvGuard::set(&lock, "CODEX_CLI_REASONING", "high");
    let args_log_path = args_log.path().to_string_lossy().to_string();
    let _argv_log = EnvGuard::set(&lock, "CODEX_TEST_ARGV_LOG", &args_log_path);

    let mut stdin = BufReader::new(Cursor::new(""));
    let mut stdout: Vec<u8> = Vec::new();
    let mut stderr: Vec<u8> = Vec::new();
    let code = agent::prompt_with_io(
        &["hello".into(), "world".into()],
        agent::exec::ExecOptions::default(),
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert_eq!(code, 0);
    assert!(stderr.is_empty());

    assert_eq!(
        read_args(&args_log),
        vec![
            "exec",
            "--dangerously-bypass-approvals-and-sandbox",
            "-s",
            "workspace-write",
            "-m",
            "gpt-test",
            "-c",
            "model_reasoning_effort=\"high\"",
            "--",
            "hello world",
        ]
        .into_iter()
        .map(|s| s.to_string())
        .collect::<Vec<_>>()
    );
}

#[test]
fn agent_prompt_refreshes_remote_auth_before_codex_exec() {
    let lock = GlobalStateLock::new();
    let stub = StubBinDir::new();
    let args_log = write_codex_stub(&stub);
    let _path = prepend_path(&lock, stub.path());

    let dir = tempfile::TempDir::new().expect("tempdir");
    let auth_file = dir.path().join("auth.json");
    let cache = dir.path().join("cache");
    std::fs::create_dir_all(&cache).expect("cache dir");
    let ssh_args_file = dir.path().join("ssh-args.txt");
    stub.write_exe(
        "ssh",
        r#"#!/bin/bash
set -euo pipefail
printf '%s\n' "$*" > "$SSH_ARGS_FILE"
printf '%s\n' "$REMOTE_AUTH_PAYLOAD"
"#,
    );

    let _danger = EnvGuard::set(&lock, "CODEX_ALLOW_DANGEROUS_ENABLED", "true");
    let _ephemeral = EnvGuard::remove(&lock, "CODEX_CLI_EPHEMERAL_ENABLED");
    let _model = EnvGuard::set(&lock, "CODEX_CLI_MODEL", "gpt-test");
    let _reasoning = EnvGuard::set(&lock, "CODEX_CLI_REASONING", "high");
    let args_log_path = args_log.path().to_string_lossy().to_string();
    let _argv_log = EnvGuard::set(&lock, "CODEX_TEST_ARGV_LOG", &args_log_path);
    let auth_file_path = auth_file.to_string_lossy().to_string();
    let cache_path = cache.to_string_lossy().to_string();
    let ssh_args_path = ssh_args_file.to_string_lossy().to_string();
    let _auth_file = EnvGuard::set(&lock, "CODEX_AUTH_FILE", &auth_file_path);
    let _secret_cache = EnvGuard::set(&lock, "CODEX_SECRET_CACHE_DIR", &cache_path);
    let _remote_ssh = EnvGuard::set(&lock, "CODEX_AUTH_REMOTE_SSH", "auth-host");
    let _remote_name = EnvGuard::set(&lock, "CODEX_AUTH_REMOTE_NAME", "team");
    let _remote_refresh = EnvGuard::set(&lock, "CODEX_AUTH_REMOTE_REFRESH", "true");
    let _remote_payload = EnvGuard::set(
        &lock,
        "REMOTE_AUTH_PAYLOAD",
        r#"{"tokens":{"access_token":"remote-access-token","id_token":"remote-id-token","account_id":"acct_001"},"last_refresh":"2025-01-20T12:34:56Z"}"#,
    );
    let _ssh_args = EnvGuard::set(&lock, "SSH_ARGS_FILE", &ssh_args_path);

    let mut stdin = BufReader::new(Cursor::new(""));
    let mut stdout: Vec<u8> = Vec::new();
    let mut stderr: Vec<u8> = Vec::new();
    let code = agent::prompt_with_io(
        &["hello".into(), "world".into()],
        agent::exec::ExecOptions::default(),
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert_eq!(code, 0);
    assert!(stderr.is_empty());

    let captured_args = std::fs::read_to_string(&ssh_args_file).expect("read ssh args");
    assert!(captured_args.contains("auth-host"));
    assert!(captured_args.contains("codex-cli auth remote export"));
    assert!(captured_args.contains("--name team"));
    assert!(captured_args.contains("--refresh"));

    let applied: Value =
        serde_json::from_str(&std::fs::read_to_string(&auth_file).expect("read auth"))
            .expect("auth json");
    assert_eq!(applied["tokens"]["access_token"], "remote-access-token");
    assert!(!read_args(&args_log).is_empty());
}

#[test]
fn agent_prompt_execs_codex_with_ephemeral_arg_when_requested() {
    let lock = GlobalStateLock::new();
    let stub = StubBinDir::new();
    let args_log = write_codex_stub(&stub);
    let _path = prepend_path(&lock, stub.path());

    let _danger = EnvGuard::set(&lock, "CODEX_ALLOW_DANGEROUS_ENABLED", "true");
    let _ephemeral = EnvGuard::remove(&lock, "CODEX_CLI_EPHEMERAL_ENABLED");
    let _model = EnvGuard::set(&lock, "CODEX_CLI_MODEL", "gpt-test");
    let _reasoning = EnvGuard::set(&lock, "CODEX_CLI_REASONING", "high");
    let args_log_path = args_log.path().to_string_lossy().to_string();
    let _argv_log = EnvGuard::set(&lock, "CODEX_TEST_ARGV_LOG", &args_log_path);

    let mut stdin = BufReader::new(Cursor::new(""));
    let mut stdout: Vec<u8> = Vec::new();
    let mut stderr: Vec<u8> = Vec::new();
    let code = agent::prompt_with_io(
        &["hello".into(), "world".into()],
        agent::exec::ExecOptions { ephemeral: true },
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert_eq!(code, 0);
    assert!(stderr.is_empty());

    assert_eq!(
        read_args(&args_log),
        vec![
            "exec",
            "--dangerously-bypass-approvals-and-sandbox",
            "-s",
            "workspace-write",
            "-m",
            "gpt-test",
            "-c",
            "model_reasoning_effort=\"high\"",
            "--ephemeral",
            "--",
            "hello world",
        ]
        .into_iter()
        .map(|s| s.to_string())
        .collect::<Vec<_>>()
    );
}

#[test]
fn agent_prompt_reads_stdin_when_no_args() {
    let lock = GlobalStateLock::new();
    let stub = StubBinDir::new();
    let args_log = write_codex_stub(&stub);
    let _path = prepend_path(&lock, stub.path());

    let _danger = EnvGuard::set(&lock, "CODEX_ALLOW_DANGEROUS_ENABLED", "true");
    let _ephemeral = EnvGuard::remove(&lock, "CODEX_CLI_EPHEMERAL_ENABLED");
    let _model = EnvGuard::set(&lock, "CODEX_CLI_MODEL", "m");
    let _reasoning = EnvGuard::set(&lock, "CODEX_CLI_REASONING", "medium");
    let args_log_path = args_log.path().to_string_lossy().to_string();
    let _argv_log = EnvGuard::set(&lock, "CODEX_TEST_ARGV_LOG", &args_log_path);

    let mut stdin = BufReader::new(Cursor::new("from stdin\n"));
    let mut stdout: Vec<u8> = Vec::new();
    let mut stderr: Vec<u8> = Vec::new();
    let code = agent::prompt_with_io(
        &[],
        agent::exec::ExecOptions::default(),
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert_eq!(code, 0);
    assert_eq!(String::from_utf8_lossy(&stdout), "Prompt: ");
    assert!(stderr.is_empty());

    let args = read_args(&args_log);
    assert_eq!(args.last().map(|s| s.as_str()), Some("from stdin"));
}

#[test]
fn agent_prompt_empty_stdin_exits_1_without_exec() {
    let lock = GlobalStateLock::new();
    let stub = StubBinDir::new();
    let args_log = write_codex_stub(&stub);
    let _path = prepend_path(&lock, stub.path());

    let mut stdin = BufReader::new(Cursor::new(""));
    let mut stdout: Vec<u8> = Vec::new();
    let mut stderr: Vec<u8> = Vec::new();
    let code = agent::prompt_with_io(
        &[],
        agent::exec::ExecOptions::default(),
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert_eq!(code, 1);
    assert_eq!(String::from_utf8_lossy(&stdout), "Prompt: ");
    assert!(read_args(&args_log).is_empty());
}

#[test]
fn agent_prompt_blank_line_exits_1_with_missing_prompt_message() {
    let lock = GlobalStateLock::new();
    let stub = StubBinDir::new();
    let args_log = write_codex_stub(&stub);
    let _path = prepend_path(&lock, stub.path());

    let mut stdin = BufReader::new(Cursor::new("\n"));
    let mut stdout: Vec<u8> = Vec::new();
    let mut stderr: Vec<u8> = Vec::new();
    let code = agent::prompt_with_io(
        &[],
        agent::exec::ExecOptions::default(),
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert_eq!(code, 1);
    assert_eq!(String::from_utf8_lossy(&stdout), "Prompt: ");
    assert!(String::from_utf8_lossy(&stderr).contains("codex-tools: missing prompt"));
    assert!(read_args(&args_log).is_empty());
}