use anyhow::Result;use app_test_support::TestAppServer;use app_test_support::to_response;use codex_app_server_protocol::JSONRPCResponse;use codex_app_server_protocol::RequestId;use codex_app_server_protocol::ThreadStartParams;use codex_app_server_protocol::ThreadStartResponse;use codex_app_server_protocol::TurnStartParams;use codex_app_server_protocol::TurnStartResponse;use codex_app_server_protocol::UserInput as V2UserInput;use core_test_support::responses;use core_test_support::skip_if_no_network;use pretty_assertions::assert_eq;use std::path::Path;use tempfile::TempDir;use tokio::time::timeout;const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);#[tokio::test]async fn turn_start_accepts_output_schema_v2() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let body = responses::sse(vec![ responses::ev_response_created("resp-1"), responses::ev_assistant_message("msg-1", "Done"), responses::ev_completed("resp-1"), ]); let response_mock = responses::mount_sse_once(&server, body).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp .send_thread_start_request(ThreadStartParams { ..Default::default() }) .await?; let thread_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), ) .await??; let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?; let output_schema = serde_json::json!({ "type": "object", "properties": { "answer": { "type": "string" } }, "required": ["answer"], "additionalProperties": false }); let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), }], output_schema: Some(output_schema.clone()), ..Default::default() }) .await?; let turn_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), ) .await??; let _turn: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?; timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_notification_message("turn/completed"), ) .await??; let request = response_mock.single_request(); let payload = request.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": output_schema, }) ); Ok(())}#[tokio::test]async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let body1 = responses::sse(vec![ responses::ev_response_created("resp-1"), responses::ev_assistant_message("msg-1", "Done"), responses::ev_completed("resp-1"), ]); let response_mock1 = responses::mount_sse_once(&server, body1).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp .send_thread_start_request(ThreadStartParams { ..Default::default() }) .await?; let thread_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), ) .await??; let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?; let output_schema = serde_json::json!({ "type": "object", "properties": { "answer": { "type": "string" } }, "required": ["answer"], "additionalProperties": false }); let turn_req_1 = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), }], output_schema: Some(output_schema.clone()), ..Default::default() }) .await?; let turn_resp_1: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(turn_req_1)), ) .await??; let _turn: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp_1)?; timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_notification_message("turn/completed"), ) .await??; let payload1 = response_mock1.single_request().body_json(); assert_eq!( payload1.pointer("/text/format"), Some(&serde_json::json!({ "name": "codex_output_schema", "type": "json_schema", "strict": true, "schema": output_schema, })) ); let body2 = responses::sse(vec![ responses::ev_response_created("resp-2"), responses::ev_assistant_message("msg-2", "Done"), responses::ev_completed("resp-2"), ]); let response_mock2 = responses::mount_sse_once(&server, body2).await; let turn_req_2 = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello again".to_string(), text_elements: Vec::new(), }], output_schema: None, ..Default::default() }) .await?; let turn_resp_2: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(turn_req_2)), ) .await??; let _turn: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp_2)?; timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_notification_message("turn/completed"), ) .await??; let payload2 = response_mock2.single_request().body_json(); assert_eq!(payload2.pointer("/text/format"), None); Ok(())}fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); std::fs::write( config_toml, format!( r#"model = "mock-model"approval_policy = "never"sandbox_mode = "read-only"model_provider = "mock_provider"[model_providers.mock_provider]name = "Mock provider for test"base_url = "{server_uri}/v1"wire_api = "responses"request_max_retries = 0stream_max_retries = 0"# ), )}