From e3786d3aaeb1c2c233c3fa98d8d65778dacdbd48 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Tue, 21 Apr 2026 11:36:03 -0700 Subject: [PATCH] integration test Co-authored-by: Codex --- codex-rs/core/src/guardian/tests.rs | 118 ++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index e7590af051..189e090083 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::compact::SUMMARIZATION_PROMPT; use crate::config::Config; use crate::config::ConfigOverrides; use crate::config::Constrained; @@ -6,6 +7,7 @@ use crate::config::ManagedFeatures; use crate::config::NetworkProxySpec; use crate::config::test_config; use crate::guardian::approval_request::guardian_request_target_item_id; +use crate::guardian::review::proactively_sync_guardian_trunk; use crate::session::session::Session; use crate::session::turn_context::TurnContext; use crate::test_support; @@ -44,6 +46,7 @@ use core_test_support::context_snapshot; use core_test_support::context_snapshot::ContextSnapshotOptions; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_completed_with_tokens; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_response_once; use core_test_support::responses::mount_sse_once; @@ -145,12 +148,22 @@ async fn guardian_test_session_and_turn( async fn guardian_test_session_and_turn_with_base_url( base_url: &str, +) -> (Arc, Arc) { + guardian_test_session_and_turn_with_config(base_url, |_| {}).await +} + +async fn guardian_test_session_and_turn_with_config( + base_url: &str, + configure: impl FnOnce(&mut Config), ) -> (Arc, Arc) { let (mut session, mut turn) = crate::session::tests::make_session_and_context().await; session.conversation_id = fixed_guardian_parent_session_id(); let mut config = (*turn.config).clone(); config.model_provider.base_url = Some(format!("{base_url}/v1")); config.user_instructions = None; + configure(&mut config); + turn.approval_policy = config.permissions.approval_policy.clone(); + turn.compact_prompt = config.compact_prompt.clone(); let config = Arc::new(config); let models_manager = test_support::models_manager_with_provider( config.codex_home.to_path_buf(), @@ -1743,6 +1756,111 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn proactive_guardian_sync_compacts_trunk_before_review_request() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let compact_summary = "GUARDIAN_COMPACT_SUMMARY"; + let guardian_assessment = serde_json::json!({ + "risk_level": "low", + "user_authorization": "high", + "outcome": "allow", + "rationale": "The action is narrow and explicitly requested.", + }) + .to_string(); + let request_log = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-guardian-compact"), + ev_assistant_message("msg-guardian-compact", compact_summary), + ev_completed_with_tokens("resp-guardian-compact", /*total_tokens*/ 10), + ]), + sse(vec![ + ev_response_created("resp-guardian-review"), + ev_assistant_message("msg-guardian-review", &guardian_assessment), + ev_completed("resp-guardian-review"), + ]), + ], + ) + .await; + + let (session, turn) = + guardian_test_session_and_turn_with_config(server.uri().as_str(), |config| { + config.approvals_reviewer = ApprovalsReviewer::GuardianSubagent; + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.model_provider.name = "OpenAI-compatible guardian compact test".to_string(); + config.compact_prompt = Some(SUMMARIZATION_PROMPT.to_string()); + config.model_auto_compact_token_limit = Some(1); + }) + .await; + seed_guardian_parent_history(&session, &turn).await; + + proactively_sync_guardian_trunk(&session, &turn).await; + + let sync_requests = request_log.requests(); + assert_eq!( + sync_requests.len(), + 1, + "proactive transcript sync should compact before any approval review request" + ); + assert!( + sync_requests[0].body_contains_text(SUMMARIZATION_PROMPT), + "proactive sync should trigger a compact request" + ); + assert!( + sync_requests[0] + .body_contains_text("Transcript sync only. No approval decision is requested"), + "compact request should include the synced guardian transcript" + ); + + let outcome = run_guardian_review_session_for_test( + Arc::clone(&session), + Arc::clone(&turn), + GuardianApprovalRequest::Shell { + id: "shell-after-proactive-compact".to_string(), + command: vec!["git".to_string(), "push".to_string()], + cwd: test_path_buf("/repo/codex-rs/core").abs(), + sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault, + additional_permissions: None, + justification: Some("Need to push the reviewed docs fix.".to_string()), + }, + /*retry_reason*/ None, + guardian_output_schema(), + /*external_cancel*/ None, + ) + .await; + let GuardianReviewOutcome::Completed(Ok(assessment)) = outcome else { + panic!("expected guardian assessment"); + }; + assert_eq!(assessment.outcome, GuardianAssessmentOutcome::Allow); + + let requests = request_log.requests(); + assert_eq!( + requests.len(), + 2, + "expected exactly one proactive compact request and one later guardian review request" + ); + let compact_request = &requests[0]; + let review_request = &requests[1]; + assert!(compact_request.body_contains_text(SUMMARIZATION_PROMPT)); + assert!( + review_request.body_contains_text(compact_summary), + "review request should use compacted guardian trunk history" + ); + assert!( + review_request.body_contains_text(">>> APPROVAL REQUEST START"), + "second request should be the approval review" + ); + assert!( + !review_request.body_contains_text(SUMMARIZATION_PROMPT), + "approval review should not do request-time compaction after proactive compaction succeeded" + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() -> anyhow::Result<()> { skip_if_no_network!(Ok(()));