From 5b0a195e481fc04fd101ac0406bff762764190f8 Mon Sep 17 00:00:00 2001 From: Liang-Ting Jiang Date: Tue, 12 May 2026 08:07:51 +0000 Subject: [PATCH] test(core): cover code mode app file uploads Add integration coverage that exercises Code Mode's nested MCP tool path through openai/fileParams rewriting for Codex Apps file uploads. Co-authored-by: Codex --- codex-rs/core/tests/suite/openai_file_mcp.rs | 138 +++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/codex-rs/core/tests/suite/openai_file_mcp.rs b/codex-rs/core/tests/suite/openai_file_mcp.rs index 0f0dcf46f1..1887c4513e 100644 --- a/codex-rs/core/tests/suite/openai_file_mcp.rs +++ b/codex-rs/core/tests/suite/openai_file_mcp.rs @@ -15,11 +15,13 @@ use core_test_support::apps_test_server::DOCUMENT_EXTRACT_TEXT_RESOURCE_URI; use core_test_support::hooks::trust_discovered_hooks; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_custom_tool_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::test_codex; use pretty_assertions::assert_eq; use serde_json::Value; @@ -245,3 +247,139 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res server.verify().await; Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn code_mode_nested_mcp_file_params_upload_local_paths_before_tool_call() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let apps_server = AppsTestServer::mount(&server).await?; + + Mock::given(method("POST")) + .and(path("/files")) + .and(header("chatgpt-account-id", "account_id")) + .and(body_json(json!({ + "file_name": "report.txt", + "file_size": 11, + "use_case": "codex", + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "file_id": "file_123", + "upload_url": format!("{}/upload/file_123", server.uri()), + }))) + .expect(1) + .mount(&server) + .await; + Mock::given(method("PUT")) + .and(path("/upload/file_123")) + .and(header("content-length", "11")) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(path("/files/file_123/uploaded")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "status": "success", + "download_url": format!("{}/download/file_123", server.uri()), + "file_name": "report.txt", + "mime_type": "text/plain", + "file_size_bytes": 11, + }))) + .expect(1) + .mount(&server) + .await; + + let code = r#" +const result = await tools.mcp__codex_apps__calendar_extract_text({ + file: "report.txt", +}); +text(result.content?.[0]?.text ?? "missing result"); +"#; + mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_custom_tool_call("call-1", "exec", code), + 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 = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(move |config| { + configure_apps(config, apps_server.chatgpt_base_url.as_str()); + config + .features + .enable(Feature::CodeMode) + .expect("test config should allow feature update"); + }); + let test = builder.build(&server).await?; + tokio::fs::write(test.cwd.path().join("report.txt"), b"hello world").await?; + + test.submit_turn_with_approval_and_permission_profile( + "Use code mode to extract the report text with the app tool.", + AskForApproval::Never, + PermissionProfile::Disabled, + ) + .await?; + + let apps_tool_call = server + .received_requests() + .await + .unwrap_or_default() + .into_iter() + .find_map(|request| { + let body: Value = serde_json::from_slice(&request.body).ok()?; + (request.url.path() == "/api/codex/apps" + && body.get("method").and_then(Value::as_str) == Some("tools/call") + && body.pointer("/params/name").and_then(Value::as_str) + == Some("calendar_extract_text")) + .then_some(body) + }) + .expect("apps calendar_extract_text tools/call request should be recorded"); + + assert_eq!( + apps_tool_call.pointer("/params/arguments/file"), + Some(&json!({ + "download_url": format!("{}/download/file_123", server.uri()), + "file_id": "file_123", + "mime_type": "text/plain", + "file_name": "report.txt", + "uri": "sediment://file_123", + "file_size_bytes": 11, + })) + ); + let codex_apps_meta = apps_tool_call + .pointer("/params/_meta/_codex_apps") + .expect("codex apps metadata should be forwarded"); + assert!( + codex_apps_meta + .pointer("/call_id") + .and_then(Value::as_str) + .is_some_and(|call_id| call_id.starts_with("exec-")) + ); + assert_eq!( + codex_apps_meta.pointer("/resource_uri"), + Some(&json!(DOCUMENT_EXTRACT_TEXT_RESOURCE_URI)) + ); + assert_eq!( + codex_apps_meta.pointer("/contains_mcp_source"), + Some(&json!(true)) + ); + assert_eq!( + codex_apps_meta.pointer("/connector_id"), + Some(&json!("calendar")) + ); + + server.verify().await; + Ok(()) +}