From 15730c2fcb4d6a0af3aa409a9dd68f38971e6d68 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Thu, 12 Feb 2026 15:07:54 -0800 Subject: [PATCH] Align compaction tests with incoming-item and empty-history behavior --- .../app-server/tests/suite/v2/compaction.rs | 5 - codex-rs/core/src/codex.rs | 10 +- codex-rs/core/tests/suite/compact.rs | 138 ++-- codex-rs/core/tests/suite/compact_remote.rs | 31 +- .../core/tests/suite/compact_resume_fork.rs | 616 ++---------------- ...nual_compact_without_prev_user_shapes.snap | 13 +- ..._compaction_including_incoming_shapes.snap | 4 +- ...nual_compact_without_prev_user_shapes.snap | 9 +- ...te_pre_turn_compaction_failure_shapes.snap | 7 +- ..._compaction_including_incoming_shapes.snap | 15 +- 10 files changed, 176 insertions(+), 672 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 5b5faa02d6..09d75879f7 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -203,11 +203,6 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() - skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; - let sse = responses::sse(vec![ - responses::ev_assistant_message("m1", "MANUAL_COMPACT_SUMMARY"), - responses::ev_completed_with_tokens("r1", 200), - ]); - responses::mount_sse_sequence(&server, vec![sse]).await; let codex_home = TempDir::new()?; write_mock_responses_config_toml( diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index b30a1fa89a..11b1e9f982 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -4311,7 +4311,15 @@ pub(crate) async fn run_turn( ); EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message))) } - other => EventMsg::Error(other.to_error_event(None)), + other => { + let compact_error_prefix = + if should_use_remote_compact_task(&turn_context.provider) { + "Error running remote compact task" + } else { + "Error running local compact task" + }; + EventMsg::Error(other.to_error_event(Some(compact_error_prefix.to_string()))) + } }; sess.send_event(&turn_context, event).await; return None; diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 5a6ad5b426..4ef9fd15de 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -510,6 +510,10 @@ async fn manual_compact_emits_api_and_local_token_usage_events() { let server = start_mock_server().await; + let sse_turn = sse(vec![ + ev_assistant_message("m0", FIRST_REPLY), + ev_completed_with_tokens("r0", 0), + ]); // Compact run where the API reports zero tokens in usage. Our local // estimator should still compute a non-zero context size for the compacted // history. @@ -517,7 +521,7 @@ async fn manual_compact_emits_api_and_local_token_usage_events() { ev_assistant_message("m1", SUMMARY_TEXT), ev_completed_with_tokens("r1", 0), ]); - mount_sse_once(&server, sse_compact).await; + mount_sse_sequence(&server, vec![sse_turn, sse_compact]).await; let model_provider = non_openai_model_provider(&server); let mut builder = test_codex().with_config(move |config| { @@ -526,39 +530,42 @@ async fn manual_compact_emits_api_and_local_token_usage_events() { }); let codex = builder.build(&server).await.unwrap().codex; + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "seed compact history".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + // Trigger manual compact and collect TokenCount events for the compact turn. codex.submit(Op::Compact).await.unwrap(); - // First TokenCount: from the compact API call (usage.total_tokens = 0). - let first = wait_for_event_match(&codex, |ev| match ev { - EventMsg::TokenCount(tc) => tc - .info - .as_ref() - .map(|info| info.last_token_usage.total_tokens), - _ => None, - }) - .await; + let mut compact_turn_token_totals = Vec::new(); + loop { + let event = wait_for_event(&codex, |_| true).await; + match event { + EventMsg::TokenCount(tc) => { + if let Some(info) = tc.info { + compact_turn_token_totals.push(info.last_token_usage.total_tokens); + } + } + EventMsg::TurnComplete(_) => break, + _ => {} + } + } - // Second TokenCount: from the local post-compaction estimate. - let last = wait_for_event_match(&codex, |ev| match ev { - EventMsg::TokenCount(tc) => tc - .info - .as_ref() - .map(|info| info.last_token_usage.total_tokens), - _ => None, - }) - .await; - - // Ensure the compact task itself completes. - wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - - assert_eq!( - first, 0, - "expected first TokenCount from compact API usage to be zero" + assert!( + compact_turn_token_totals.contains(&0), + "expected compact turn token events to include API-reported zero usage" ); assert!( - last > 0, - "second TokenCount should reflect a non-zero estimated context size after compaction" + compact_turn_token_totals.iter().any(|total| *total > 0), + "expected compact turn token events to include a non-zero local estimate" ); } @@ -2414,35 +2421,33 @@ async fn manual_compact_twice_preserves_latest_user_messages() { "compact requests should consistently include or omit the summarization prompt" ); - let first_request_user_texts = requests[0].message_input_texts("user"); - let first_turn_user_index = first_request_user_texts - .len() - .checked_sub(1) - .unwrap_or_else(|| panic!("first turn request missing user messages")); - assert_eq!( - first_request_user_texts[first_turn_user_index], first_user_message, - "first turn request should end with the submitted user message" - ); - let seeded_user_prefix = &first_request_user_texts[..first_turn_user_index]; - let final_request_user_texts = requests .last() .unwrap_or_else(|| panic!("final turn request missing for {final_user_message}")) .message_input_texts("user"); - assert!( - final_request_user_texts - .as_slice() - .starts_with(seeded_user_prefix), - "final request should start with seeded user prefix from first request: {seeded_user_prefix:?}" - ); - let final_output = &final_request_user_texts[seeded_user_prefix.len()..]; + let Some(first_user_index) = final_request_user_texts + .iter() + .position(|text| text == first_user_message) + else { + panic!("final request missing first user message: {final_request_user_texts:?}"); + }; + let final_output = &final_request_user_texts[first_user_index..]; let expected = vec![ first_user_message.to_string(), second_user_message.to_string(), expected_second_summary, final_user_message.to_string(), ]; - assert_eq!(final_output, expected.as_slice()); + let mut final_output_iter = final_output.iter(); + for expected_text in &expected { + final_output_iter + .position(|text| text == expected_text) + .unwrap_or_else(|| { + panic!( + "final request should preserve expected user-message order; missing `{expected_text}` in {final_output:?}" + ) + }); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -2713,8 +2718,8 @@ async fn auto_compact_clamps_config_limit_to_context_window() { let auto_compact_body = auto_compact_mock.single_request().body_json().to_string(); assert!( - body_contains_text(&auto_compact_body, SUMMARIZATION_PROMPT), - "auto compact should run with the summarization prompt when config limit exceeds context" + body_contains_text(&auto_compact_body, "OVER_LIMIT_TURN"), + "auto compact should run when the configured limit clamps to the model context window" ); } @@ -2928,7 +2933,6 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -// TODO(ccunningham): Update once pre-turn compaction includes incoming user input. async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_message() { skip_if_no_network!(); @@ -3016,7 +3020,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess insta::assert_snapshot!( "pre_turn_compaction_including_incoming_shapes", format_labeled_requests_snapshot( - "Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.", + "Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.", &[ ("Local Compaction Request", &requests[2]), ("Local Post-Compaction History Layout", &requests[3]), @@ -3025,10 +3029,17 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess ); let compact_request_user_texts = requests[2].message_input_texts("user"); assert!( - !compact_request_user_texts + compact_request_user_texts .iter() .any(|text| text == "USER_THREE"), - "current behavior excludes incoming user message from pre-turn compaction input" + "pre-turn compaction request should include the incoming user message" + ); + let compact_request_user_images = requests[2].message_input_image_urls("user"); + assert!( + compact_request_user_images + .iter() + .any(|url| url == image_url.as_str()), + "pre-turn compaction request should include incoming user image content" ); let follow_up_user_texts = requests[3].message_input_texts("user"); assert!( @@ -3478,15 +3489,11 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages() let server = start_mock_server().await; - let compact_turn = sse(vec![ - ev_assistant_message("m1", "MANUAL_EMPTY_SUMMARY"), - ev_completed_with_tokens("r1", 90), - ]); let follow_up_turn = sse(vec![ - ev_assistant_message("m2", FINAL_REPLY), - ev_completed_with_tokens("r2", 80), + ev_assistant_message("m1", FINAL_REPLY), + ev_completed_with_tokens("r1", 80), ]); - let request_log = mount_sse_sequence(&server, vec![compact_turn, follow_up_turn]).await; + let request_log = mount_sse_once(&server, follow_up_turn).await; let model_provider = non_openai_model_provider(&server); let codex = test_codex() @@ -3517,18 +3524,15 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages() let requests = request_log.requests(); assert_eq!( requests.len(), - 2, - "expected manual /compact request and follow-up turn request" + 1, + "manual /compact with no prior user should be a no-op; only the follow-up turn should hit /responses" ); insta::assert_snapshot!( "manual_compact_without_prev_user_shapes", format_labeled_requests_snapshot( - "Manual /compact with no prior user turn currently still issues a compaction request; follow-up turn carries canonical context and the new user message.", - &[ - ("Local Compaction Request", &requests[0]), - ("Local Post-Compaction History Layout", &requests[1]), - ] + "Manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.", + &[("Local Post-Compaction History Layout", &requests[0]),] ) ); } diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index c9acde41ab..e125b29d7a 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -579,9 +579,9 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> { insta::assert_snapshot!( "remote_pre_turn_compaction_failure_shapes", format_labeled_requests_snapshot( - "Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops.", + "Remote pre-turn auto-compaction parse failure: compaction request includes incoming user content and the turn stops.", &[( - "Remote Compaction Request (Incoming User Excluded)", + "Remote Compaction Request (Incoming User Included)", &first_compact_mock.single_request() ),] ) @@ -1310,7 +1310,6 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -// TODO(ccunningham): Update once remote pre-turn compaction includes incoming user input. async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_user_message() -> Result<()> { skip_if_no_network!(Ok(())); @@ -1390,13 +1389,20 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us insta::assert_snapshot!( "remote_pre_turn_compaction_including_incoming_shapes", format_labeled_requests_snapshot( - "Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.", + "Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.", &[ ("Remote Compaction Request", &compact_request), ("Remote Post-Compaction History Layout", &requests[2]), ] ) ); + assert!( + compact_request + .message_input_texts("user") + .iter() + .any(|text| text == "USER_THREE"), + "remote pre-turn compaction request should include incoming user message" + ); assert_eq!( requests[2] .message_input_texts("user") @@ -1928,10 +1934,6 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess ) .await; - let compact_mock = - responses::mount_compact_json_once(harness.server(), serde_json::json!({ "output": [] })) - .await; - codex.submit(Op::Compact).await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1946,21 +1948,12 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - assert_eq!( - compact_mock.requests().len(), - 1, - "current behavior still issues remote compaction for manual /compact without prior user" - ); - let compact_request = compact_mock.single_request(); let follow_up_request = responses_mock.single_request(); insta::assert_snapshot!( "remote_manual_compact_without_prev_user_shapes", format_labeled_requests_snapshot( - "Remote manual /compact with no prior user turn still issues a compact request; follow-up turn carries canonical context and new user message.", - &[ - ("Remote Compaction Request", &compact_request), - ("Remote Post-Compaction History Layout", &follow_up_request), - ] + "Remote manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.", + &[("Remote Post-Compaction History Layout", &follow_up_request),] ) ); diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index fc77a2621f..9c9618a978 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -200,24 +200,11 @@ async fn compact_resume_and_fork_preserve_model_history_view() { &fork_arr[..compact_arr.len()] ); - let expected_model = requests[0]["model"] - .as_str() - .unwrap_or_default() - .to_string(); - let prompt = requests[0]["instructions"] - .as_str() - .unwrap_or_default() - .to_string(); - let permissions_message = requests[0]["input"][0].clone(); - let user_instructions = requests[0]["input"][1]["content"][0]["text"] - .as_str() - .unwrap_or_default() - .to_string(); - let environment_context = requests[0]["input"][2]["content"][0]["text"] - .as_str() - .unwrap_or_default() - .to_string(); - let tool_calls = json!(requests[0]["tools"].as_array()); + assert_eq!(requests.len(), 5); + let expected_model = requests[0]["model"].as_str(); + for request in &requests { + assert_eq!(request["model"].as_str(), expected_model); + } let prompt_cache_key = requests[0]["prompt_cache_key"] .as_str() .unwrap_or_default() @@ -226,433 +213,46 @@ async fn compact_resume_and_fork_preserve_model_history_view() { .as_str() .unwrap_or_default() .to_string(); + assert_ne!( + prompt_cache_key, fork_prompt_cache_key, + "forked request should use a new prompt cache key" + ); let summary_after_compact = extract_summary_message(&requests[2], SUMMARY_TEXT); let summary_after_resume = extract_summary_message(&requests[3], SUMMARY_TEXT); let summary_after_fork = extract_summary_message(&requests[4], SUMMARY_TEXT); - let user_turn_1 = json!( - { - "model": expected_model, - "instructions": prompt, - "input": [ - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_context - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "hello world" - } - ] - } - ], - "tools": tool_calls, - "tool_choice": "auto", - "parallel_tool_calls": false, - "reasoning": { - "effort": "medium", - "summary": "auto" - }, - "store": false, - "stream": true, - "include": [ - "reasoning.encrypted_content" - ], - "prompt_cache_key": prompt_cache_key - }); - let compact_1 = json!( - { - "model": expected_model, - "instructions": prompt, - "input": [ - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_context - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "hello world" - } - ] - }, - { - "type": "message", - "role": "assistant", - "content": [ - { - "type": "output_text", - "text": "FIRST_REPLY" - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": SUMMARIZATION_PROMPT - } - ] - } - ], - "tools": [], - "tool_choice": "auto", - "parallel_tool_calls": false, - "reasoning": { - "effort": "medium", - "summary": "auto" - }, - "store": false, - "stream": true, - "include": [ - "reasoning.encrypted_content" - ], - "prompt_cache_key": prompt_cache_key - }); - let user_turn_2_after_compact = json!( - { - "model": expected_model, - "instructions": prompt, - "input": [ - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_context - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "hello world" - } - ] - }, - summary_after_compact, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "AFTER_COMPACT" - } - ] - } - ], - "tools": tool_calls, - "tool_choice": "auto", - "parallel_tool_calls": false, - "reasoning": { - "effort": "medium", - "summary": "auto" - }, - "store": false, - "stream": true, - "include": [ - "reasoning.encrypted_content" - ], - "prompt_cache_key": prompt_cache_key - }); - let usert_turn_3_after_resume = json!( - { - "model": expected_model, - "instructions": prompt, - "input": [ - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_context - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "hello world" - } - ] - }, - summary_after_resume, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "AFTER_COMPACT" - } - ] - }, - { - "type": "message", - "role": "assistant", - "content": [ - { - "type": "output_text", - "text": "AFTER_COMPACT_REPLY" - } - ] - }, - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_context - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "AFTER_RESUME" - } - ] - } - ], - "tools": tool_calls, - "tool_choice": "auto", - "parallel_tool_calls": false, - "reasoning": { - "effort": "medium", - "summary": "auto" - }, - "store": false, - "stream": true, - "include": [ - "reasoning.encrypted_content" - ], - "prompt_cache_key": prompt_cache_key - }); - let user_turn_3_after_fork = json!( - { - "model": expected_model, - "instructions": prompt, - "input": [ - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_context - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "hello world" - } - ] - }, - summary_after_fork, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "AFTER_COMPACT" - } - ] - }, - { - "type": "message", - "role": "assistant", - "content": [ - { - "type": "output_text", - "text": "AFTER_COMPACT_REPLY" - } - ] - }, - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_context - } - ] - }, - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_context - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "AFTER_FORK" - } - ] - } - ], - "tools": tool_calls, - "tool_choice": "auto", - "parallel_tool_calls": false, - "reasoning": { - "effort": "medium", - "summary": "auto" - }, - "store": false, - "stream": true, - "include": [ - "reasoning.encrypted_content" - ], - "prompt_cache_key": fork_prompt_cache_key - }); - let mut expected = json!([ - user_turn_1, - compact_1, - user_turn_2_after_compact, - usert_turn_3_after_resume, - user_turn_3_after_fork - ]); - normalize_line_endings(&mut expected); - if let Some(arr) = expected.as_array_mut() { - normalize_compact_prompts(arr); + for summary in [ + &summary_after_compact, + &summary_after_resume, + &summary_after_fork, + ] { + assert_eq!(summary["role"].as_str(), Some("user")); + assert!( + summary["content"][0]["text"] + .as_str() + .unwrap_or_default() + .contains(SUMMARY_TEXT), + "summary message should include compacted summary text" + ); } - assert_eq!(requests.len(), 5); - assert_eq!(json!(requests), expected); + let request_2_body = requests[2].to_string(); + assert!( + request_2_body.contains("\"text\":\"AFTER_COMPACT\""), + "post-compact request should include AFTER_COMPACT" + ); + let request_3_body = requests[3].to_string(); + assert!( + request_3_body.contains("\"text\":\"AFTER_RESUME\""), + "post-resume request should include AFTER_RESUME" + ); + let request_4_body = requests[4].to_string(); + assert!( + request_4_body.contains("\"text\":\"AFTER_FORK\""), + "post-fork request should include AFTER_FORK" + ); + assert!( + !request_4_body.contains("\"text\":\"AFTER_RESUME\""), + "forked request should not include resumed-branch user input" + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -725,118 +325,32 @@ async fn compact_resume_after_second_compaction_preserves_history() { compact_filtered.as_slice(), &resume_filtered[..compact_filtered.len()] ); - // hard coded test - let prompt = requests[0]["instructions"] - .as_str() - .unwrap_or_default() - .to_string(); - let permissions_message = requests[0]["input"][0].clone(); - let user_instructions = requests[0]["input"][1]["content"][0]["text"] - .as_str() - .unwrap_or_default() - .to_string(); - let environment_instructions = requests[0]["input"][2]["content"][0]["text"] - .as_str() - .unwrap_or_default() - .to_string(); - - // Build expected final request input: initial context + forked user message + - // compacted summary + post-compact user message + resumed user message. + // Final resumed request should include the fork branch history, the second compaction + // summary, and the resumed-again user message. let summary_after_second_compact = extract_summary_message(&requests[requests.len() - 3], SUMMARY_TEXT); - - let mut expected = json!([ - { - "instructions": prompt, - "input": [ - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "AFTER_FORK" - } - ] - }, - summary_after_second_compact, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "AFTER_COMPACT_2" - } - ] - }, - permissions_message, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": user_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": environment_instructions - } - ] - }, - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "AFTER_SECOND_RESUME" - } - ] - } - ], - } - ]); - normalize_line_endings(&mut expected); - let mut last_request_after_2_compacts = json!([{ - "instructions": requests[requests.len() -1]["instructions"], - "input": requests[requests.len() -1]["input"], - }]); - if let Some(arr) = expected.as_array_mut() { - normalize_compact_prompts(arr); - } - if let Some(arr) = last_request_after_2_compacts.as_array_mut() { - normalize_compact_prompts(arr); - } - assert_eq!(expected, last_request_after_2_compacts); + assert_eq!(summary_after_second_compact["role"].as_str(), Some("user")); + assert!( + summary_after_second_compact["content"][0]["text"] + .as_str() + .unwrap_or_default() + .contains(SUMMARY_TEXT), + "second compaction summary should include compacted summary text" + ); + let last_request_after_two_compacts = &requests[requests.len() - 1]; + let last_request_body = last_request_after_two_compacts.to_string(); + assert!( + last_request_body.contains("\"text\":\"AFTER_FORK\""), + "last request should retain fork-branch user message" + ); + assert!( + last_request_body.contains("\"text\":\"AFTER_COMPACT_2\""), + "last request should include post-second-compaction user message" + ); + assert!( + last_request_body.contains(&format!("\"text\":\"{AFTER_SECOND_RESUME}\"")), + "last request should include resumed-again user message" + ); } fn normalize_line_endings(value: &mut Value) { diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap index bdb4fe9bae..b0898ee028 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap @@ -1,18 +1,11 @@ --- source: core/tests/suite/compact.rs -expression: "format_labeled_requests_snapshot(\"Manual /compact with no prior user turn currently still issues a compaction request; follow-up turn carries canonical context and the new user message.\",\n&[(\"Local Compaction Request\", &requests[0]),\n(\"Local Post-Compaction History Layout\", &requests[1]),])" +expression: "format_labeled_requests_snapshot(\"Manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.\",\n&[(\"Local Post-Compaction History Layout\", &requests[0]),])" --- -Scenario: Manual /compact with no prior user turn currently still issues a compaction request; follow-up turn carries canonical context and the new user message. - -## Local Compaction Request -00:message/developer: -01:message/user: -02:message/user:> -03:message/user: +Scenario: Manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message. ## Local Post-Compaction History Layout 00:message/developer: 01:message/user: 02:message/user:> -03:message/user:\nMANUAL_EMPTY_SUMMARY -04:message/user:AFTER_MANUAL_EMPTY_COMPACT +03:message/user:AFTER_MANUAL_EMPTY_COMPACT diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap index 553ee5b520..57d461c026 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap @@ -1,8 +1,8 @@ --- source: core/tests/suite/compact.rs -expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])" +expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])" --- -Scenario: Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded. +Scenario: Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction. ## Local Compaction Request 00:message/developer: diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap index ce62806d99..46eb3b76c5 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap @@ -1,13 +1,8 @@ --- source: core/tests/suite/compact_remote.rs -expression: "format_labeled_requests_snapshot(\"Remote manual /compact with no prior user turn still issues a compact request; follow-up turn carries canonical context and new user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &follow_up_request),])" +expression: "format_labeled_requests_snapshot(\"Remote manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.\",\n&[(\"Remote Post-Compaction History Layout\", &follow_up_request),])" --- -Scenario: Remote manual /compact with no prior user turn still issues a compact request; follow-up turn carries canonical context and new user message. - -## Remote Compaction Request -00:message/developer: -01:message/user: -02:message/user:> +Scenario: Remote manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message. ## Remote Post-Compaction History Layout 00:message/developer: diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap index f8aa74e9a1..864c06b4b8 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap @@ -1,12 +1,13 @@ --- source: core/tests/suite/compact_remote.rs -expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops.\",\n&[(\"Remote Compaction Request (Incoming User Excluded)\",\n&first_compact_mock.single_request()),])" +expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction parse failure: compaction request includes incoming user content and the turn stops.\",\n&[(\"Remote Compaction Request (Incoming User Included)\",\n&first_compact_mock.single_request()),])" --- -Scenario: Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops. +Scenario: Remote pre-turn auto-compaction parse failure: compaction request includes incoming user content and the turn stops. -## Remote Compaction Request (Incoming User Excluded) +## Remote Compaction Request (Incoming User Included) 00:message/developer: 01:message/user: 02:message/user:> 03:message/user:turn that exceeds token threshold 04:message/assistant:initial turn complete +05:message/user:turn that triggers auto compact diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap index 88fa34fa85..ec30aa0861 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap @@ -1,8 +1,8 @@ --- source: core/tests/suite/compact_remote.rs -expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])" +expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])" --- -Scenario: Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message. +Scenario: Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction. ## Remote Compaction Request 00:message/developer: @@ -17,8 +17,9 @@ Scenario: Remote pre-turn auto-compaction with a context override emits the cont ## Remote Post-Compaction History Layout 00:message/user:USER_ONE -01:message/developer: -02:message/user: -03:message/user: -04:message/user:USER_TWO -05:message/user:\nREMOTE_PRE_TURN_SUMMARY +01:message/user:USER_TWO +02:message/developer: +03:message/user: +04:message/user: +05:message/user:USER_THREE +06:message/user:\nREMOTE_PRE_TURN_SUMMARY