@@ -6,8 +6,8 @@ use codex_core::protocol::EventMsg;
66use codex_core:: protocol:: InputItem ;
77use codex_core:: protocol:: Op ;
88use codex_core:: protocol:: SandboxPolicy ;
9- use codex_core:: protocol_config_types:: ReasoningEffort as ReasoningEffortConfig ;
10- use codex_core:: protocol_config_types:: ReasoningSummary as ReasoningSummaryConfig ;
9+ use codex_core:: protocol_config_types:: ReasoningEffort ;
10+ use codex_core:: protocol_config_types:: ReasoningSummary ;
1111use codex_login:: CodexAuth ;
1212use core_test_support:: load_default_config_for_test;
1313use core_test_support:: load_sse_fixture_with_id;
@@ -197,8 +197,8 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() {
197197 exclude_slash_tmp : true ,
198198 } ) ,
199199 model : Some ( "o3" . to_string ( ) ) ,
200- effort : Some ( ReasoningEffortConfig :: High ) ,
201- summary : Some ( ReasoningSummaryConfig :: Detailed ) ,
200+ effort : Some ( ReasoningEffort :: High ) ,
201+ summary : Some ( ReasoningSummary :: Detailed ) ,
202202 } )
203203 . await
204204 . unwrap ( ) ;
@@ -256,3 +256,107 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() {
256256 ) ;
257257 assert_eq ! ( body2[ "input" ] , expected_body2) ;
258258}
259+
260+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
261+ async fn per_turn_overrides_keep_cached_prefix_and_key_constant ( ) {
262+ use pretty_assertions:: assert_eq;
263+
264+ let server = MockServer :: start ( ) . await ;
265+
266+ let sse = sse_completed ( "resp" ) ;
267+ let template = ResponseTemplate :: new ( 200 )
268+ . insert_header ( "content-type" , "text/event-stream" )
269+ . set_body_raw ( sse, "text/event-stream" ) ;
270+
271+ // Expect two POSTs to /v1/responses
272+ Mock :: given ( method ( "POST" ) )
273+ . and ( path ( "/v1/responses" ) )
274+ . respond_with ( template)
275+ . expect ( 2 )
276+ . mount ( & server)
277+ . await ;
278+
279+ let model_provider = ModelProviderInfo {
280+ base_url : Some ( format ! ( "{}/v1" , server. uri( ) ) ) ,
281+ ..built_in_model_providers ( ) [ "openai" ] . clone ( )
282+ } ;
283+
284+ let cwd = TempDir :: new ( ) . unwrap ( ) ;
285+ let codex_home = TempDir :: new ( ) . unwrap ( ) ;
286+ let mut config = load_default_config_for_test ( & codex_home) ;
287+ config. cwd = cwd. path ( ) . to_path_buf ( ) ;
288+ config. model_provider = model_provider;
289+ config. user_instructions = Some ( "be consistent and helpful" . to_string ( ) ) ;
290+
291+ let conversation_manager = ConversationManager :: default ( ) ;
292+ let codex = conversation_manager
293+ . new_conversation_with_auth ( config, Some ( CodexAuth :: from_api_key ( "Test API Key" ) ) )
294+ . await
295+ . expect ( "create new conversation" )
296+ . conversation ;
297+
298+ // First turn
299+ codex
300+ . submit ( Op :: UserInput {
301+ items : vec ! [ InputItem :: Text {
302+ text: "hello 1" . into( ) ,
303+ } ] ,
304+ } )
305+ . await
306+ . unwrap ( ) ;
307+ wait_for_event ( & codex, |ev| matches ! ( ev, EventMsg :: TaskComplete ( _) ) ) . await ;
308+
309+ // Second turn using per-turn overrides via UserTurn
310+ let new_cwd = TempDir :: new ( ) . unwrap ( ) ;
311+ let writable = TempDir :: new ( ) . unwrap ( ) ;
312+ codex
313+ . submit ( Op :: UserTurn {
314+ items : vec ! [ InputItem :: Text {
315+ text: "hello 2" . into( ) ,
316+ } ] ,
317+ cwd : new_cwd. path ( ) . to_path_buf ( ) ,
318+ approval_policy : AskForApproval :: Never ,
319+ sandbox_policy : SandboxPolicy :: WorkspaceWrite {
320+ writable_roots : vec ! [ writable. path( ) . to_path_buf( ) ] ,
321+ network_access : true ,
322+ exclude_tmpdir_env_var : true ,
323+ exclude_slash_tmp : true ,
324+ } ,
325+ model : "o3" . to_string ( ) ,
326+ effort : ReasoningEffort :: High ,
327+ summary : ReasoningSummary :: Detailed ,
328+ } )
329+ . await
330+ . unwrap ( ) ;
331+ wait_for_event ( & codex, |ev| matches ! ( ev, EventMsg :: TaskComplete ( _) ) ) . await ;
332+
333+ // Verify we issued exactly two requests, and the cached prefix stayed identical.
334+ let requests = server. received_requests ( ) . await . unwrap ( ) ;
335+ assert_eq ! ( requests. len( ) , 2 , "expected two POST requests" ) ;
336+
337+ let body1 = requests[ 0 ] . body_json :: < serde_json:: Value > ( ) . unwrap ( ) ;
338+ let body2 = requests[ 1 ] . body_json :: < serde_json:: Value > ( ) . unwrap ( ) ;
339+
340+ // prompt_cache_key should remain constant across per-turn overrides
341+ assert_eq ! (
342+ body1[ "prompt_cache_key" ] , body2[ "prompt_cache_key" ] ,
343+ "prompt_cache_key should not change across per-turn overrides"
344+ ) ;
345+
346+ // The entire prefix from the first request should be identical and reused
347+ // as the prefix of the second request.
348+ let expected_user_message_2 = serde_json:: json!( {
349+ "type" : "message" ,
350+ "id" : serde_json:: Value :: Null ,
351+ "role" : "user" ,
352+ "content" : [ { "type" : "input_text" , "text" : "hello 2" } ]
353+ } ) ;
354+ let expected_body2 = serde_json:: json!(
355+ [
356+ body1[ "input" ] . as_array( ) . unwrap( ) . as_slice( ) ,
357+ [ expected_user_message_2] . as_slice( ) ,
358+ ]
359+ . concat( )
360+ ) ;
361+ assert_eq ! ( body2[ "input" ] , expected_body2) ;
362+ }
0 commit comments