nils-codex-cli 0.7.3

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;

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_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());
}