use super::*; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use pretty_assertions::assert_eq; async fn process_compacted_history_with_test_session( compacted_history: Vec, previous_turn_settings: Option<&PreviousTurnSettings>, ) -> (Vec, Vec) { let (session, turn_context) = crate::session::tests::make_session_and_context().await; session .set_previous_turn_settings(previous_turn_settings.cloned()) .await; let initial_context = session.build_initial_context(&turn_context).await; let refreshed = crate::compact_remote::process_compacted_history( &session, &turn_context, compacted_history, InitialContextInjection::BeforeLastUserMessage, ) .await; (refreshed, initial_context) } #[test] fn content_items_to_text_joins_non_empty_segments() { let items = vec![ ContentItem::InputText { text: "hello".to_string(), }, ContentItem::OutputText { text: String::new(), }, ContentItem::OutputText { text: "world".to_string(), }, ]; let joined = content_items_to_text(&items); assert_eq!(Some("hello\nworld".to_string()), joined); } #[test] fn content_items_to_text_ignores_image_only_content() { let items = vec![ContentItem::InputImage { image_url: "file://image.png".to_string(), }]; let joined = content_items_to_text(&items); assert_eq!(None, joined); } #[test] fn collect_user_messages_extracts_user_text_only() { let items = vec![ ResponseItem::Message { id: Some("assistant".to_string()), role: "assistant".to_string(), content: vec![ContentItem::OutputText { text: "ignored".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: Some("user".to_string()), role: "user".to_string(), content: vec![ContentItem::InputText { text: "first".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Other, ]; let collected = collect_user_messages(&items); assert_eq!(vec!["first".to_string()], collected); } #[test] fn collect_user_messages_filters_session_prefix_entries() { let items = vec![ ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: r#"# AGENTS.md instructions for project do things "# .to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "cwd=/tmp".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "real user message".to_string(), }], end_turn: None, phase: None, }, ]; let collected = collect_user_messages(&items); assert_eq!(vec!["real user message".to_string()], collected); } #[test] fn build_token_limited_compacted_history_truncates_overlong_user_messages() { // Use a small truncation limit so the test remains fast while still validating // that oversized user content is truncated. let max_tokens = 16; let big = "word ".repeat(200); let history = super::build_compacted_history_with_limit( Vec::new(), std::slice::from_ref(&big), "SUMMARY", max_tokens, ); assert_eq!(history.len(), 2); let truncated_message = &history[0]; let summary_message = &history[1]; let truncated_text = match truncated_message { ResponseItem::Message { role, content, .. } if role == "user" => { content_items_to_text(content).unwrap_or_default() } other => panic!("unexpected item in history: {other:?}"), }; assert!( truncated_text.contains("tokens truncated"), "expected truncation marker in truncated user message" ); assert!( !truncated_text.contains(&big), "truncated user message should not include the full oversized user text" ); let summary_text = match summary_message { ResponseItem::Message { role, content, .. } if role == "user" => { content_items_to_text(content).unwrap_or_default() } other => panic!("unexpected item in history: {other:?}"), }; assert_eq!(summary_text, "SUMMARY"); } #[test] fn build_token_limited_compacted_history_appends_summary_message() { let initial_context: Vec = Vec::new(); let user_messages = vec!["first user message".to_string()]; let summary_text = "summary text"; let history = build_compacted_history(initial_context, &user_messages, summary_text); assert!( !history.is_empty(), "expected compacted history to include summary" ); let last = history.last().expect("history should have a summary entry"); let summary = match last { ResponseItem::Message { role, content, .. } if role == "user" => { content_items_to_text(content).unwrap_or_default() } other => panic!("expected summary message, found {other:?}"), }; assert_eq!(summary, summary_text); } #[test] fn should_use_remote_compact_task_for_azure_provider() { let provider = ModelProviderInfo { name: "Azure".into(), base_url: Some("https://example.com/openai".into()), env_key: Some("AZURE_OPENAI_API_KEY".into()), env_key_instructions: None, experimental_bearer_token: None, auth: None, wire_api: WireApi::Responses, query_params: None, http_headers: None, env_http_headers: None, request_max_retries: None, stream_max_retries: None, stream_idle_timeout_ms: None, websocket_connect_timeout_ms: None, requires_openai_auth: false, supports_websockets: false, }; assert!(should_use_remote_compact_task(&provider)); } #[tokio::test] async fn process_compacted_history_replaces_developer_messages() { let compacted_history = vec![ ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: "stale permissions".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "summary".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: "stale personality".to_string(), }], end_turn: None, phase: None, }, ]; let (refreshed, mut expected) = process_compacted_history_with_test_session( compacted_history, /*previous_turn_settings*/ None, ) .await; expected.push(ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "summary".to_string(), }], end_turn: None, phase: None, }); assert_eq!(refreshed, expected); } #[tokio::test] async fn process_compacted_history_reinjects_full_initial_context() { let compacted_history = vec![ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "summary".to_string(), }], end_turn: None, phase: None, }]; let (refreshed, mut expected) = process_compacted_history_with_test_session( compacted_history, /*previous_turn_settings*/ None, ) .await; expected.push(ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "summary".to_string(), }], end_turn: None, phase: None, }); assert_eq!(refreshed, expected); } #[tokio::test] async fn process_compacted_history_drops_non_user_content_messages() { let compacted_history = vec![ ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: r#"# AGENTS.md instructions for /repo keep me updated "# .to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: r#" /repo zsh "# .to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: r#" turn-1 interrupted "# .to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "summary".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: "stale developer instructions".to_string(), }], end_turn: None, phase: None, }, ]; let (refreshed, mut expected) = process_compacted_history_with_test_session( compacted_history, /*previous_turn_settings*/ None, ) .await; expected.push(ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "summary".to_string(), }], end_turn: None, phase: None, }); assert_eq!(refreshed, expected); } #[tokio::test] async fn process_compacted_history_inserts_context_before_last_real_user_message_only() { let compacted_history = vec![ ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "older user".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: format!("{SUMMARY_PREFIX}\nsummary text"), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "latest user".to_string(), }], end_turn: None, phase: None, }, ]; let (refreshed, initial_context) = process_compacted_history_with_test_session( compacted_history, /*previous_turn_settings*/ None, ) .await; let mut expected = vec![ ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "older user".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: format!("{SUMMARY_PREFIX}\nsummary text"), }], end_turn: None, phase: None, }, ]; expected.extend(initial_context); expected.push(ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "latest user".to_string(), }], end_turn: None, phase: None, }); assert_eq!(refreshed, expected); } #[tokio::test] async fn process_compacted_history_reinjects_model_switch_message() { let compacted_history = vec![ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "summary".to_string(), }], end_turn: None, phase: None, }]; let previous_turn_settings = PreviousTurnSettings { model: "previous-regular-model".to_string(), realtime_active: None, }; let (refreshed, initial_context) = process_compacted_history_with_test_session( compacted_history, Some(&previous_turn_settings), ) .await; let ResponseItem::Message { role, content, .. } = &initial_context[0] else { panic!("expected developer message"); }; assert_eq!(role, "developer"); let [ContentItem::InputText { text }, ..] = content.as_slice() else { panic!("expected developer text"); }; assert!(text.contains("")); let mut expected = initial_context; expected.push(ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "summary".to_string(), }], end_turn: None, phase: None, }); assert_eq!(refreshed, expected); } #[test] fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() { let compacted_history = vec![ ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "older user".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "latest user".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: format!("{SUMMARY_PREFIX}\nsummary text"), }], end_turn: None, phase: None, }, ]; let initial_context = vec![ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: "fresh permissions".to_string(), }], end_turn: None, phase: None, }]; let refreshed = insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context); let expected = vec![ ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "older user".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: "fresh permissions".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "latest user".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: format!("{SUMMARY_PREFIX}\nsummary text"), }], end_turn: None, phase: None, }, ]; assert_eq!(refreshed, expected); } #[test] fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last() { let compacted_history = vec![ResponseItem::Compaction { encrypted_content: "encrypted".to_string(), }]; let initial_context = vec![ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: "fresh permissions".to_string(), }], end_turn: None, phase: None, }]; let refreshed = insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context); let expected = vec![ ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: "fresh permissions".to_string(), }], end_turn: None, phase: None, }, ResponseItem::Compaction { encrypted_content: "encrypted".to_string(), }, ]; assert_eq!(refreshed, expected); }