Codex Handbook
core/tests/suite/mcp_turn_metadata.rs 461 lines
#![cfg(not(target_os = "windows"))]#![allow(clippy::unwrap_used)]use anyhow::Result;use codex_config::types::AppToolApproval;use codex_core::config::Config;use codex_features::Feature;use codex_protocol::config_types::ApprovalsReviewer;use codex_protocol::config_types::CollaborationMode;use codex_protocol::config_types::ModeKind;use codex_protocol::config_types::Settings;use codex_protocol::models::PermissionProfile;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::ElicitationAction;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::Op;use codex_protocol::request_user_input::RequestUserInputAnswer;use codex_protocol::request_user_input::RequestUserInputResponse;use codex_protocol::user_input::UserInput;use core_test_support::PathExt;use core_test_support::apps_test_server::AppsTestServer;use core_test_support::apps_test_server::SEARCH_CALENDAR_CREATE_TOOL;use core_test_support::apps_test_server::SEARCH_CALENDAR_NAMESPACE;use core_test_support::apps_test_server::recorded_apps_tool_call_by_call_id;use core_test_support::apps_test_server::search_capable_apps_builder;use core_test_support::responses::ev_assistant_message;use core_test_support::responses::ev_completed;use core_test_support::responses::ev_function_call;use core_test_support::responses::ev_function_call_with_namespace;use core_test_support::responses::ev_response_created;use core_test_support::responses::mount_sse_sequence;use core_test_support::responses::sse;use core_test_support::responses::start_mock_server;use core_test_support::skip_if_no_network;use core_test_support::test_codex::TestCodex;use core_test_support::test_codex::local_selections;use core_test_support::test_codex::turn_permission_fields;use core_test_support::wait_for_event;use core_test_support::wait_for_event_match;use pretty_assertions::assert_eq;use serde_json::json;use std::collections::HashMap;fn set_calendar_approval_mode(config: &mut Config, approval_mode: AppToolApproval) {    let approval_mode = match approval_mode {        AppToolApproval::Auto => "auto",        AppToolApproval::Prompt => "prompt",        AppToolApproval::Approve => "approve",    };    let user_config_path = config.codex_home.join("config.toml").abs();    let user_config = toml::from_str(&format!(        r#"[apps.calendar]default_tools_approval_mode = "{approval_mode}""#    ))    .expect("apps config should parse");    config.config_layer_stack = config        .config_layer_stack        .with_user_config(&user_config_path, user_config);}fn set_calendar_approval_mode_and_default_reviewer(    config: &mut Config,    approval_mode: AppToolApproval,    default_approvals_reviewer: ApprovalsReviewer,) {    let approval_mode = match approval_mode {        AppToolApproval::Auto => "auto",        AppToolApproval::Prompt => "prompt",        AppToolApproval::Approve => "approve",    };    let user_config_path = config.codex_home.join("config.toml").abs();    let user_config = toml::from_str(&format!(        r#"[apps._default]approvals_reviewer = "{default_approvals_reviewer}"[apps.calendar]default_tools_approval_mode = "{approval_mode}""#    ))    .expect("apps config should parse");    config.config_layer_stack = config        .config_layer_stack        .with_user_config(&user_config_path, user_config);}async fn submit_user_turn(    test: &TestCodex,    text: &str,    approval_policy: AskForApproval,    collaboration_mode: Option<CollaborationMode>,) -> Result<()> {    let session_model = test.session_configured.model.clone();    let (sandbox_policy, permission_profile) =        turn_permission_fields(PermissionProfile::Disabled, test.cwd.path());    test.codex        .submit(Op::UserInput {            items: vec![UserInput::Text {                text: text.to_string(),                text_elements: Vec::new(),            }],            final_output_json_schema: None,            responsesapi_client_metadata: None,            additional_context: Default::default(),            thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {                environments: Some(local_selections(test.config.cwd.clone())),                approval_policy: Some(approval_policy),                sandbox_policy: Some(sandbox_policy),                permission_profile,                collaboration_mode: collaboration_mode.or({                    Some(codex_protocol::config_types::CollaborationMode {                        mode: codex_protocol::config_types::ModeKind::Default,                        settings: codex_protocol::config_types::Settings {                            model: session_model,                            reasoning_effort: None,                            developer_instructions: None,                        },                    })                }),                ..Default::default()            },        })        .await?;    Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn approved_mcp_tool_call_metadata_records_prior_user_input_request() -> Result<()> {    skip_if_no_network!(Ok(()));    let server = start_mock_server().await;    let apps_server = AppsTestServer::mount(&server).await?;    let call_id = "calendar-call-approval";    let calendar_args = serde_json::to_string(&json!({        "title": "Lunch",        "starts_at": "2026-03-10T12:00:00Z"    }))?;    let mock = mount_sse_sequence(        &server,        vec![            sse(vec![                ev_response_created("resp-1"),                ev_function_call_with_namespace(                    call_id,                    SEARCH_CALENDAR_NAMESPACE,                    SEARCH_CALENDAR_CREATE_TOOL,                    &calendar_args,                ),                ev_completed("resp-1"),            ]),            sse(vec![                ev_response_created("resp-2"),                ev_assistant_message("msg-1", "done"),                ev_completed("resp-2"),            ]),        ],    )    .await;    let mut builder = search_capable_apps_builder(apps_server.chatgpt_base_url.clone())        .with_config(|config| {            // Use the opposite global reviewer so this route must come from apps._default.            config.approvals_reviewer = ApprovalsReviewer::AutoReview;            config                .features                .enable(Feature::ToolCallMcpElicitation)                .expect("test config should allow feature update");            set_calendar_approval_mode_and_default_reviewer(                config,                AppToolApproval::Prompt,                ApprovalsReviewer::User,            );        });    let test = builder.build(&server).await?;    submit_user_turn(        &test,        "Use [$calendar](app://calendar) to create a calendar event.",        AskForApproval::OnRequest,        /*collaboration_mode*/ None,    )    .await?;    let EventMsg::McpToolCallBegin(begin) = wait_for_event(&test.codex, |event| {        matches!(event, EventMsg::McpToolCallBegin(_))    })    .await    else {        unreachable!("event guard guarantees McpToolCallBegin");    };    assert_eq!(begin.call_id, call_id);    let EventMsg::ElicitationRequest(request) = wait_for_event(&test.codex, |event| {        matches!(            event,            EventMsg::ElicitationRequest(_) | EventMsg::TurnComplete(_)        )    })    .await    else {        panic!("expected apps._default user to route the app approval to the user");    };    test.codex        .submit(Op::ResolveElicitation {            server_name: request.server_name,            request_id: request.id,            decision: ElicitationAction::Accept,            content: None,            meta: None,        })        .await?;    wait_for_event(&test.codex, |event| {        matches!(event, EventMsg::TurnComplete(_))    })    .await;    assert_eq!(mock.requests().len(), 2);    let apps_tool_call = recorded_apps_tool_call_by_call_id(&server, call_id).await;    assert_eq!(        apps_tool_call            .pointer("/params/_meta/x-codex-turn-metadata/user_input_requested_during_turn"),        Some(&json!(true))    );    Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apps_default_auto_review_routes_actual_mcp_approval_to_guardian() -> Result<()> {    skip_if_no_network!(Ok(()));    let server = start_mock_server().await;    let apps_server = AppsTestServer::mount(&server).await?;    let call_id = "calendar-default-auto-review";    let calendar_args = serde_json::to_string(&json!({        "title": "Lunch",        "starts_at": "2026-03-10T12:00:00Z"    }))?;    let responses = mount_sse_sequence(        &server,        vec![            sse(vec![                ev_response_created("resp-parent-tool"),                ev_function_call_with_namespace(                    call_id,                    SEARCH_CALENDAR_NAMESPACE,                    SEARCH_CALENDAR_CREATE_TOOL,                    &calendar_args,                ),                ev_completed("resp-parent-tool"),            ]),            sse(vec![                ev_response_created("resp-guardian-review"),                ev_assistant_message(                    "msg-guardian-review",                    &json!({                        "risk_level": "low",                        "user_authorization": "high",                        "outcome": "allow",                        "rationale": "Creating this calendar event is low risk.",                    })                    .to_string(),                ),                ev_completed("resp-guardian-review"),            ]),            sse(vec![                ev_response_created("resp-parent-done"),                ev_assistant_message("msg-parent-done", "done"),                ev_completed("resp-parent-done"),            ]),        ],    )    .await;    let mut builder = search_capable_apps_builder(apps_server.chatgpt_base_url.clone())        .with_config(|config| {            // Use the opposite global reviewer so this route must come from apps._default.            config.approvals_reviewer = ApprovalsReviewer::User;            config                .features                .enable(Feature::ToolCallMcpElicitation)                .expect("test config should allow feature update");            set_calendar_approval_mode_and_default_reviewer(                config,                AppToolApproval::Prompt,                ApprovalsReviewer::AutoReview,            );        });    let test = builder.build(&server).await?;    submit_user_turn(        &test,        "Use [$calendar](app://calendar) to create a calendar event.",        AskForApproval::OnRequest,        /*collaboration_mode*/ None,    )    .await?;    let route_event = wait_for_event(&test.codex, |event| {        matches!(            event,            EventMsg::ElicitationRequest(_) | EventMsg::TurnComplete(_)        )    })    .await;    assert!(        matches!(route_event, EventMsg::TurnComplete(_)),        "expected apps._default auto_review to route the app approval to Guardian"    );    let guardian_request = responses        .requests()        .into_iter()        .find(|request| {            request                .instructions_text()                .starts_with("You are judging one planned coding-agent action.")        })        .expect("expected a Guardian request for the app MCP approval");    assert!(guardian_request.body_contains_text("calendar_create_event"));    assert!(guardian_request.body_contains_text("Lunch"));    let apps_tool_call = recorded_apps_tool_call_by_call_id(&server, call_id).await;    assert_eq!(        apps_tool_call.pointer("/params/arguments/title"),        Some(&json!("Lunch"))    );    Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn mcp_tool_call_metadata_records_prior_request_user_input_tool() -> Result<()> {    skip_if_no_network!(Ok(()));    let server = start_mock_server().await;    let apps_server = AppsTestServer::mount(&server).await?;    let request_user_input_call_id = "user-input-call";    let calendar_call_id = "calendar-call-after-user-input";    let request_user_input_args = json!({        "questions": [{            "id": "confirm_path",            "header": "Confirm",            "question": "Proceed with the plan?",            "options": [{                "label": "Yes (Recommended)",                "description": "Continue the current plan."            }, {                "label": "No",                "description": "Stop and revisit the approach."            }]        }]    })    .to_string();    let calendar_args = serde_json::to_string(&json!({        "title": "Lunch",        "starts_at": "2026-03-10T12:00:00Z"    }))?;    let mock = mount_sse_sequence(        &server,        vec![            sse(vec![                ev_response_created("resp-1"),                ev_function_call(                    request_user_input_call_id,                    "request_user_input",                    &request_user_input_args,                ),                ev_completed("resp-1"),            ]),            sse(vec![                ev_response_created("resp-2"),                ev_function_call_with_namespace(                    calendar_call_id,                    SEARCH_CALENDAR_NAMESPACE,                    SEARCH_CALENDAR_CREATE_TOOL,                    &calendar_args,                ),                ev_completed("resp-2"),            ]),            sse(vec![                ev_response_created("resp-3"),                ev_assistant_message("msg-1", "done"),                ev_completed("resp-3"),            ]),        ],    )    .await;    let mut builder = search_capable_apps_builder(apps_server.chatgpt_base_url.clone())        .with_config(|config| {            set_calendar_approval_mode(config, AppToolApproval::Approve);        });    let test = builder.build(&server).await?;    submit_user_turn(        &test,        "Ask for confirmation, then create a calendar event.",        AskForApproval::Never,        Some(CollaborationMode {            mode: ModeKind::Plan,            settings: Settings {                model: test.session_configured.model.clone(),                reasoning_effort: None,                developer_instructions: None,            },        }),    )    .await?;    let request = wait_for_event_match(&test.codex, |event| match event {        EventMsg::RequestUserInput(request) => Some(request.clone()),        _ => None,    })    .await;    assert_eq!(request.call_id, request_user_input_call_id);    test.codex        .submit(Op::UserInputAnswer {            id: request.turn_id,            response: RequestUserInputResponse {                answers: HashMap::from([(                    "confirm_path".to_string(),                    RequestUserInputAnswer {                        answers: vec!["Yes (Recommended)".to_string()],                    },                )]),            },        })        .await?;    let EventMsg::McpToolCallBegin(begin) = wait_for_event(&test.codex, |event| {        matches!(event, EventMsg::McpToolCallBegin(_))    })    .await    else {        unreachable!("event guard guarantees McpToolCallBegin");    };    assert_eq!(begin.call_id, calendar_call_id);    wait_for_event(&test.codex, |event| {        matches!(event, EventMsg::TurnComplete(_))    })    .await;    assert_eq!(mock.requests().len(), 3);    let apps_tool_call = recorded_apps_tool_call_by_call_id(&server, calendar_call_id).await;    assert_eq!(        apps_tool_call            .pointer("/params/_meta/x-codex-turn-metadata/user_input_requested_during_turn"),        Some(&json!(true))    );    Ok(())}