diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs index 7bec14cd2f..0697506702 100644 --- a/codex-rs/cloud-tasks/src/lib.rs +++ b/codex-rs/cloud-tasks/src/lib.rs @@ -2144,14 +2144,7 @@ mod tests { use codex_cloud_tasks_client::TaskStatus; use codex_cloud_tasks_client::TaskSummary; use codex_cloud_tasks_mock_client::MockClient; - use codex_tui::ComposerAction; - use codex_tui::ComposerInput; - use crossterm::event::KeyCode; - use crossterm::event::KeyEvent; - use crossterm::event::KeyModifiers; use pretty_assertions::assert_eq; - use ratatui::buffer::Buffer; - use ratatui::layout::Rect; struct StubGitInfo { default_branch: Option, @@ -2377,33 +2370,4 @@ mod tests { assert_eq!(url.0, "task_i_123456"); assert!(parse_task_id(" ").is_err()); } - - #[test] - #[ignore = "very slow"] - fn composer_input_renders_typed_characters() { - let mut composer = ComposerInput::new(); - let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE); - match composer.input(key) { - ComposerAction::Submitted(_) => panic!("unexpected submission"), - ComposerAction::None => {} - } - - let area = Rect::new(0, 0, 20, 5); - let mut buf = Buffer::empty(area); - composer.render_ref(area, &mut buf); - - let found = buf.content().iter().any(|cell| cell.symbol() == "a"); - assert!(found, "typed character was not rendered: {buf:?}"); - - composer.set_hint_items(vec![("⌃O", "env"), ("⌃C", "quit")]); - composer.render_ref(area, &mut buf); - let footer = buf - .content() - .iter() - .skip((area.width as usize) * (area.height as usize - 1)) - .map(ratatui::buffer::Cell::symbol) - .collect::>() - .join(""); - assert!(footer.contains("⌃O env")); - } } diff --git a/codex-rs/core/tests/suite/codex_delegate.rs b/codex-rs/core/tests/suite/codex_delegate.rs index 98d9548e0e..c0b437de53 100644 --- a/codex-rs/core/tests/suite/codex_delegate.rs +++ b/codex-rs/core/tests/suite/codex_delegate.rs @@ -1,16 +1,8 @@ -use codex_core::config::Constrained; -use codex_core::sandboxing::SandboxPermissions; -use codex_protocol::models::PermissionProfile; -use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget; -use core_test_support::responses::ev_apply_patch_custom_tool_call; -use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; -use core_test_support::responses::ev_function_call; use core_test_support::responses::ev_reasoning_item_added; use core_test_support::responses::ev_reasoning_summary_text_delta; use core_test_support::responses::ev_response_created; @@ -22,180 +14,6 @@ use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; -/// Delegate should surface ExecApprovalRequest from sub-agent and proceed -/// after parent submits an approval decision. -#[ignore = "TODO once we have a delegate that can ask for approvals"] -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() { - skip_if_no_network!(); - - // Sub-agent turn 1: emit a shell_command function_call requiring approval, then complete. - let call_id = "call-exec-1"; - let args = serde_json::json!({ - "command": "rm -rf delegated", - "timeout_ms": 1000, - "sandbox_permissions": SandboxPermissions::RequireEscalated, - }) - .to_string(); - let sse1 = sse(vec![ - ev_response_created("resp-1"), - ev_function_call(call_id, "shell_command", &args), - ev_completed("resp-1"), - ]); - - // Sub-agent turn 2: return structured review output and complete. - let review_json = serde_json::json!({ - "findings": [], - "overall_correctness": "ok", - "overall_explanation": "delegate approved exec", - "overall_confidence_score": 0.5 - }) - .to_string(); - let sse2 = sse(vec![ - ev_response_created("resp-2"), - ev_assistant_message("msg-1", &review_json), - ev_completed("resp-2"), - ]); - - let server = start_mock_server().await; - mount_sse_sequence(&server, vec![sse1, sse2]).await; - - // Build a conversation configured to require approvals so the delegate - // routes ExecApprovalRequest via the parent. - let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { - config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config - .permissions - .set_permission_profile(PermissionProfile::read_only()) - .expect("set permission profile"); - }); - let test = builder.build(&server).await.expect("build test codex"); - - // Kick off review (sub-agent starts internally). - test.codex - .submit(Op::Review { - review_request: ReviewRequest { - target: ReviewTarget::Custom { - instructions: "Please review".to_string(), - }, - user_facing_hint: None, - }, - }) - .await - .expect("submit review"); - - // Lifecycle: Entered -> ExecApprovalRequest -> Exited(Some) -> TurnComplete. - wait_for_event(&test.codex, |ev| { - matches!(ev, EventMsg::EnteredReviewMode(_)) - }) - .await; - - // Expect parent-side approval request (forwarded by delegate). - let approval_event = wait_for_event(&test.codex, |ev| { - matches!(ev, EventMsg::ExecApprovalRequest(_)) - }) - .await; - let EventMsg::ExecApprovalRequest(approval) = approval_event else { - panic!("expected ExecApprovalRequest event"); - }; - - // Approve via parent using the emitted approval call ID. - test.codex - .submit(Op::ExecApproval { - id: approval.effective_approval_id(), - turn_id: None, - decision: ReviewDecision::Approved, - }) - .await - .expect("submit exec approval"); - - wait_for_event(&test.codex, |ev| { - matches!(ev, EventMsg::ExitedReviewMode(_)) - }) - .await; - wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; -} - -/// Delegate should surface ApplyPatchApprovalRequest and honor parent decision -/// so the sub-agent can proceed to completion. -#[ignore = "TODO once we have a delegate that can ask for approvals"] -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() { - skip_if_no_network!(); - - let call_id = "call-patch-1"; - let patch = "*** Begin Patch\n*** Add File: delegated.txt\n+hello\n*** End Patch\n"; - let sse1 = sse(vec![ - ev_response_created("resp-1"), - ev_apply_patch_custom_tool_call(call_id, patch), - ev_completed("resp-1"), - ]); - let review_json = serde_json::json!({ - "findings": [], - "overall_correctness": "ok", - "overall_explanation": "delegate patch handled", - "overall_confidence_score": 0.5 - }) - .to_string(); - let sse2 = sse(vec![ - ev_response_created("resp-2"), - ev_assistant_message("msg-1", &review_json), - ev_completed("resp-2"), - ]); - - let server = start_mock_server().await; - mount_sse_sequence(&server, vec![sse1, sse2]).await; - - let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { - config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - // Use a restricted sandbox so patch approval is required - config - .permissions - .set_permission_profile(PermissionProfile::read_only()) - .expect("set permission profile"); - }); - let test = builder.build(&server).await.expect("build test codex"); - - test.codex - .submit(Op::Review { - review_request: ReviewRequest { - target: ReviewTarget::Custom { - instructions: "Please review".to_string(), - }, - user_facing_hint: None, - }, - }) - .await - .expect("submit review"); - - wait_for_event(&test.codex, |ev| { - matches!(ev, EventMsg::EnteredReviewMode(_)) - }) - .await; - let approval_event = wait_for_event(&test.codex, |ev| { - matches!(ev, EventMsg::ApplyPatchApprovalRequest(_)) - }) - .await; - let EventMsg::ApplyPatchApprovalRequest(approval) = approval_event else { - panic!("expected ApplyPatchApprovalRequest event"); - }; - - // Deny via parent so delegate can continue, using the emitted approval call ID. - test.codex - .submit(Op::PatchApproval { - id: approval.call_id, - decision: ReviewDecision::denied("rejected by user"), - }) - .await - .expect("submit patch approval"); - - wait_for_event(&test.codex, |ev| { - matches!(ev, EventMsg::ExitedReviewMode(_)) - }) - .await; - wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn codex_delegate_ignores_legacy_deltas() { skip_if_no_network!(); diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 62b10cf424..7ede6f7216 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -3484,154 +3484,6 @@ async fn write_stdin_calls_run_in_parallel_across_sessions() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore] -async fn unified_exec_prunes_exited_sessions_first() -> Result<()> { - skip_if_no_network!(Ok(())); - skip_if_sandbox!(Ok(())); - skip_if_host_windows!(Ok(())); - - let server = start_mock_server().await; - - let mut builder = test_codex().with_config(|config| { - config.use_experimental_unified_exec_tool = true; - config - .features - .enable(Feature::UnifiedExec) - .expect("test config should allow feature update"); - }); - let test = builder.build_with_auto_env(&server).await?; - - const MAX_SESSIONS_FOR_TEST: i32 = 64; - const FILLER_SESSIONS: i32 = MAX_SESSIONS_FOR_TEST - 1; - - let keep_call_id = "uexec-prune-keep"; - let keep_args = serde_json::json!({ - "cmd": "/bin/cat", - "yield_time_ms": 250, - "tty": true, - }); - - let prune_call_id = "uexec-prune-target"; - // Give the sleeper time to exit before the filler sessions trigger pruning. - let prune_args = serde_json::json!({ - "cmd": "sleep 1", - "yield_time_ms": 1_250, - "tty": true, - }); - - let mut events = vec![ev_response_created("resp-prune-1")]; - events.push(ev_function_call( - keep_call_id, - "exec_command", - &serde_json::to_string(&keep_args)?, - )); - events.push(ev_function_call( - prune_call_id, - "exec_command", - &serde_json::to_string(&prune_args)?, - )); - - for idx in 0..FILLER_SESSIONS { - let filler_args = serde_json::json!({ - "cmd": format!("echo filler {idx}"), - "yield_time_ms": 250, - }); - let call_id = format!("uexec-prune-fill-{idx}"); - events.push(ev_function_call( - &call_id, - "exec_command", - &serde_json::to_string(&filler_args)?, - )); - } - - let keep_write_call_id = "uexec-prune-keep-write"; - let keep_write_args = serde_json::json!({ - "chars": "still alive\n", - "session_id": 1000, - "yield_time_ms": 500, - }); - events.push(ev_function_call( - keep_write_call_id, - "write_stdin", - &serde_json::to_string(&keep_write_args)?, - )); - - let probe_call_id = "uexec-prune-probe"; - let probe_args = serde_json::json!({ - "chars": "should fail\n", - "session_id": 1001, - "yield_time_ms": 500, - }); - events.push(ev_function_call( - probe_call_id, - "write_stdin", - &serde_json::to_string(&probe_args)?, - )); - - events.push(ev_completed("resp-prune-1")); - let first_response = sse(events); - let completion_response = sse(vec![ - ev_response_created("resp-prune-2"), - ev_assistant_message("msg-prune", "done"), - ev_completed("resp-prune-2"), - ]); - let response_mock = - mount_sse_sequence(&server, vec![first_response, completion_response]).await; - - submit_unified_exec_turn(&test, "fill session cache", PermissionProfile::Disabled).await?; - - wait_for_event(&test.codex, |event| { - matches!(event, EventMsg::TurnComplete(_)) - }) - .await; - - let requests = response_mock.requests(); - assert!( - !requests.is_empty(), - "expected at least one response request" - ); - - let keep_start = requests - .iter() - .find_map(|req| req.function_call_output_text(keep_call_id)) - .expect("missing initial keep session output"); - let keep_start_output = parse_unified_exec_output(&keep_start)?; - assert!(keep_start_output.process_id.is_some()); - assert!(keep_start_output.exit_code.is_none()); - - let prune_start = requests - .iter() - .find_map(|req| req.function_call_output_text(prune_call_id)) - .expect("missing initial prune process output"); - let prune_start_output = parse_unified_exec_output(&prune_start)?; - assert!(prune_start_output.process_id.is_some()); - assert!(prune_start_output.exit_code.is_none()); - - let keep_write = requests - .iter() - .find_map(|req| req.function_call_output_text(keep_write_call_id)) - .expect("missing keep write output"); - let keep_write_output = parse_unified_exec_output(&keep_write)?; - assert!(keep_write_output.process_id.is_some()); - assert!( - keep_write_output.output.contains("still alive"), - "expected cat process to echo input, got {:?}", - keep_write_output.output - ); - - let pruned_probe = requests - .iter() - .find_map(|req| req.function_call_output_text(probe_call_id)) - .expect("missing probe output"); - assert!( - pruned_probe.contains("UnknownProcessId") || pruned_probe.contains("Unknown process id"), - "expected probe to fail after pruning, got {pruned_probe:?}" - ); - - Ok(()) -} - fn assert_command(command: &[String], expected_args: &str, expected_cmd: &str) { assert_eq!(command.len(), 3); let shell_path = &command[0];