Compare commits

...

3 Commits

Author SHA1 Message Date
pakrym-oai
f2eb50be9f exec: fix config_overrides merging to ensure root flags are applied to subcommands
- Merge config_overrides from the root CLI into subcommands in run_main.
- Extend tests for config_overrides arguments ordering in config_overrides_are_applied_resume.
2025-09-30 09:46:11 -07:00
pakrym-oai
17cee69a85 cli: suppress clippy::large_enum_variant warning for Subcommand enum 2025-09-30 09:39:17 -07:00
pakrym-oai
19d8658917 Support all exec flags on resume 2025-09-30 09:33:49 -07:00
6 changed files with 193 additions and 38 deletions

View File

@@ -51,6 +51,7 @@ struct MultitoolCli {
subcommand: Option<Subcommand>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, clap::Subcommand)]
enum Subcommand {
/// Run Codex non-interactively.
@@ -255,7 +256,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
}
Some(Subcommand::Exec(mut exec_cli)) => {
prepend_config_flags(
&mut exec_cli.config_overrides,
&mut exec_cli.shared.config_overrides,
root_config_overrides.clone(),
);
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;

View File

@@ -1,3 +1,4 @@
use clap::Args;
use clap::Parser;
use clap::ValueEnum;
use codex_common::CliConfigOverrides;
@@ -10,6 +11,33 @@ pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[clap(flatten)]
pub shared: SharedExecArgs,
}
#[derive(Debug, clap::Subcommand)]
pub enum Command {
/// Resume a previous session by id or pick the most recent with --last.
Resume(ResumeArgs),
}
#[derive(Parser, Debug)]
pub struct ResumeArgs {
/// Conversation/session id (UUID). When provided, resumes this session.
/// If omitted, use --last to pick the most recent recorded session.
#[arg(value_name = "SESSION_ID")]
pub session_id: Option<String>,
/// Resume the most recent recorded session (newest) without specifying an id.
#[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
pub last: bool,
#[clap(flatten)]
pub shared: SharedExecArgs,
}
#[derive(Debug, Clone, Default, Args)]
pub struct SharedExecArgs {
/// Optional image(s) to attach to the initial prompt.
#[arg(long = "image", short = 'i', value_name = "FILE", value_delimiter = ',', num_args = 1..)]
pub images: Vec<PathBuf>,
@@ -56,9 +84,6 @@ pub struct Cli {
#[arg(long = "output-schema", value_name = "FILE")]
pub output_schema: Option<PathBuf>,
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
/// Specifies color settings for use in the output.
#[arg(long = "color", value_enum, default_value_t = Color::Auto)]
pub color: Color,
@@ -86,34 +111,15 @@ pub struct Cli {
#[arg(long = "output-last-message")]
pub last_message_file: Option<PathBuf>,
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
/// Initial instructions for the agent. If not provided as an argument (or
/// if `-` is used), instructions are read from stdin.
#[arg(value_name = "PROMPT")]
pub prompt: Option<String>,
}
#[derive(Debug, clap::Subcommand)]
pub enum Command {
/// Resume a previous session by id or pick the most recent with --last.
Resume(ResumeArgs),
}
#[derive(Parser, Debug)]
pub struct ResumeArgs {
/// Conversation/session id (UUID). When provided, resumes this session.
/// If omitted, use --last to pick the most recent recorded session.
#[arg(value_name = "SESSION_ID")]
pub session_id: Option<String>,
/// Resume the most recent recorded session (newest) without specifying an id.
#[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
pub last: bool,
/// Prompt to send after resuming the session. If `-` is used, read from stdin.
#[arg(value_name = "PROMPT")]
pub prompt: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum Color {

View File

@@ -36,6 +36,7 @@ use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
use crate::cli::Command as ExecCommand;
use crate::cli::SharedExecArgs;
use crate::event_processor::CodexStatus;
use crate::event_processor::EventProcessor;
use codex_core::default_client::set_default_originator;
@@ -46,8 +47,19 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
tracing::warn!(?err, "Failed to set codex exec originator override {err:?}");
}
let Cli {
command,
let Cli { command, shared } = cli;
// Merge overrides from root CLI.
let mut config_overrides = shared.config_overrides.clone();
let shared = command
.as_ref()
.map(|cmd| match cmd {
ExecCommand::Resume(args) => args.shared.clone(),
})
.unwrap_or(shared);
let SharedExecArgs {
images,
model: model_cli_arg,
oss,
@@ -61,19 +73,15 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
json: json_mode,
experimental_json,
sandbox_mode: sandbox_mode_cli_arg,
prompt,
output_schema: output_schema_path,
include_plan_tool,
config_overrides,
} = cli;
prompt: prompt_arg,
..
} = shared;
// Determine the prompt source (parent or subcommand) and read from stdin if needed.
let prompt_arg = match &command {
// Allow prompt before the subcommand by falling back to the parent-level prompt
// when the Resume subcommand did not provide its own prompt.
Some(ExecCommand::Resume(args)) => args.prompt.clone().or(prompt),
None => prompt,
};
config_overrides
.raw_overrides
.splice(0..0, shared.config_overrides.raw_overrides);
let prompt = match prompt_arg {
Some(p) if p != "-" => p,

View File

@@ -30,6 +30,7 @@ fn main() -> anyhow::Result<()> {
// Merge root-level overrides into inner CLI struct so downstream logic remains unchanged.
let mut inner = top_cli.inner;
inner
.shared
.config_overrides
.raw_overrides
.splice(0..0, top_cli.config_overrides.raw_overrides);

View File

@@ -0,0 +1,138 @@
#![cfg(not(target_os = "windows"))]
#![allow(clippy::expect_used, clippy::unwrap_used)]
use core_test_support::responses;
use core_test_support::test_codex_exec::test_codex_exec;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_overrides_are_applied_resume() -> anyhow::Result<()> {
config_overrides_are_applied_resume_inner(
&["--model", "o3", "prompt text"],
&[
"resume",
"--skip-git-repo-check",
"fake id",
"--model",
"o3",
"resume prompt text",
],
)
.await?;
config_overrides_are_applied_resume_inner(
&["-c", "model=o3", "prompt text"],
&[
"resume",
"--skip-git-repo-check",
"fake id",
"-c",
"model=o3",
"resume prompt text",
],
)
.await?;
config_overrides_are_applied_resume_inner(
&["-c", "model=o3", "prompt text"],
&[
"-c",
"model=o3",
"resume",
"--skip-git-repo-check",
"fake id",
"resume prompt text",
],
)
.await?;
Ok(())
}
async fn config_overrides_are_applied_resume_inner(
args: &[&str],
resume_args: &[&str],
) -> anyhow::Result<()> {
let test = test_codex_exec();
let match_config_overrides_applied = |req: &wiremock::Request| {
let body = std::str::from_utf8(&req.body).unwrap_or("");
body.contains("prompt text") && body.contains("o3")
};
let server = responses::start_mock_server().await;
responses::mount_sse_once_match(
&server,
match_config_overrides_applied,
responses::sse(vec![
responses::ev_assistant_message("msg-1", "config overrides are applied."),
responses::ev_completed("resp-2"),
]),
)
.await;
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("--experimental-json")
.args(args)
.assert()
.code(0);
let match_config_resume_overrides_applied = |req: &wiremock::Request| {
let body = std::str::from_utf8(&req.body).unwrap_or("");
body.contains("resume prompt text") && body.contains("o3")
};
responses::mount_sse_once_match(
&server,
match_config_resume_overrides_applied,
responses::sse(vec![
responses::ev_assistant_message("msg-1", "config overrides are applied resume."),
responses::ev_completed("resp-2"),
]),
)
.await;
test.cmd_with_server(&server)
.args(resume_args)
.assert()
.code(0);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_overrides_are_applied() -> anyhow::Result<()> {
config_overrides_are_applied_inner(&["-c", "model=o3", "prompt text"]).await?;
config_overrides_are_applied_inner(&["prompt text", "-c", "model=o3"]).await?;
config_overrides_are_applied_inner(&["--model", "o3", "prompt text"]).await?;
Ok(())
}
async fn config_overrides_are_applied_inner(args: &[&str]) -> anyhow::Result<()> {
let test = test_codex_exec();
let match_config_overrides_applied = |req: &wiremock::Request| {
let body = std::str::from_utf8(&req.body).unwrap_or("");
body.contains("prompt text") && body.contains("o3")
};
let server = responses::start_mock_server().await;
responses::mount_sse_once_match(
&server,
match_config_overrides_applied,
responses::sse(vec![
responses::ev_assistant_message("msg-1", "config overrides are applied."),
responses::ev_completed("resp-2"),
]),
)
.await;
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.args(args)
.assert()
.code(0);
Ok(())
}

View File

@@ -1,5 +1,6 @@
// Aggregates all former standalone integration tests as modules.
mod apply_patch;
mod config_overrides;
mod output_schema;
mod resume;
mod sandbox;