diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 6c79c34c99..8274510b9f 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -2346,7 +2346,7 @@ mod tests { } #[test] - fn exec_resume_accepts_output_last_message_flag_after_subcommand() { + fn exec_resume_accepts_output_flags_after_subcommand() { let cli = MultitoolCli::try_parse_from([ "codex", "exec", @@ -2354,6 +2354,8 @@ mod tests { "session-123", "-o", "/tmp/resume-output.md", + "--output-schema", + "/tmp/schema.json", "re-review", ]) .expect("parse should succeed"); @@ -2369,6 +2371,10 @@ mod tests { exec.last_message_file, Some(std::path::PathBuf::from("/tmp/resume-output.md")) ); + assert_eq!( + exec.output_schema, + Some(std::path::PathBuf::from("/tmp/schema.json")) + ); assert_eq!(args.session_id.as_deref(), Some("session-123")); assert_eq!(args.prompt.as_deref(), Some("re-review")); } diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 3a5ebfd1ba..53124c11a5 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -50,7 +50,7 @@ pub struct Cli { pub removed_full_auto: bool, /// Path to a JSON Schema file describing the model's final response shape. - #[arg(long = "output-schema", value_name = "FILE")] + #[arg(long = "output-schema", value_name = "FILE", global = true)] pub output_schema: Option, #[clap(skip)] diff --git a/codex-rs/exec/src/cli_tests.rs b/codex-rs/exec/src/cli_tests.rs index 45f2aa330d..524067dd66 100644 --- a/codex-rs/exec/src/cli_tests.rs +++ b/codex-rs/exec/src/cli_tests.rs @@ -36,7 +36,7 @@ fn resume_parses_prompt_after_global_flags() { } #[test] -fn resume_accepts_output_last_message_flag_after_subcommand() { +fn resume_accepts_output_flags_after_subcommand() { const PROMPT: &str = "echo resume-with-output-file"; let cli = Cli::parse_from([ "codex-exec", @@ -44,6 +44,8 @@ fn resume_accepts_output_last_message_flag_after_subcommand() { "session-123", "-o", "/tmp/resume-output.md", + "--output-schema", + "/tmp/schema.json", PROMPT, ]); @@ -51,6 +53,7 @@ fn resume_accepts_output_last_message_flag_after_subcommand() { cli.last_message_file, Some(PathBuf::from("/tmp/resume-output.md")) ); + assert_eq!(cli.output_schema, Some(PathBuf::from("/tmp/schema.json"))); let Some(Command::Resume(args)) = cli.command else { panic!("expected resume command"); }; diff --git a/codex-rs/exec/tests/suite/resume.rs b/codex-rs/exec/tests/suite/resume.rs index 2bf6416ccc..904c6a8f19 100644 --- a/codex-rs/exec/tests/suite/resume.rs +++ b/codex-rs/exec/tests/suite/resume.rs @@ -375,6 +375,62 @@ async fn exec_resume_accepts_global_flags_after_subcommand() -> anyhow::Result<( Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn exec_resume_includes_output_schema_in_request() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let test = test_codex_exec(); + let server = MockServer::start().await; + let response_mock = mount_exec_responses(&server, /*count*/ 2).await; + + let schema_contents = serde_json::json!({ + "type": "object", + "properties": { + "answer": { "type": "string" } + }, + "required": ["answer"], + "additionalProperties": false + }); + let schema_path = test.cwd_path().join("schema.json"); + std::fs::write(&schema_path, serde_json::to_vec_pretty(&schema_contents)?)?; + + test.cmd_with_server(&server) + .arg("--skip-git-repo-check") + .arg("echo seed-resume-session") + .assert() + .success(); + + test.cmd_with_server(&server) + .arg("--skip-git-repo-check") + .arg("resume") + .arg("--last") + .arg("--json") + .arg("--output-schema") + .arg(&schema_path) + .arg("echo resume-with-schema") + .assert() + .success(); + + let requests = response_mock.requests(); + assert_eq!(requests.len(), 2); + let payload: Value = requests[1].body_json(); + let text = payload.get("text").expect("request missing text field"); + let format = text + .get("format") + .expect("request missing text.format field"); + assert_eq!( + format, + &serde_json::json!({ + "name": "codex_output_schema", + "type": "json_schema", + "strict": true, + "schema": schema_contents, + }) + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> { skip_if_no_network!(Ok(()));