diff --git a/codex-rs/core/tests/common/context_snapshot.rs b/codex-rs/core/tests/common/context_snapshot.rs index 0b160dfbe2..ccb3007666 100644 --- a/codex-rs/core/tests/common/context_snapshot.rs +++ b/codex-rs/core/tests/common/context_snapshot.rs @@ -13,30 +13,12 @@ pub enum ContextSnapshotRenderMode { #[derive(Debug, Clone)] pub struct ContextSnapshotOptions { render_mode: ContextSnapshotRenderMode, - normalize_environment_context: bool, - text_exact_replacements: Vec<(String, String)>, - text_prefix_replacements: Vec<(String, String)>, - text_contains_replacements: Vec<(String, String)>, - cwd_contains_replacements: Vec<(String, String)>, - tool_name_replacements: Vec<(String, String)>, } impl Default for ContextSnapshotOptions { fn default() -> Self { Self { render_mode: ContextSnapshotRenderMode::RedactedText, - normalize_environment_context: true, - text_exact_replacements: Vec::new(), - text_prefix_replacements: vec![( - "# AGENTS.md instructions for ".to_string(), - "".to_string(), - )], - text_contains_replacements: vec![( - "".to_string(), - "".to_string(), - )], - cwd_contains_replacements: Vec::new(), - tool_name_replacements: Vec::new(), } } } @@ -46,61 +28,6 @@ impl ContextSnapshotOptions { self.render_mode = render_mode; self } - - pub fn normalize_environment_context(mut self, enabled: bool) -> Self { - self.normalize_environment_context = enabled; - self - } - - pub fn replace_exact_text( - mut self, - text: impl Into, - replacement: impl Into, - ) -> Self { - self.text_exact_replacements - .push((text.into(), replacement.into())); - self - } - - pub fn replace_text_with_prefix( - mut self, - prefix: impl Into, - replacement: impl Into, - ) -> Self { - self.text_prefix_replacements - .push((prefix.into(), replacement.into())); - self - } - - pub fn replace_text_containing( - mut self, - needle: impl Into, - replacement: impl Into, - ) -> Self { - self.text_contains_replacements - .push((needle.into(), replacement.into())); - self - } - - pub fn replace_cwd_when_contains( - mut self, - cwd_substring: impl Into, - replacement: impl Into, - ) -> Self { - self.cwd_contains_replacements - .push((cwd_substring.into(), replacement.into())); - self - } - - pub fn replace_tool_name( - mut self, - original_name: impl Into, - replacement: impl Into, - ) -> Self { - self.tool_name_replacements - .push((original_name.into(), replacement.into())); - self - } } pub fn request_input_shape(request: &ResponsesRequest, options: &ContextSnapshotOptions) -> String { @@ -146,32 +73,13 @@ pub fn response_items_shape(items: &[Value], options: &ContextSnapshotOptions) - } "function_call" => { let name = item.get("name").and_then(Value::as_str).unwrap_or("unknown"); - let normalized_name = options - .tool_name_replacements - .iter() - .find_map(|(original_name, replacement)| { - (original_name == name).then_some(replacement.as_str()) - }) - .unwrap_or(name); - format!("{idx:02}:function_call/{normalized_name}") + format!("{idx:02}:function_call/{name}") } "function_call_output" => { let output = item .get("output") .and_then(Value::as_str) - .map(|output| match options.render_mode { - ContextSnapshotRenderMode::RedactedText => { - if output.starts_with("unsupported call: ") - || output.starts_with("unsupported custom tool call: ") - { - "".to_string() - } else { - normalize_shape_text(output, options) - } - } - ContextSnapshotRenderMode::FullText => output.replace('\n', "\\n"), - ContextSnapshotRenderMode::KindOnly => unreachable!(), - }) + .map(|output| output.replace('\n', "\\n")) .unwrap_or_else(|| "".to_string()); format!("{idx:02}:function_call_output:{output}") } @@ -249,52 +157,10 @@ pub fn sectioned_item_shapes( } fn normalize_shape_text(text: &str, options: &ContextSnapshotOptions) -> String { - if options.render_mode == ContextSnapshotRenderMode::FullText { - return text.replace('\n', "\\n"); + match options.render_mode { + ContextSnapshotRenderMode::RedactedText | ContextSnapshotRenderMode::FullText => { + text.replace('\n', "\\n") + } + ContextSnapshotRenderMode::KindOnly => unreachable!(), } - - if let Some((_, replacement)) = options - .text_exact_replacements - .iter() - .find(|(target, _)| target == text) - { - return replacement.clone(); - } - - if let Some((_, replacement)) = options - .text_prefix_replacements - .iter() - .find(|(prefix, _)| text.starts_with(prefix.as_str())) - { - return replacement.clone(); - } - - if options.normalize_environment_context && text.starts_with("") { - let cwd = text.lines().find_map(|line| { - let trimmed = line.trim(); - let cwd = trimmed.strip_prefix("")?.strip_suffix("")?; - if let Some((_, replacement)) = options - .cwd_contains_replacements - .iter() - .find(|(needle, _)| cwd.contains(needle.as_str())) - { - return Some(replacement.clone()); - } - Some("".to_string()) - }); - return match cwd { - Some(cwd) => format!(""), - None => ">".to_string(), - }; - } - - if let Some((_, replacement)) = options - .text_contains_replacements - .iter() - .find(|(needle, _)| text.contains(needle.as_str())) - { - return replacement.clone(); - } - - text.replace('\n', "\\n") } diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 1c2ae99d96..f24122b3e6 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -22,6 +22,7 @@ use codex_protocol::openai_models::ModelsResponse; use codex_protocol::user_input::UserInput; use core_test_support::context_snapshot; use core_test_support::context_snapshot::ContextSnapshotOptions; +use core_test_support::context_snapshot::ContextSnapshotRenderMode; use core_test_support::responses::ev_local_shell_call; use core_test_support::responses::ev_reasoning_item; use core_test_support::responses::mount_models_once; @@ -192,21 +193,7 @@ async fn assert_compaction_uses_turn_lifecycle_id(codex: &std::sync::Arc ContextSnapshotOptions { - ContextSnapshotOptions::default() - .replace_exact_text(SUMMARIZATION_PROMPT, "") - .replace_cwd_when_contains( - PRETURN_CONTEXT_DIFF_CWD_MARKER, - PRETURN_CONTEXT_DIFF_CWD_MARKER, - ) - .replace_tool_name(DUMMY_FUNCTION_NAME, "") -} - -fn summary_snapshot_text(summary: &str) -> String { - summary_with_prefix(summary).replace('\n', "\\n") -} - -fn request_input_shape(request: &core_test_support::responses::ResponsesRequest) -> String { - context_snapshot::request_input_shape(request, &context_snapshot_options()) + ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::KindOnly) } fn sectioned_request_shapes( @@ -216,6 +203,13 @@ fn sectioned_request_shapes( context_snapshot::sectioned_request_shapes(scenario, sections, &context_snapshot_options()) } +fn request_contains_text( + request: &core_test_support::responses::ResponsesRequest, + text: &str, +) -> bool { + body_contains_text(&request.body_json().to_string(), text) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn summarize_context_three_requests_and_instructions() { skip_if_no_network!(); @@ -3055,8 +3049,6 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess let requests = request_log.requests(); assert_eq!(requests.len(), 4, "expected user, user, compact, follow-up"); - let compact_shape = request_input_shape(&requests[2]); - let follow_up_shape = request_input_shape(&requests[3]); insta::assert_snapshot!( "pre_turn_compaction_including_incoming_shapes", sectioned_request_shapes( @@ -3068,15 +3060,15 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess ) ); assert!( - compact_shape.contains(""), + request_contains_text(&requests[2], SUMMARIZATION_PROMPT), "expected compact request to include summarization prompt" ); assert!( - compact_shape.contains(PRETURN_CONTEXT_DIFF_CWD_MARKER), + request_contains_text(&requests[2], PRETURN_CONTEXT_DIFF_CWD_MARKER), "expected compact request to include pre-turn context diff" ); assert!( - !compact_shape.contains("USER_THREE"), + !request_contains_text(&requests[2], "USER_THREE"), "current behavior excludes incoming user message from pre-turn compaction input" ); let follow_up_has_incoming_image = requests[3].inputs_of_type("message").iter().any(|item| { @@ -3101,7 +3093,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess "expected post-compaction follow-up request to keep incoming user image content" ); assert!( - follow_up_shape.contains(&summary_snapshot_text("PRE_TURN_SUMMARY")), + request_contains_text(&requests[3], &summary_with_prefix("PRE_TURN_SUMMARY")), "expected post-compaction request to include summary text" ); } @@ -3178,7 +3170,6 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { "expected first turn and at least one compaction request" ); - let include_attempt_shape = request_input_shape(&requests[1]); insta::assert_snapshot!( "pre_turn_compaction_context_window_exceeded_shapes", sectioned_request_shapes( @@ -3191,7 +3182,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { ); assert!( - !include_attempt_shape.contains("USER_TWO"), + !request_contains_text(&requests[1], "USER_TWO"), "current behavior excludes incoming user message from pre-turn compaction input" ); assert!( @@ -3251,8 +3242,6 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() { let requests = request_log.requests(); assert_eq!(requests.len(), 3, "expected user, compact, follow-up"); - let compact_shape = request_input_shape(&requests[1]); - let follow_up_shape = request_input_shape(&requests[2]); insta::assert_snapshot!( "mid_turn_compaction_shapes", sectioned_request_shapes( @@ -3264,15 +3253,17 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() { ) ); assert!( - compact_shape.contains("function_call_output"), + !requests[1] + .inputs_of_type("function_call_output") + .is_empty(), "mid-turn compaction request should include function call output" ); assert!( - compact_shape.contains(""), + request_contains_text(&requests[1], SUMMARIZATION_PROMPT), "mid-turn compaction request should include summarization prompt" ); assert!( - follow_up_shape.contains(&summary_snapshot_text("MID_TURN_SUMMARY")), + request_contains_text(&requests[2], &summary_with_prefix("MID_TURN_SUMMARY")), "post-mid-turn compaction request should include summary text" ); } @@ -3326,8 +3317,6 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages() "expected manual /compact request and follow-up turn request" ); - let compact_shape = request_input_shape(&requests[0]); - let follow_up_shape = request_input_shape(&requests[1]); insta::assert_snapshot!( "manual_compact_without_prev_user_shapes", sectioned_request_shapes( @@ -3339,11 +3328,11 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages() ) ); assert!( - compact_shape.contains(""), + request_contains_text(&requests[0], SUMMARIZATION_PROMPT), "manual /compact request should include summarization prompt" ); assert!( - follow_up_shape.contains("AFTER_MANUAL_EMPTY_COMPACT"), + request_contains_text(&requests[1], "AFTER_MANUAL_EMPTY_COMPACT"), "follow-up request should include the submitted user message" ); } @@ -3410,8 +3399,6 @@ async fn snapshot_request_shape_manual_compact_with_previous_user_messages() { let requests = request_log.requests(); assert_eq!(requests.len(), 3, "expected user, compact, follow-up"); - let compact_shape = request_input_shape(&requests[1]); - let follow_up_shape = request_input_shape(&requests[2]); insta::assert_snapshot!( "manual_compact_with_history_shapes", sectioned_request_shapes( @@ -3423,19 +3410,19 @@ async fn snapshot_request_shape_manual_compact_with_previous_user_messages() { ) ); assert!( - compact_shape.contains("USER_ONE"), + request_contains_text(&requests[1], "USER_ONE"), "manual compact request should include existing user history" ); assert!( - compact_shape.contains(""), + request_contains_text(&requests[1], SUMMARIZATION_PROMPT), "manual compact request should include summarization prompt" ); assert!( - follow_up_shape.contains(&summary_snapshot_text("MANUAL_SUMMARY")), + request_contains_text(&requests[2], &summary_with_prefix("MANUAL_SUMMARY")), "post-compact request should include compact summary text" ); assert!( - follow_up_shape.contains("USER_TWO"), + request_contains_text(&requests[2], "USER_TWO"), "post-compact request should include the latest user message" ); } diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index d1cc704f46..8cbfcf1160 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -18,6 +18,7 @@ use codex_protocol::models::ResponseItem; use codex_protocol::user_input::UserInput; use core_test_support::context_snapshot; use core_test_support::context_snapshot::ContextSnapshotOptions; +use core_test_support::context_snapshot::ContextSnapshotRenderMode; use core_test_support::responses; use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; @@ -65,20 +66,7 @@ fn user_message_item(text: &str) -> ResponseItem { } fn context_snapshot_options() -> ContextSnapshotOptions { - ContextSnapshotOptions::default() - .replace_cwd_when_contains( - PRETURN_CONTEXT_DIFF_CWD_MARKER, - PRETURN_CONTEXT_DIFF_CWD_MARKER, - ) - .replace_tool_name(DUMMY_FUNCTION_NAME, "") -} - -fn summary_snapshot_text(summary: &str) -> String { - summary_with_prefix(summary).replace('\n', "\\n") -} - -fn request_input_shape(request: &responses::ResponsesRequest) -> String { - context_snapshot::request_input_shape(request, &context_snapshot_options()) + ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::KindOnly) } fn sectioned_request_shapes( @@ -88,6 +76,20 @@ fn sectioned_request_shapes( context_snapshot::sectioned_request_shapes(scenario, sections, &context_snapshot_options()) } +fn json_fragment(text: &str) -> String { + serde_json::to_string(text) + .expect("serialize text to JSON") + .trim_matches('"') + .to_string() +} + +fn request_contains_text(request: &responses::ResponsesRequest, text: &str) -> bool { + request + .body_json() + .to_string() + .contains(&json_fragment(text)) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_replaces_history_for_followups() -> Result<()> { skip_if_no_network!(Ok(())); @@ -1452,8 +1454,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us ); let compact_request = compact_mock.single_request(); - let compact_shape = request_input_shape(&compact_request); - let follow_up_shape = request_input_shape(&requests[2]); insta::assert_snapshot!( "remote_pre_turn_compaction_including_incoming_shapes", sectioned_request_shapes( @@ -1465,19 +1465,26 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us ) ); assert!( - compact_shape.contains(PRETURN_CONTEXT_DIFF_CWD_MARKER), + request_contains_text(&compact_request, PRETURN_CONTEXT_DIFF_CWD_MARKER), "expected remote compact request to include pre-turn context diff" ); assert!( - !compact_shape.contains("USER_THREE"), + !request_contains_text(&compact_request, "USER_THREE"), "current behavior excludes incoming user message from remote pre-turn compaction input" ); assert!( - follow_up_shape.contains(&summary_snapshot_text("REMOTE_PRE_TURN_SUMMARY")), + request_contains_text( + &requests[2], + &summary_with_prefix("REMOTE_PRE_TURN_SUMMARY") + ), "post-compaction request should include remote summary text" ); assert_eq!( - follow_up_shape.matches("USER_THREE").count(), + requests[2] + .message_input_texts("user") + .iter() + .filter(|text| text.as_str() == "USER_THREE") + .count(), 1, "post-compaction request should contain incoming user exactly once from runtime append" ); @@ -1565,7 +1572,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_failure_stops_without ); let include_attempt_request = first_compact_mock.single_request(); - let include_attempt_shape = request_input_shape(&include_attempt_request); insta::assert_snapshot!( "remote_pre_turn_compaction_failure_shapes", sectioned_request_shapes( @@ -1577,7 +1583,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_failure_stops_without ) ); assert!( - !include_attempt_shape.contains("USER_TWO"), + !request_contains_text(&include_attempt_request, "USER_TWO"), "current behavior excludes incoming user message from remote pre-turn compaction input" ); assert!( @@ -1673,7 +1679,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed ); let include_attempt_request = compact_mock.single_request(); - let include_attempt_shape = request_input_shape(&include_attempt_request); insta::assert_snapshot!( "remote_pre_turn_compaction_context_window_exceeded_shapes", sectioned_request_shapes( @@ -1685,7 +1690,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed ) ); assert!( - !include_attempt_shape.contains("USER_TWO"), + !request_contains_text(&include_attempt_request, "USER_TWO"), "current behavior excludes incoming user message from remote pre-turn compaction input" ); assert!( @@ -1755,8 +1760,6 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res ); let compact_request = compact_mock.single_request(); - let compact_shape = request_input_shape(&compact_request); - let follow_up_shape = request_input_shape(&requests[1]); insta::assert_snapshot!( "remote_mid_turn_compaction_shapes", sectioned_request_shapes( @@ -1768,11 +1771,16 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res ) ); assert!( - compact_shape.contains("function_call_output"), + !compact_request + .inputs_of_type("function_call_output") + .is_empty(), "remote mid-turn compaction request should include function call output" ); assert!( - follow_up_shape.contains(&summary_snapshot_text("REMOTE_MID_TURN_SUMMARY")), + request_contains_text( + &requests[1], + &summary_with_prefix("REMOTE_MID_TURN_SUMMARY") + ), "post-mid-turn request should include remote compaction summary text" ); @@ -1825,7 +1833,6 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess ); let compact_request = compact_mock.single_request(); let follow_up_request = responses_mock.single_request(); - let follow_up_shape = request_input_shape(&follow_up_request); insta::assert_snapshot!( "remote_manual_compact_without_prev_user_shapes", sectioned_request_shapes( @@ -1837,11 +1844,14 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess ) ); assert!( - !follow_up_shape.contains(&summary_snapshot_text("REMOTE_MANUAL_EMPTY_SUMMARY")), + !request_contains_text( + &follow_up_request, + &summary_with_prefix("REMOTE_MANUAL_EMPTY_SUMMARY"), + ), "post-compact request should not include compact summary when remote compaction is skipped" ); assert!( - follow_up_shape.contains("USER_ONE"), + request_contains_text(&follow_up_request, "USER_ONE"), "post-compact request should include the submitted user message" ); @@ -1913,8 +1923,6 @@ async fn snapshot_request_shape_remote_manual_compact_with_previous_user_message assert_eq!(requests.len(), 2, "expected user and post-compact requests"); let compact_request = compact_mock.single_request(); - let compact_shape = request_input_shape(&compact_request); - let follow_up_shape = request_input_shape(&requests[1]); insta::assert_snapshot!( "remote_manual_compact_with_history_shapes", sectioned_request_shapes( @@ -1926,15 +1934,18 @@ async fn snapshot_request_shape_remote_manual_compact_with_previous_user_message ) ); assert!( - compact_shape.contains("USER_ONE"), + request_contains_text(&compact_request, "USER_ONE"), "remote compaction request should include existing user history" ); assert!( - follow_up_shape.contains("USER_TWO"), + request_contains_text(&requests[1], "USER_TWO"), "post-compact request should include latest user message" ); assert!( - follow_up_shape.contains(&summary_snapshot_text("REMOTE_MANUAL_WITH_HISTORY_SUMMARY")), + request_contains_text( + &requests[1], + &summary_with_prefix("REMOTE_MANUAL_WITH_HISTORY_SUMMARY"), + ), "post-compact request should include remote compact summary text" );