#![allow(clippy::unwrap_used)] use anyhow::Result; use codex_config::test_support::CloudConfigBundleFixture; use codex_core::EnvironmentConfig; use codex_core::TurnInputRequest; use codex_core::config::Constrained; use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_execpolicy::Decision; use codex_execpolicy::Policy; use codex_execpolicy::RequirementsExecPolicy; use codex_features::Feature; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::error::CodexErrorDetails; use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfileSnapshot; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::user_input::UserInput; 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_response_created; use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_target_windows; use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; use std::fs; const COMPLEX_FORCED_RM_COMMAND: &str = "for target in \"\"; do rm -rf \"$target\"; done"; fn collaboration_mode_for_model(model: String) -> CollaborationMode { CollaborationMode { mode: ModeKind::Default, settings: Settings { model, reasoning_effort: None, developer_instructions: Some("exercise approvals in collaboration mode".to_string()), }, } } async fn submit_user_turn( test: &core_test_support::test_codex::TestCodex, prompt: &str, approval_policy: AskForApproval, permission_profile: PermissionProfile, collaboration_mode: Option, ) -> Result<()> { let session_model = test.session_configured.model.clone(); let (sandbox_policy, permission_profile) = turn_permission_fields(permission_profile, test.config.cwd.as_path()); test.codex .start_or_steer_turn( TurnInputRequest::user_input(vec![UserInput::Text { text: prompt.into(), text_elements: Vec::new(), }]) .with_thread_settings(ThreadSettingsOverrides { environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), sandbox_policy: Some(sandbox_policy), permission_profile, collaboration_mode: collaboration_mode.or({ Some(CollaborationMode { mode: ModeKind::Default, settings: Settings { model: session_model, reasoning_effort: None, developer_instructions: None, }, }) }), ..Default::default() }), ) .await?; Ok(()) } fn assert_no_matched_rules_invariant(output_item: &Value) { let output = output_item .get("output") .and_then(Value::as_str) .expect("function call output should include a string output payload"); assert!( !output.contains("invariant failed: matched_rules must be non-empty"), "unexpected invariant panic surfaced in output: {output}" ); } #[tokio::test] async fn git_status_requires_approval_under_unless_trusted() -> Result<()> { skip_if_wine_exec!(Ok(()), "command approval requires host-native paths"); let server = start_mock_server().await; let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| { config .features .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); config.permissions.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); config .permissions .set_permission_profile(PermissionProfile::workspace_write()) .expect("set workspace-write permissions"); config.approvals_reviewer = ApprovalsReviewer::User; }); let test = builder.build_with_auto_env(&server).await?; let call_id = "git-status-approval"; let args = json!({"cmd": "git status", "yield_time_ms": 1_000}); let initial_mock = mount_sse_once( &server, sse(vec![ ev_response_created("resp-git-status-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-git-status-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-git-status-1", "done"), ev_completed("resp-git-status-2"), ]), ) .await; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "check git status".into(), text_elements: Vec::new(), }])) .await?; let event = wait_for_event(&test.codex, |event| { matches!( event, EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_) ) }) .await; let EventMsg::ExecApprovalRequest(approval) = event else { let output = results_mock.function_call_output_text(call_id); panic!( "expected git status to request approval before turn completion; output: {output:?}" ); }; assert_eq!(approval.call_id, call_id); test.codex .submit(Op::ExecApproval { id: approval.effective_approval_id(), turn_id: None, decision: ReviewDecision::denied("git status was not approved"), }) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; assert!( initial_mock .single_request() .message_input_texts("user") .iter() .any(|text| text == "check git status") ); let output = results_mock .single_request() .function_call_output_text(call_id) .expect("shell command output"); assert!(output.contains("git status was not approved"), "{output}"); Ok(()) } #[tokio::test] async fn startup_migrates_default_policy_and_honors_ignore_rules() -> Result<()> { const LEGACY_POLICY: &str = r#"prefix_rule(pattern=["rm"], decision="allow") prefix_rule(pattern=["git", "status"], decision="allow") "#; const MIGRATED_POLICY: &str = r#"prefix_rule(pattern=["git", "status"], decision="allow") "#; const MIGRATION_MARKER_FILENAME: &str = ".sandbox_migration"; let server = start_mock_server().await; let mut migrated_builder = test_codex().with_config(|config| { let policy_path = config.codex_home.join("rules/default.rules"); fs::create_dir_all(policy_path.parent().expect("rules directory")) .expect("create rules directory"); fs::write(policy_path, LEGACY_POLICY).expect("write legacy policy"); }); let migrated = migrated_builder.build_with_auto_env(&server).await?; let migrated_policy_path = migrated.codex_home_path().join("rules/default.rules"); assert_eq!(fs::read_to_string(&migrated_policy_path)?, MIGRATED_POLICY); assert_eq!( fs::read_to_string(migrated.codex_home_path().join(MIGRATION_MARKER_FILENAME))?, "v1\n" ); let mut ignored_builder = test_codex().with_config(|config| { let policy_path = config.codex_home.join("rules/default.rules"); fs::create_dir_all(policy_path.parent().expect("rules directory")) .expect("create rules directory"); fs::write(policy_path, LEGACY_POLICY).expect("write legacy policy"); config.config_layer_stack = config .config_layer_stack .clone() .with_user_and_project_exec_policy_rules_ignored( /*ignore_user_and_project_exec_policy_rules*/ true, ); }); let ignored = ignored_builder.build_with_auto_env(&server).await?; let ignored_policy_path = ignored.codex_home_path().join("rules/default.rules"); assert_eq!(fs::read_to_string(&ignored_policy_path)?, LEGACY_POLICY); assert!( !ignored .codex_home_path() .join(MIGRATION_MARKER_FILENAME) .exists() ); Ok(()) } #[tokio::test] async fn granular_complex_forced_rm_denial_explains_why_the_command_was_rejected() -> Result<()> { skip_if_target_windows!(Ok(()), "uses a POSIX shell command fixture"); let server = start_mock_server().await; let mut builder = test_codex(); let test = builder.build_with_auto_env(&server).await?; let call_id = "forced-rm-denied"; let args = json!({ "cmd": COMPLEX_FORCED_RM_COMMAND, "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-forced-rm-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-forced-rm-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-forced-rm-1", "done"), ev_completed("resp-forced-rm-2"), ]), ) .await; submit_user_turn( &test, "run the forced rm loop", AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: false, rules: true, skill_approval: true, request_permissions: true, mcp_elicitations: true, }), PermissionProfile::read_only(), /*collaboration_mode*/ None, ) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let output_item = results_mock.single_request().function_call_output(call_id); let output = output_item .get("output") .and_then(Value::as_str) .expect("function call output should include a string output payload"); assert!( output.contains("rm -f style commands are not permitted. Use a safer approach"), "unexpected output: {output}" ); Ok(()) } #[tokio::test] async fn granular_complex_forced_rm_requests_approval_when_allowed() -> Result<()> { skip_if_target_windows!(Ok(()), "uses a POSIX shell command fixture"); let server = start_mock_server().await; let mut builder = test_codex(); let test = builder.build_with_auto_env(&server).await?; let call_id = "forced-rm-approval"; let args = json!({ "cmd": COMPLEX_FORCED_RM_COMMAND, "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-forced-rm-approval-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-forced-rm-approval-1"), ]), ) .await; mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-forced-rm-approval-1", "done"), ev_completed("resp-forced-rm-approval-2"), ]), ) .await; submit_user_turn( &test, "run the forced rm loop", AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: true, rules: true, skill_approval: true, request_permissions: true, mcp_elicitations: true, }), PermissionProfile::read_only(), /*collaboration_mode*/ None, ) .await?; let approval_event = wait_for_event(&test.codex, |event| { matches!( event, EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_) ) }) .await; let EventMsg::ExecApprovalRequest(approval) = approval_event else { panic!("expected forced rm to request approval before turn completion"); }; assert_eq!( approval.command.last().map(String::as_str), Some(COMPLEX_FORCED_RM_COMMAND) ); test.codex .submit(Op::ExecApproval { id: approval.effective_approval_id(), turn_id: None, decision: ReviewDecision::denied("rejected by user"), }) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; Ok(()) } #[tokio::test] async fn deeply_nested_forced_rm_is_rejected_before_execution_when_approvals_are_disabled() -> Result<()> { skip_if_target_windows!(Ok(()), "uses a POSIX shell command fixture"); let server = start_mock_server().await; let mut builder = test_codex(); let test = builder.build_with_auto_env(&server).await?; let sentinel = test.config.cwd.join("forced-rm-sentinel"); fs::write(&sentinel, "must not be deleted")?; let call_id = "deeply-nested-forced-rm"; let args = json!({ "cmd": "env env env env env env env env env rm -rf forced-rm-sentinel", "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-deeply-nested-forced-rm-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-deeply-nested-forced-rm-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-deeply-nested-forced-rm", "done"), ev_completed("resp-deeply-nested-forced-rm-2"), ]), ) .await; submit_user_turn( &test, "run the deeply nested forced rm", AskForApproval::Never, PermissionProfile::Disabled, /*collaboration_mode*/ None, ) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let output_item = results_mock.single_request().function_call_output(call_id); let output = output_item .get("output") .and_then(Value::as_str) .expect("function call output should include a string output payload"); assert!( output.contains("rejected: blocked by policy"), "unexpected output: {output}" ); assert!(sentinel.exists(), "the rejected command must not execute"); Ok(()) } #[cfg(windows)] #[tokio::test] async fn unified_exec_disabled_windows_sandbox_rejects_managed_read_only_command() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_config(|config| { config .features .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); config .features .disable(Feature::WindowsSandbox) .expect("test config should allow feature update"); config .features .disable(Feature::WindowsSandboxElevated) .expect("test config should allow feature update"); config.set_windows_sandbox_enabled(false); config.set_windows_elevated_sandbox_enabled(false); }); let test = builder.build(&server).await?; let call_id = "unified-exec-disabled-windows-sandbox-read-only"; let args = json!({ "cmd": "cmd.exe /c dir", "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-disabled-windows-sandbox-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-disabled-windows-sandbox-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-disabled-windows-sandbox-1", "done"), ev_completed("resp-disabled-windows-sandbox-2"), ]), ) .await; submit_user_turn( &test, "run unified exec with disabled Windows sandbox", AskForApproval::Never, PermissionProfile::read_only(), None, ) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let output_item = results_mock.single_request().function_call_output(call_id); let output = output_item .get("output") .and_then(Value::as_str) .expect("function call output should include a string output payload"); assert!( output.contains("cmd.exe /c dir") && output.contains("rejected: blocked by policy"), "unexpected output: {output}", ); Ok(()) } #[tokio::test] async fn execpolicy_blocks_shell_invocation() -> Result<()> { let mut builder = test_codex().with_config(|config| { let policy_path = config.codex_home.join("rules").join("policy.rules"); fs::create_dir_all( policy_path .parent() .expect("policy directory must have a parent"), ) .expect("create policy directory"); fs::write( &policy_path, r#"prefix_rule(pattern=["echo"], decision="forbidden")"#, ) .expect("write policy file"); }); let server = start_mock_server().await; let test = builder.build(&server).await?; let call_id = "shell-forbidden"; let args = json!({ "cmd": "echo blocked", "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-1", "done"), ev_completed("resp-2"), ]), ) .await; let session_model = test.session_configured.model.clone(); let (sandbox_policy, permission_profile) = turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); test.codex .start_or_steer_turn( TurnInputRequest::user_input(vec![UserInput::Text { text: "run shell command".into(), text_elements: Vec::new(), }]) .with_thread_settings(ThreadSettingsOverrides { environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, collaboration_mode: Some(CollaborationMode { mode: ModeKind::Default, settings: Settings { model: session_model, reasoning_effort: None, developer_instructions: None, }, }), ..Default::default() }), ) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let output = results_mock .single_request() .function_call_output_text(call_id) .expect("forbidden command should produce a tool response"); assert!( output.contains("policy forbids commands starting with `echo`"), "unexpected output: {output}" ); Ok(()) } #[tokio::test] async fn malformed_custom_rules_preserve_managed_forbidden_prefix() -> Result<()> { skip_if_target_windows!( Ok(()), "managed prefix fixture uses POSIX executable semantics" ); let mut builder = test_codex() .with_cloud_config_bundle( CloudConfigBundleFixture::loader_with_enterprise_requirement( r#" [rules] prefix_rules = [ { pattern = [{ token = "echo" }], decision = "forbidden" }, ] "#, ), ) .with_config(|config| { config .features .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); let policy_path = config.codex_home.join("rules").join("broken.rules"); fs::create_dir_all( policy_path .parent() .expect("policy directory must have a parent"), ) .expect("create policy directory"); fs::write(policy_path, "prefix_rule(").expect("write malformed policy file"); }); let server = start_mock_server().await; let test = builder.build_with_auto_env(&server).await?; let call_id = "managed-shell-forbidden"; let args = json!({ "cmd": "echo blocked", "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-managed-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-managed-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-managed-1", "done"), ev_completed("resp-managed-2"), ]), ) .await; test.submit_turn_with_approval_and_permission_profile( "run shell command", AskForApproval::Never, PermissionProfile::Disabled, ) .await?; let output_item = results_mock.single_request().function_call_output(call_id); let output = output_item .get("output") .and_then(Value::as_str) .expect("function call output should include a string output payload"); assert!( output.contains("policy forbids commands starting with `echo`"), "unexpected output: {output}" ); Ok(()) } #[tokio::test] async fn environment_command_restrictions_override_saved_prefix_approvals() -> Result<()> { skip_if_target_windows!( Ok(()), "managed prefix fixture uses POSIX executable semantics" ); let mut builder = test_codex().with_config(|config| { config .features .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); let policy_path = config.codex_home.join("rules").join("approved.rules"); fs::create_dir_all(policy_path.parent().expect("rules directory")) .expect("create rules directory"); fs::write( policy_path, r#"prefix_rule(pattern=["echo"], decision="allow")"#, ) .expect("write approved command prefix"); }); let server = start_mock_server().await; let test = builder.build_with_auto_env(&server).await?; let selection = test .codex .environment_selections() .await .into_iter() .next() .expect("thread should select its executor environment"); let mut invalid_policy = Policy::empty(); invalid_policy.add_prefix_rule(&["echo".to_string()], Decision::Allow)?; let error = test .codex .environment_ready( &selection, EnvironmentConfig { allow_login_shell: true, workspace_roots: selection.workspace_roots.clone(), permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled), shell_environment_policy: Default::default(), windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config), windows_sandbox_private_desktop: test .config .permissions .windows_sandbox_private_desktop, use_legacy_landlock: test.config.features.use_legacy_landlock(), exec_policy: Some(RequirementsExecPolicy::new(invalid_policy)), mcp_policy: None, network_policy: None, selected_capability_roots: Vec::new(), }, ) .await .expect_err("environment policies must not introduce command allowances"); assert!(matches!( error.details(), CodexErrorDetails::InvalidRequest(message) if message == "environment command policy cannot contain allow rules" )); let mut environment_policy = Policy::empty(); environment_policy.add_prefix_rule(&["echo".to_string()], Decision::Forbidden)?; test.codex .environment_ready( &selection, EnvironmentConfig { allow_login_shell: true, workspace_roots: selection.workspace_roots.clone(), permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled), shell_environment_policy: Default::default(), windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config), windows_sandbox_private_desktop: test .config .permissions .windows_sandbox_private_desktop, use_legacy_landlock: test.config.features.use_legacy_landlock(), exec_policy: Some(RequirementsExecPolicy::new(environment_policy)), mcp_policy: None, network_policy: None, selected_capability_roots: Vec::new(), }, ) .await?; let call_id = "environment-managed-command"; let args = json!({ "cmd": "echo blocked", "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-environment-managed-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-environment-managed-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-environment-managed", "done"), ev_completed("resp-environment-managed-2"), ]), ) .await; test.submit_turn_with_approval_and_permission_profile( "run shell command", AskForApproval::Never, PermissionProfile::Disabled, ) .await?; let output_item = results_mock.single_request().function_call_output(call_id); let output = output_item .get("output") .and_then(Value::as_str) .expect("function call output should include a string output payload"); assert!( output.contains("policy forbids commands starting with `echo`"), "unexpected output: {output}" ); Ok(()) } #[tokio::test] async fn environment_command_policy_changes_invalidate_session_approvals() -> Result<()> { skip_if_target_windows!( Ok(()), "managed prefix fixture uses POSIX executable semantics" ); let mut builder = test_codex().with_config(|config| { config .features .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config .permissions .set_permission_profile(PermissionProfile::Disabled) .expect("test config should allow unrestricted permissions"); config.approvals_reviewer = ApprovalsReviewer::User; let policy_path = config.codex_home.join("rules").join("prompt.rules"); fs::create_dir_all(policy_path.parent().expect("rules directory")) .expect("create rules directory"); fs::write( policy_path, r#"prefix_rule(pattern=["echo"], decision="prompt")"#, ) .expect("write prompt command prefix"); }); let server = start_mock_server().await; let test = builder.build_with_auto_env(&server).await?; let selection = test .codex .environment_selections() .await .into_iter() .next() .expect("thread should select its executor environment"); for (attempt, decision) in [ ("before-owner-policy", ReviewDecision::ApprovedForSession), ("after-owner-policy", ReviewDecision::Approved), ] { if attempt == "after-owner-policy" { let mut policy = Policy::empty(); policy.add_prefix_rule(&["echo".to_string()], Decision::Prompt)?; test.codex .environment_ready( &selection, EnvironmentConfig { allow_login_shell: true, workspace_roots: selection.workspace_roots.clone(), permission_profile: PermissionProfileSnapshot::legacy( PermissionProfile::Disabled, ), shell_environment_policy: Default::default(), windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config), windows_sandbox_private_desktop: test .config .permissions .windows_sandbox_private_desktop, use_legacy_landlock: test.config.features.use_legacy_landlock(), exec_policy: Some(RequirementsExecPolicy::new(policy)), mcp_policy: None, network_policy: None, selected_capability_roots: Vec::new(), }, ) .await?; } let args = json!({ "cmd": "echo approval", "yield_time_ms": 1_000 }); mount_sse_once( &server, sse(vec![ ev_response_created(attempt), ev_function_call(attempt, "exec_command", &serde_json::to_string(&args)?), ev_completed(attempt), ]), ) .await; mount_sse_once( &server, sse(vec![ ev_assistant_message(attempt, "done"), ev_completed(attempt), ]), ) .await; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: attempt.into(), text_elements: Vec::new(), }])) .await?; let event = wait_for_event(&test.codex, |event| { matches!( event, EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_) ) }) .await; let EventMsg::ExecApprovalRequest(approval) = event else { panic!("expected a fresh command approval {attempt}"); }; test.codex .submit(Op::ExecApproval { id: approval.effective_approval_id(), turn_id: None, decision, }) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; } Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_empty_script_with_collaboration_mode_does_not_panic() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| { config .features .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); }); let test = builder.build(&server).await?; let call_id = "unified-exec-empty-script-collab"; let args = json!({ "cmd": "", "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-empty-unified-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-empty-unified-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-empty-unified-1", "done"), ev_completed("resp-empty-unified-2"), ]), ) .await; let collaboration_mode = collaboration_mode_for_model(test.session_configured.model.clone()); submit_user_turn( &test, "run empty unified exec command", AskForApproval::OnRequest, PermissionProfile::Disabled, Some(collaboration_mode), ) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let output_item = results_mock.single_request().function_call_output(call_id); assert_no_matched_rules_invariant(&output_item); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_whitespace_script_with_collaboration_mode_does_not_panic() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| { config .features .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); }); let test = builder.build(&server).await?; let call_id = "unified-exec-whitespace-script-collab"; let args = json!({ "cmd": " \n \t", "yield_time_ms": 1_000, }); mount_sse_once( &server, sse(vec![ ev_response_created("resp-whitespace-unified-1"), ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), ev_completed("resp-whitespace-unified-1"), ]), ) .await; let results_mock = mount_sse_once( &server, sse(vec![ ev_assistant_message("msg-whitespace-unified-1", "done"), ev_completed("resp-whitespace-unified-2"), ]), ) .await; let collaboration_mode = collaboration_mode_for_model(test.session_configured.model.clone()); submit_user_turn( &test, "run whitespace unified exec command", AskForApproval::OnRequest, PermissionProfile::Disabled, Some(collaboration_mode), ) .await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let output_item = results_mock.single_request().function_call_output(call_id); assert_no_matched_rules_invariant(&output_item); Ok(()) }