From fb04d9774e752e9b581183668d1fa6f8af848c31 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Fri, 29 Aug 2025 16:30:45 -0700 Subject: [PATCH] wip test --- codex-rs/core/tests/suite/prompt_caching.rs | 148 ++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 68605ab44f..13a8ecbb04 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -3,6 +3,7 @@ use codex_core::ConversationManager; use codex_core::ModelProviderInfo; use codex_core::built_in_model_providers; +use codex_core::config_types::McpServerConfig; use codex_core::model_family::find_family_for_model; use codex_core::protocol::AskForApproval; use codex_core::protocol::EventMsg; @@ -556,3 +557,150 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() { ); assert_eq!(body2["input"], expected_body2); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn mcp_server_ordering_is_preserved_across_requests() { + use pretty_assertions::assert_eq; + + let server = MockServer::start().await; + + let sse = sse_completed("resp"); + let template = ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(sse, "text/event-stream"); + + // Expect two POSTs to /v1/responses + Mock::given(method("POST")) + .and(path("/v1/responses")) + .respond_with(template) + .expect(2) + .mount(&server) + .await; + + let model_provider = ModelProviderInfo { + base_url: Some(format!("{}/v1", server.uri())), + ..built_in_model_providers()["openai"].clone() + }; + + let cwd = TempDir::new().unwrap(); + let codex_home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&codex_home); + config.cwd = cwd.path().to_path_buf(); + config.model_provider = model_provider; + config.user_instructions = Some("be consistent and helpful".to_string()); + let mut mcp_servers = indexmap::IndexMap::new(); + mcp_servers.insert( + "test_mcp_server_1".to_string(), + McpServerConfig { + command: "bash -lc 'while read -r line; do echo \"{\"id\": 1, \"tools\": [{\"name\": \"test_tool_1\", \"description\": \"Test tool 1\" }]} \"; done'".to_string(), + args: vec![], + env: None, + }, + ); + // mcp_servers.insert( + // "test_mcp_server_2".to_string(), + // McpServerConfig { + // command: "echo".to_string(), + // args: vec![ + // "{\"id\": 2, \"tools\": [{\"name\": \"test_tool_2\", \"description\": \"Test tool 2\"}]}".to_string(), + // ], + // env: None, + // }, + // ); + config.mcp_servers = mcp_servers; + + let conversation_manager = + ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key")); + let codex = conversation_manager + .new_conversation(config) + .await + .expect("create new conversation") + .conversation; + + // First turn + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello 1".into(), + }], + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Second turn using per-turn overrides via UserTurn + let new_cwd = TempDir::new().unwrap(); + let writable = TempDir::new().unwrap(); + codex + .submit(Op::UserTurn { + items: vec![InputItem::Text { + text: "hello 2".into(), + }], + cwd: new_cwd.path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::WorkspaceWrite { + writable_roots: vec![writable.path().to_path_buf()], + network_access: true, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }, + model: "o3".to_string(), + effort: ReasoningEffort::High, + summary: ReasoningSummary::Detailed, + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + // Verify we issued exactly two requests, and the cached prefix stayed identical. + let requests = server.received_requests().await.unwrap(); + assert_eq!(requests.len(), 2, "expected two POST requests"); + + let body1 = requests[0].body_json::().unwrap(); + let body2 = requests[1].body_json::().unwrap(); + + // Validate MCP tools presence in the request body. The tool name should be + // fully qualified as "__". + let expected_mcp_tool_name = "test_mcp_server_1__test_tool_1"; + let tools1 = body1["tools"].as_array().unwrap(); + assert!( + tools1.iter().any(|t| { + t.get("type").and_then(|v| v.as_str()) == Some("function") + && t.get("name").and_then(|v| v.as_str()) == Some(expected_mcp_tool_name) + }), + "missing expected MCP tool in first request: {}", + expected_mcp_tool_name + ); + let tools2 = body2["tools"].as_array().unwrap(); + assert!( + tools2.iter().any(|t| { + t.get("type").and_then(|v| v.as_str()) == Some("function") + && t.get("name").and_then(|v| v.as_str()) == Some(expected_mcp_tool_name) + }), + "missing expected MCP tool in second request: {}", + expected_mcp_tool_name + ); + + // prompt_cache_key should remain constant across per-turn overrides + assert_eq!( + body1["prompt_cache_key"], body2["prompt_cache_key"], + "prompt_cache_key should not change across per-turn overrides" + ); + + // The entire prefix from the first request should be identical and reused + // as the prefix of the second request. + let expected_user_message_2 = serde_json::json!({ + "type": "message", + "id": serde_json::Value::Null, + "role": "user", + "content": [ { "type": "input_text", "text": "hello 2" } ] + }); + let expected_body2 = serde_json::json!( + [ + body1["input"].as_array().unwrap().as_slice(), + [expected_user_message_2].as_slice(), + ] + .concat() + ); + assert_eq!(body2["input"], expected_body2); +}