mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
Allow global exec flags after resume and fix CI codex build/timeout (#8440)
**Motivation** - Bring `codex exec resume` to parity with top‑level flags so global options (git check bypass, json, model, sandbox toggles) work after the subcommand, including when outside a git repo. **Description** - Exec CLI: mark `--skip-git-repo-check`, `--json`, `--model`, `--full-auto`, and `--dangerously-bypass-approvals-and-sandbox` as global so they’re accepted after `resume`. - Tests: add `exec_resume_accepts_global_flags_after_subcommand` to verify those flags work when passed after `resume`. **Testing** - `just fmt` - `cargo test -p codex-exec` (pass; ran with elevated perms to allow network/port binds) - Manual: exercised `codex exec resume` with global flags after the subcommand to confirm behavior.
This commit is contained in:
committed by
pap
parent
eeae40fa67
commit
b79cc88d19
@@ -11,11 +11,17 @@ pub struct Cli {
|
||||
pub command: Option<Command>,
|
||||
|
||||
/// Optional image(s) to attach to the initial prompt.
|
||||
#[arg(long = "image", short = 'i', value_name = "FILE", value_delimiter = ',', num_args = 1..)]
|
||||
#[arg(
|
||||
long = "image",
|
||||
short = 'i',
|
||||
value_name = "FILE",
|
||||
value_delimiter = ',',
|
||||
num_args = 1..
|
||||
)]
|
||||
pub images: Vec<PathBuf>,
|
||||
|
||||
/// Model the agent should use.
|
||||
#[arg(long, short = 'm')]
|
||||
#[arg(long, short = 'm', global = true)]
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Use open-source provider.
|
||||
@@ -37,7 +43,7 @@ pub struct Cli {
|
||||
pub config_profile: Option<String>,
|
||||
|
||||
/// Convenience alias for low-friction sandboxed automatic execution (-a on-request, --sandbox workspace-write).
|
||||
#[arg(long = "full-auto", default_value_t = false)]
|
||||
#[arg(long = "full-auto", default_value_t = false, global = true)]
|
||||
pub full_auto: bool,
|
||||
|
||||
/// Skip all confirmation prompts and execute commands without sandboxing.
|
||||
@@ -46,6 +52,7 @@ pub struct Cli {
|
||||
long = "dangerously-bypass-approvals-and-sandbox",
|
||||
alias = "yolo",
|
||||
default_value_t = false,
|
||||
global = true,
|
||||
conflicts_with = "full_auto"
|
||||
)]
|
||||
pub dangerously_bypass_approvals_and_sandbox: bool,
|
||||
@@ -55,7 +62,7 @@ pub struct Cli {
|
||||
pub cwd: Option<PathBuf>,
|
||||
|
||||
/// Allow running Codex outside a Git repository.
|
||||
#[arg(long = "skip-git-repo-check", default_value_t = false)]
|
||||
#[arg(long = "skip-git-repo-check", global = true, default_value_t = false)]
|
||||
pub skip_git_repo_check: bool,
|
||||
|
||||
/// Additional directories that should be writable alongside the primary workspace.
|
||||
@@ -74,7 +81,12 @@ pub struct Cli {
|
||||
pub color: Color,
|
||||
|
||||
/// Print events to stdout as JSONL.
|
||||
#[arg(long = "json", alias = "experimental-json", default_value_t = false)]
|
||||
#[arg(
|
||||
long = "json",
|
||||
alias = "experimental-json",
|
||||
default_value_t = false,
|
||||
global = true
|
||||
)]
|
||||
pub json: bool,
|
||||
|
||||
/// Specifies file where the last message from the agent should be written.
|
||||
@@ -107,6 +119,16 @@ pub struct ResumeArgs {
|
||||
#[arg(long = "last", default_value_t = false)]
|
||||
pub last: bool,
|
||||
|
||||
/// Optional image(s) to attach to the prompt sent after resuming.
|
||||
#[arg(
|
||||
long = "image",
|
||||
short = 'i',
|
||||
value_name = "FILE",
|
||||
value_delimiter = ',',
|
||||
num_args = 1
|
||||
)]
|
||||
pub images: Vec<PathBuf>,
|
||||
|
||||
/// Prompt to send after resuming the session. If `-` is used, read from stdin.
|
||||
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
|
||||
pub prompt: Option<String>,
|
||||
|
||||
@@ -335,6 +335,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
let prompt_text = resolve_prompt(prompt_arg);
|
||||
let mut items: Vec<UserInput> = imgs
|
||||
.into_iter()
|
||||
.chain(args.images.into_iter())
|
||||
.map(|path| UserInput::LocalImage { path })
|
||||
.collect();
|
||||
items.push(UserInput::Text {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
use anyhow::Context;
|
||||
use core_test_support::test_codex_exec::test_codex_exec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
use std::string::ToString;
|
||||
@@ -69,6 +70,39 @@ fn extract_conversation_id(path: &std::path::Path) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn last_user_image_count(path: &std::path::Path) -> usize {
|
||||
let content = std::fs::read_to_string(path).unwrap_or_default();
|
||||
let mut last_count = 0;
|
||||
for line in content.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Ok(item): Result<Value, _> = serde_json::from_str(line) else {
|
||||
continue;
|
||||
};
|
||||
if item.get("type").and_then(|t| t.as_str()) != Some("response_item") {
|
||||
continue;
|
||||
}
|
||||
let Some(payload) = item.get("payload") else {
|
||||
continue;
|
||||
};
|
||||
if payload.get("type").and_then(|t| t.as_str()) != Some("message") {
|
||||
continue;
|
||||
}
|
||||
if payload.get("role").and_then(|r| r.as_str()) != Some("user") {
|
||||
continue;
|
||||
}
|
||||
let Some(content_items) = payload.get("content").and_then(|v| v.as_array()) else {
|
||||
continue;
|
||||
};
|
||||
last_count = content_items
|
||||
.iter()
|
||||
.filter(|entry| entry.get("type").and_then(|t| t.as_str()) == Some("input_image"))
|
||||
.count();
|
||||
}
|
||||
last_count
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
@@ -177,6 +211,41 @@ fn exec_resume_last_accepts_prompt_after_flag_in_json_mode() -> anyhow::Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_resume_accepts_global_flags_after_subcommand() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
let fixture =
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cli_responses_fixture.sse");
|
||||
|
||||
// Seed a session.
|
||||
test.cmd()
|
||||
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
||||
.env("OPENAI_BASE_URL", "http://unused.local")
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("echo seed-resume-session")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Resume while passing global flags after the subcommand to ensure clap accepts them.
|
||||
test.cmd()
|
||||
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
||||
.env("OPENAI_BASE_URL", "http://unused.local")
|
||||
.arg("resume")
|
||||
.arg("--last")
|
||||
.arg("--json")
|
||||
.arg("--model")
|
||||
.arg("gpt-5.2-codex")
|
||||
.arg("--config")
|
||||
.arg("reasoning_level=xhigh")
|
||||
.arg("--dangerously-bypass-approvals-and-sandbox")
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("echo resume-with-global-flags-after-subcommand")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
@@ -309,3 +378,64 @@ fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> {
|
||||
assert!(content.contains(&marker2));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_resume_accepts_images_after_subcommand() -> anyhow::Result<()> {
|
||||
let test = test_codex_exec();
|
||||
let fixture =
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cli_responses_fixture.sse");
|
||||
|
||||
let marker = format!("resume-image-{}", Uuid::new_v4());
|
||||
let prompt = format!("echo {marker}");
|
||||
|
||||
test.cmd()
|
||||
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
||||
.env("OPENAI_BASE_URL", "http://unused.local")
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("-C")
|
||||
.arg(env!("CARGO_MANIFEST_DIR"))
|
||||
.arg(&prompt)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let image_path = test.cwd_path().join("resume_image.png");
|
||||
let image_path_2 = test.cwd_path().join("resume_image_2.png");
|
||||
let image_bytes: &[u8] = &[
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
|
||||
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F,
|
||||
0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00,
|
||||
0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
];
|
||||
std::fs::write(&image_path, image_bytes)?;
|
||||
std::fs::write(&image_path_2, image_bytes)?;
|
||||
|
||||
let marker2 = format!("resume-image-2-{}", Uuid::new_v4());
|
||||
let prompt2 = format!("echo {marker2}");
|
||||
test.cmd()
|
||||
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
||||
.env("OPENAI_BASE_URL", "http://unused.local")
|
||||
.arg("--skip-git-repo-check")
|
||||
.arg("-C")
|
||||
.arg(env!("CARGO_MANIFEST_DIR"))
|
||||
.arg("resume")
|
||||
.arg("--last")
|
||||
.arg("--image")
|
||||
.arg(&image_path)
|
||||
.arg("--image")
|
||||
.arg(&image_path_2)
|
||||
.arg(&prompt2)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let sessions_dir = test.home_path().join("sessions");
|
||||
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
||||
.expect("no session file found after resume with images");
|
||||
let image_count = last_user_image_count(&resumed_path);
|
||||
assert_eq!(
|
||||
image_count, 2,
|
||||
"resume prompt should include both attached images"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user