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:
Curtis 'Fjord' Hawthorne
2026-01-05 14:12:09 -08:00
committed by GitHub
parent 58a91a0b50
commit 5f8776d34d
3 changed files with 158 additions and 5 deletions

View File

@@ -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(())
}