Codex Handbook
app-server/tests/suite/v2/output_schema.rs 237 lines
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"#        ),    )}