diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 0acf8fcbe9..3a940deff1 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -4502,12 +4502,8 @@ fn sandbox_policy_metadata_mapping_is_stable() { ); } -async fn setup_tool_call_metadata_runtime_test() -> ( - Arc, - Arc, - async_channel::Receiver, - codex_protocol::models::SandboxPolicyMetadata, -) { +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tool_call_metadata_stamps_escalated_review_decision_when_feature_enabled() { let (mut sess, tc, rx) = make_session_and_context_with_rx().await; let expected_sandbox_policy = sandbox_policy_to_metadata(tc.sandbox_policy.get()); Arc::get_mut(&mut sess) @@ -4530,27 +4526,27 @@ async fn setup_tool_call_metadata_runtime_test() -> ( .await; while rx.try_recv().is_ok() {} - (sess, tc, rx, expected_sandbox_policy) -} + sess.record_call_approval_outcome( + "call-1".to_string(), + ApprovalOutcomeMetadata::reviewed( + &ReviewDecision::Denied, + codex_protocol::models::ApprovalSourceMetadata::User, + ), + ) + .await; + sess.record_response_item_and_emit_turn_item( + tc.as_ref(), + ResponseItem::FunctionCall { + id: None, + name: "shell".to_string(), + namespace: None, + arguments: "{}".to_string(), + call_id: "call-1".to_string(), + metadata: None, + }, + ) + .await; -fn function_call_item(call_id: &str) -> ResponseItem { - ResponseItem::FunctionCall { - id: None, - name: "shell".to_string(), - namespace: None, - arguments: "{}".to_string(), - call_id: call_id.to_string(), - metadata: None, - } -} - -async fn assert_next_emitted_function_call_metadata( - rx: &async_channel::Receiver, - expected_sandbox_policy: codex_protocol::models::SandboxPolicyMetadata, - expected_escalated: bool, - expected_review_decision: Option, - expected_approval_source: Option, -) { let event = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv()) .await .expect("expected raw response item event") @@ -4563,51 +4559,96 @@ async fn assert_next_emitted_function_call_metadata( ResponseItem::FunctionCall { metadata: Some(metadata), .. - } if metadata.is_tool_call_escalated == Some(expected_escalated) - && metadata.review_decision == expected_review_decision - && metadata.approval_source == expected_approval_source + } if metadata.is_tool_call_escalated == Some(true) + && metadata.review_decision + == Some(codex_protocol::models::ReviewDecisionMetadata::Denied) + && metadata.approval_source + == Some(codex_protocol::models::ApprovalSourceMetadata::User) && metadata.sandbox_policy == Some(expected_sandbox_policy) ) )); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn tool_call_metadata_stamps_escalated_review_decision_when_feature_enabled() { - let (sess, tc, rx, expected_sandbox_policy) = setup_tool_call_metadata_runtime_test().await; - - sess.record_call_approval_outcome( - "call-1".to_string(), - ApprovalOutcomeMetadata::reviewed( - &ReviewDecision::Denied, - codex_protocol::models::ApprovalSourceMetadata::User, - ), - ) - .await; - sess.record_response_item_and_emit_turn_item(tc.as_ref(), function_call_item("call-1")) - .await; - assert_next_emitted_function_call_metadata( - &rx, - expected_sandbox_policy, - true, - Some(codex_protocol::models::ReviewDecisionMetadata::Denied), - Some(codex_protocol::models::ApprovalSourceMetadata::User), - ) - .await; -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tool_call_metadata_stamps_non_escalated_false_when_feature_enabled() { - let (sess, tc, rx, expected_sandbox_policy) = setup_tool_call_metadata_runtime_test().await; + let (mut sess, tc, rx) = make_session_and_context_with_rx().await; + let expected_sandbox_policy = sandbox_policy_to_metadata(tc.sandbox_policy.get()); + Arc::get_mut(&mut sess) + .expect("session should be uniquely owned in this test") + .features + .enable(crate::features::Feature::ItemMetadata) + .expect("feature flag should be enabled for this test"); - sess.record_response_item_and_emit_turn_item(tc.as_ref(), function_call_item("call-2")) - .await; - assert_next_emitted_function_call_metadata(&rx, expected_sandbox_policy, false, None, None) - .await; + sess.spawn_task( + Arc::clone(&tc), + vec![UserInput::Text { + text: "start".to_string(), + text_elements: Vec::new(), + }], + NeverEndingTask { + kind: TaskKind::Regular, + listen_to_cancellation_token: false, + }, + ) + .await; + while rx.try_recv().is_ok() {} + + sess.record_response_item_and_emit_turn_item( + tc.as_ref(), + ResponseItem::FunctionCall { + id: None, + name: "shell".to_string(), + namespace: None, + arguments: "{}".to_string(), + call_id: "call-2".to_string(), + metadata: None, + }, + ) + .await; + + let event = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv()) + .await + .expect("expected raw response item event") + .expect("channel open"); + assert!(matches!( + event.msg, + EventMsg::RawResponseItem(ref ev) + if matches!( + &ev.item, + ResponseItem::FunctionCall { + metadata: Some(metadata), + .. + } if metadata.is_tool_call_escalated == Some(false) + && metadata.review_decision.is_none() + && metadata.approval_source.is_none() + && metadata.sandbox_policy == Some(expected_sandbox_policy) + ) + )); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tool_call_metadata_stamps_guardian_direct_review_when_feature_enabled() { - let (sess, tc, rx, expected_sandbox_policy) = setup_tool_call_metadata_runtime_test().await; + let (mut sess, tc, rx) = make_session_and_context_with_rx().await; + let expected_sandbox_policy = sandbox_policy_to_metadata(tc.sandbox_policy.get()); + Arc::get_mut(&mut sess) + .expect("session should be uniquely owned in this test") + .features + .enable(crate::features::Feature::ItemMetadata) + .expect("feature flag should be enabled for this test"); + + sess.spawn_task( + Arc::clone(&tc), + vec![UserInput::Text { + text: "start".to_string(), + text_elements: Vec::new(), + }], + NeverEndingTask { + kind: TaskKind::Regular, + listen_to_cancellation_token: false, + }, + ) + .await; + while rx.try_recv().is_ok() {} sess.record_call_approval_outcome( "call-guardian-runtime-1".to_string(), @@ -4619,22 +4660,62 @@ async fn tool_call_metadata_stamps_guardian_direct_review_when_feature_enabled() .await; sess.record_response_item_and_emit_turn_item( tc.as_ref(), - function_call_item("call-guardian-runtime-1"), - ) - .await; - assert_next_emitted_function_call_metadata( - &rx, - expected_sandbox_policy, - true, - Some(codex_protocol::models::ReviewDecisionMetadata::Denied), - Some(codex_protocol::models::ApprovalSourceMetadata::Guardian), + ResponseItem::FunctionCall { + id: None, + name: "shell".to_string(), + namespace: None, + arguments: "{}".to_string(), + call_id: "call-guardian-runtime-1".to_string(), + metadata: None, + }, ) .await; + + let event = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv()) + .await + .expect("expected raw response item event") + .expect("channel open"); + assert!(matches!( + event.msg, + EventMsg::RawResponseItem(ref ev) + if matches!( + &ev.item, + ResponseItem::FunctionCall { + metadata: Some(metadata), + .. + } if metadata.is_tool_call_escalated == Some(true) + && metadata.review_decision + == Some(codex_protocol::models::ReviewDecisionMetadata::Denied) + && metadata.approval_source + == Some(codex_protocol::models::ApprovalSourceMetadata::Guardian) + && metadata.sandbox_policy == Some(expected_sandbox_policy) + ) + )); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tool_call_metadata_stamps_policy_source_without_review_decision_when_feature_enabled() { - let (sess, tc, rx, expected_sandbox_policy) = setup_tool_call_metadata_runtime_test().await; + let (mut sess, tc, rx) = make_session_and_context_with_rx().await; + let expected_sandbox_policy = sandbox_policy_to_metadata(tc.sandbox_policy.get()); + Arc::get_mut(&mut sess) + .expect("session should be uniquely owned in this test") + .features + .enable(crate::features::Feature::ItemMetadata) + .expect("feature flag should be enabled for this test"); + + sess.spawn_task( + Arc::clone(&tc), + vec![UserInput::Text { + text: "start".to_string(), + text_elements: Vec::new(), + }], + NeverEndingTask { + kind: TaskKind::Regular, + listen_to_cancellation_token: false, + }, + ) + .await; + while rx.try_recv().is_ok() {} sess.record_call_approval_outcome( "call-policy-runtime-1".to_string(), @@ -4646,17 +4727,36 @@ async fn tool_call_metadata_stamps_policy_source_without_review_decision_when_fe .await; sess.record_response_item_and_emit_turn_item( tc.as_ref(), - function_call_item("call-policy-runtime-1"), - ) - .await; - assert_next_emitted_function_call_metadata( - &rx, - expected_sandbox_policy, - true, - None, - Some(codex_protocol::models::ApprovalSourceMetadata::Policy), + ResponseItem::FunctionCall { + id: None, + name: "shell".to_string(), + namespace: None, + arguments: "{}".to_string(), + call_id: "call-policy-runtime-1".to_string(), + metadata: None, + }, ) .await; + + let event = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv()) + .await + .expect("expected raw response item event") + .expect("channel open"); + assert!(matches!( + event.msg, + EventMsg::RawResponseItem(ref ev) + if matches!( + &ev.item, + ResponseItem::FunctionCall { + metadata: Some(metadata), + .. + } if metadata.is_tool_call_escalated == Some(true) + && metadata.review_decision.is_none() + && metadata.approval_source + == Some(codex_protocol::models::ApprovalSourceMetadata::Policy) + && metadata.sandbox_policy == Some(expected_sandbox_policy) + ) + )); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -4794,87 +4894,6 @@ async fn tool_call_metadata_can_be_restamped_after_approval_outcome() { )); } -#[tokio::test] -async fn tool_call_metadata_snapshot_stamps_guardian_approval_source() { - let (_session, turn_context_raw) = make_session_and_context().await; - let turn_context = Arc::new(turn_context_raw); - let expected_sandbox_policy = sandbox_policy_to_metadata(turn_context.sandbox_policy.get()); - let item = ResponseItem::FunctionCall { - id: None, - name: "shell".to_string(), - namespace: None, - arguments: "{}".to_string(), - call_id: "call-guardian-1".to_string(), - metadata: None, - }; - let snapshot = ToolApprovalMetadataSnapshot { - approval_outcomes_by_call_id: HashMap::from([( - "call-guardian-1".to_string(), - ApprovalOutcomeMetadata { - review_decision: Some(codex_protocol::models::ReviewDecisionMetadata::Denied), - approval_source: codex_protocol::models::ApprovalSourceMetadata::Guardian, - }, - )]), - pending_approval_call_ids: HashSet::new(), - }; - - let stamped = - stamp_tool_approval_metadata_with_snapshot(turn_context.as_ref(), item, Some(&snapshot)); - - assert!(matches!( - stamped, - ResponseItem::FunctionCall { - metadata: Some(metadata), - .. - } if metadata.is_tool_call_escalated == Some(true) - && metadata.review_decision - == Some(codex_protocol::models::ReviewDecisionMetadata::Denied) - && metadata.approval_source - == Some(codex_protocol::models::ApprovalSourceMetadata::Guardian) - && metadata.sandbox_policy == Some(expected_sandbox_policy) - )); -} - -#[tokio::test] -async fn tool_call_metadata_snapshot_stamps_policy_approval_source_without_review_decision() { - let (_session, turn_context_raw) = make_session_and_context().await; - let turn_context = Arc::new(turn_context_raw); - let expected_sandbox_policy = sandbox_policy_to_metadata(turn_context.sandbox_policy.get()); - let item = ResponseItem::FunctionCall { - id: None, - name: "shell".to_string(), - namespace: None, - arguments: "{}".to_string(), - call_id: "call-policy-1".to_string(), - metadata: None, - }; - let snapshot = ToolApprovalMetadataSnapshot { - approval_outcomes_by_call_id: HashMap::from([( - "call-policy-1".to_string(), - ApprovalOutcomeMetadata { - review_decision: None, - approval_source: codex_protocol::models::ApprovalSourceMetadata::Policy, - }, - )]), - pending_approval_call_ids: HashSet::new(), - }; - - let stamped = - stamp_tool_approval_metadata_with_snapshot(turn_context.as_ref(), item, Some(&snapshot)); - - assert!(matches!( - stamped, - ResponseItem::FunctionCall { - metadata: Some(metadata), - .. - } if metadata.is_tool_call_escalated == Some(true) - && metadata.review_decision.is_none() - && metadata.approval_source - == Some(codex_protocol::models::ApprovalSourceMetadata::Policy) - && metadata.sandbox_policy == Some(expected_sandbox_policy) - )); -} - #[tokio::test] async fn steer_input_requires_active_turn() { let (sess, _tc, _rx) = make_session_and_context_with_rx().await;