use codex_api::ImageBackground;use codex_api::ImageEditRequest;use codex_api::ImageGenerationRequest;use codex_api::ImageQuality;use codex_api::ImageUrl;use codex_core::context::extension_image_generation_output_hint;use codex_extension_api::ToolOutput;use codex_extension_api::ToolPayload;use codex_extension_api::ToolSpec;use codex_protocol::models::ContentItem;use codex_protocol::models::DEFAULT_IMAGE_DETAIL;use codex_protocol::models::FunctionCallOutputBody;use codex_protocol::models::FunctionCallOutputContentItem;use codex_protocol::models::FunctionCallOutputPayload;use codex_protocol::models::ResponseInputItem;use codex_protocol::models::ResponseItem;use codex_tools::ResponsesApiNamespaceTool;use pretty_assertions::assert_eq;use super::GeneratedImageOutput;use super::ImageRequest;use super::ImagegenArgs;use super::imagegen_tool_spec;use super::request_for_call_args;use crate::IMAGE_GEN_NAMESPACE;use crate::IMAGEGEN_TOOL_NAME;const RESULT: &str = "cG5n";#[test]fn uses_reserved_image_gen_namespace() { let ToolSpec::Namespace(spec) = imagegen_tool_spec() else { panic!("imagegen should advertise a namespace tool"); }; assert_eq!(spec.name, IMAGE_GEN_NAMESPACE); let ResponsesApiNamespaceTool::Function(function) = &spec.tools[0]; assert_eq!(function.name, IMAGEGEN_TOOL_NAME);}#[tokio::test]async fn omitted_references_generate_with_fixed_defaults() { assert_eq!( request_for_call_args( &ImagegenArgs { prompt: "paint a moonlit lake".to_string(), referenced_image_paths: None, num_last_images_to_include: None, }, &[], &[], ) .await .expect("generation request should build"), ImageRequest::Generate(ImageGenerationRequest { prompt: "paint a moonlit lake".to_string(), background: Some(ImageBackground::Auto), model: "gpt-image-2".to_string(), n: None, quality: Some(ImageQuality::Auto), size: Some("auto".to_string()), }) );}#[tokio::test]async fn recent_image_fallback_selects_newest_images_in_chronological_order() { let history = vec![ ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ input_image("user-1"), input_image("user-2"), ContentItem::InputText { text: "edit these".to_string(), }, ], phase: None, metadata: None, }, ResponseItem::FunctionCall { id: None, name: "mcp_image".to_string(), namespace: None, arguments: "{}".to_string(), call_id: "mcp-call".to_string(), metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "mcp-call".to_string(), output: image_output("mcp"), metadata: None, }, ResponseItem::CustomToolCall { id: None, status: Some("completed".to_string()), call_id: "code-mode-call".to_string(), name: "exec".to_string(), input: String::new(), metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "code-mode-call".to_string(), name: Some("exec".to_string()), output: image_output("code-mode"), metadata: None, }, ResponseItem::ImageGenerationCall { id: "generated-call".to_string(), status: "completed".to_string(), revised_prompt: None, result: "generated".to_string(), metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "orphan-call".to_string(), output: image_output("orphan"), metadata: None, }, ]; assert_eq!( request_for_call_args( &ImagegenArgs { prompt: "change the lighting".to_string(), referenced_image_paths: None, num_last_images_to_include: Some(4), }, &history, &[], ) .await .expect("history-backed edit request should build"), ImageRequest::Edit(expected_edit_request( "change the lighting", &["user-2", "mcp", "code-mode", "generated"], )) );}#[tokio::test]async fn conflicting_image_selectors_return_tool_error() { let error = request_for_call_args( &ImagegenArgs { prompt: "change the lighting".to_string(), referenced_image_paths: Some(vec![ "/tmp/image.png" .try_into() .expect("test path should be absolute"), ]), num_last_images_to_include: Some(1), }, &[], &[], ) .await .expect_err("conflicting selectors should fail"); assert_eq!( error.to_string(), "provide only one of `referenced_image_paths` or `num_last_images_to_include`" );}#[tokio::test]async fn too_many_referenced_image_paths_return_tool_error() { let error = request_for_call_args( &ImagegenArgs { prompt: "change the lighting".to_string(), referenced_image_paths: Some( (0..6) .map(|index| { format!("/tmp/image-{index}.png") .try_into() .expect("test path should be absolute") }) .collect(), ), num_last_images_to_include: None, }, &[], &[], ) .await .expect_err("too many paths should fail before reading files"); assert_eq!( error.to_string(), "`referenced_image_paths` must contain at most 5 paths" );}#[tokio::test]async fn recent_image_fallback_requires_requested_count() { let error = request_for_call_args( &ImagegenArgs { prompt: "change the lighting".to_string(), referenced_image_paths: None, num_last_images_to_include: Some(2), }, &[ResponseItem::Message { id: None, role: "user".to_string(), content: vec![input_image("only-image")], phase: None, metadata: None, }], &[], ) .await .expect_err("history-backed edit should require the requested image count"); assert_eq!( error.to_string(), "requested the last 2 conversation images, but only 1 were available" );}#[test]fn generated_output_returns_image_input_and_output_hint() { let output_hint = extension_image_generation_output_hint("/tmp", "/tmp/call-1.png").expect("hint should fit"); let output = GeneratedImageOutput { result: RESULT.to_string(), output_hint: Some(output_hint.clone()), }; let ResponseInputItem::FunctionCallOutput { output: response_output, .. } = output.to_response_item("call-1", &function_payload()) else { panic!("imagegen should return function tool output"); }; let FunctionCallOutputBody::ContentItems(content_items) = response_output.body else { panic!("imagegen output should contain generated image bytes"); }; assert_eq!( content_items, vec![ FunctionCallOutputContentItem::InputImage { image_url: format!("data:image/png;base64,{RESULT}"), detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputText { text: output_hint }, ] );}#[test]fn generated_output_returns_generated_image_helper_input_in_code_mode() { let output = GeneratedImageOutput { result: RESULT.to_string(), output_hint: Some("generated image save hint".to_string()), }; assert_eq!( output.code_mode_result(&function_payload()), serde_json::json!({ "image_url": format!("data:image/png;base64,{RESULT}"), "output_hint": "generated image save hint", }) );}#[test]fn generated_output_omits_oversized_output_hint() { let long_path = "x".repeat(1024); let output = GeneratedImageOutput { result: RESULT.to_string(), output_hint: extension_image_generation_output_hint("/tmp", long_path), }; let ResponseInputItem::FunctionCallOutput { output: response_output, .. } = output.to_response_item("call-1", &function_payload()) else { panic!("imagegen should return function tool output"); }; let FunctionCallOutputBody::ContentItems(content_items) = response_output.body else { panic!("imagegen output should contain generated image bytes"); }; assert_eq!( content_items, vec![FunctionCallOutputContentItem::InputImage { image_url: format!("data:image/png;base64,{RESULT}"), detail: Some(DEFAULT_IMAGE_DETAIL), }] );}fn input_image(image: &str) -> ContentItem { ContentItem::InputImage { image_url: format!("data:image/png;base64,{image}"), detail: None, }}fn image_output(image: &str) -> FunctionCallOutputPayload { FunctionCallOutputPayload::from_content_items(vec![FunctionCallOutputContentItem::InputImage { image_url: format!("data:image/png;base64,{image}"), detail: None, }])}fn expected_edit_request(prompt: &str, images: &[&str]) -> ImageEditRequest { ImageEditRequest { images: images .iter() .map(|image| ImageUrl { image_url: format!("data:image/png;base64,{image}"), }) .collect(), prompt: prompt.to_string(), background: Some(ImageBackground::Auto), model: "gpt-image-2".to_string(), n: None, quality: Some(ImageQuality::Auto), size: Some("auto".to_string()), }}fn function_payload() -> ToolPayload { ToolPayload::Function { arguments: "{}".to_string(), }}