nils-codex-cli 0.7.3

CLI crate for nils-codex-cli in the nils-cli workspace.
Documentation
use nils_test_support::StubBinDir;
use nils_test_support::bin;
use nils_test_support::cmd::{self, CmdOptions, CmdOutput};
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::fs;
use std::path::PathBuf;

fn codex_cli_bin() -> PathBuf {
    bin::resolve("codex-cli")
}

fn run_with(args: &[&str], options: &CmdOptions) -> CmdOutput {
    let bin = codex_cli_bin();
    cmd::run_with(&bin, args, options)
}

fn stdout(output: &CmdOutput) -> String {
    output.stdout_text()
}

fn stderr(output: &CmdOutput) -> String {
    output.stderr_text()
}

fn codex_stub_script() -> &'static str {
    r#"#!/bin/bash
set -euo pipefail
if [[ -n "${NILS_TEST_STUB_LOG:-}" ]]; then
  echo "$*" >> "${NILS_TEST_STUB_LOG}"
fi
exit "${CODEX_STUB_EXIT_CODE:-0}"
"#
}

#[test]
fn auth_login_default_uses_chatgpt_browser_flow() {
    let stubs = StubBinDir::new();
    stubs.write_exe("codex", codex_stub_script());
    let log = tempfile::NamedTempFile::new().expect("log");

    let options = CmdOptions::default()
        .with_path_prepend(stubs.path())
        .with_env("NILS_TEST_STUB_LOG", log.path().to_string_lossy().as_ref());
    let output = run_with(&["auth", "login"], &options);
    assert_eq!(output.code, 0);
    assert!(stdout(&output).contains("chatgpt-browser"));

    let log_content = fs::read_to_string(log.path()).expect("read log");
    assert!(log_content.contains("login"));
}

#[test]
fn auth_login_device_code_and_api_key_map_to_expected_args() {
    let stubs = StubBinDir::new();
    stubs.write_exe("codex", codex_stub_script());
    let log = tempfile::NamedTempFile::new().expect("log");

    let options = CmdOptions::default()
        .with_path_prepend(stubs.path())
        .with_env("NILS_TEST_STUB_LOG", log.path().to_string_lossy().as_ref());
    let output = run_with(&["auth", "login", "--device-code"], &options);
    assert_eq!(output.code, 0);
    let output = run_with(&["auth", "login", "--api-key"], &options);
    assert_eq!(output.code, 0);

    let log_content = fs::read_to_string(log.path()).expect("read log");
    assert!(log_content.contains("login --device-auth"));
    assert!(log_content.contains("login --with-api-key"));
}

#[test]
fn auth_login_json_success_is_structured() {
    let stubs = StubBinDir::new();
    stubs.write_exe("codex", codex_stub_script());

    let options = CmdOptions::default().with_path_prepend(stubs.path());
    let output = run_with(&["auth", "login", "--json", "--device-code"], &options);
    assert_eq!(output.code, 0);

    let payload: Value = serde_json::from_str(&stdout(&output)).expect("json");
    assert_eq!(payload["schema_version"], "codex-cli.auth.v1");
    assert_eq!(payload["command"], "auth login");
    assert_eq!(payload["ok"], true);
    assert_eq!(payload["result"]["method"], "chatgpt-device-code");
    assert_eq!(payload["result"]["provider"], "chatgpt");
}

#[test]
fn auth_login_rejects_conflicting_flags() {
    let output = run_with(
        &["auth", "login", "--api-key", "--device-code"],
        &CmdOptions::default(),
    );
    assert_eq!(output.code, 64);
    assert!(stderr(&output).contains("--api-key"));
}

#[test]
fn auth_login_json_non_zero_status_is_structured_error() {
    let stubs = StubBinDir::new();
    stubs.write_exe("codex", codex_stub_script());

    let options = CmdOptions::default()
        .with_path_prepend(stubs.path())
        .with_env("CODEX_STUB_EXIT_CODE", "7");
    let output = run_with(&["auth", "login", "--json", "--api-key"], &options);
    assert_eq!(output.code, 7);

    let payload: Value = serde_json::from_str(&stdout(&output)).expect("json");
    assert_eq!(payload["ok"], false);
    assert_eq!(payload["error"]["code"], "login-failed");
}

#[test]
fn auth_login_json_missing_codex_is_structured_error() {
    let options = CmdOptions::default().with_env("PATH", "");
    let output = run_with(&["auth", "login", "--json"], &options);
    assert_eq!(output.code, 1);

    let payload: Value = serde_json::from_str(&stdout(&output)).expect("json");
    assert_eq!(payload["ok"], false);
    assert_eq!(payload["error"]["code"], "login-exec-failed");
}