Codex Handbook
core/src/unified_exec/process_manager_tests.rs 322 lines
use super::*;use crate::unified_exec::clamp_yield_time;use pretty_assertions::assert_eq;use tokio::time::Duration;use tokio::time::Instant;#[test]fn unified_exec_env_injects_defaults() {    let env = apply_unified_exec_env(HashMap::new());    let expected = HashMap::from([        ("NO_COLOR".to_string(), "1".to_string()),        ("TERM".to_string(), "dumb".to_string()),        ("LANG".to_string(), "C.UTF-8".to_string()),        ("LC_CTYPE".to_string(), "C.UTF-8".to_string()),        ("LC_ALL".to_string(), "C.UTF-8".to_string()),        ("COLORTERM".to_string(), String::new()),        ("PAGER".to_string(), "cat".to_string()),        ("GIT_PAGER".to_string(), "cat".to_string()),        ("GH_PAGER".to_string(), "cat".to_string()),        ("CODEX_CI".to_string(), "1".to_string()),    ]);    assert_eq!(env, expected);}#[test]fn unified_exec_env_overrides_existing_values() {    let mut base = HashMap::new();    base.insert("NO_COLOR".to_string(), "0".to_string());    base.insert("PATH".to_string(), "/usr/bin".to_string());    let env = apply_unified_exec_env(base);    assert_eq!(env.get("NO_COLOR"), Some(&"1".to_string()));    assert_eq!(env.get("PATH"), Some(&"/usr/bin".to_string()));}#[test]fn env_overlay_for_exec_server_keeps_runtime_changes_only() {    let local_policy_env = HashMap::from([        ("HOME".to_string(), "/client-home".to_string()),        ("PATH".to_string(), "/client-path".to_string()),        ("SHELL_SET".to_string(), "policy".to_string()),    ]);    let request_env = HashMap::from([        ("HOME".to_string(), "/client-home".to_string()),        ("PATH".to_string(), "/sandbox-path".to_string()),        ("SHELL_SET".to_string(), "policy".to_string()),        ("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),        (            "CODEX_SANDBOX_NETWORK_DISABLED".to_string(),            "1".to_string(),        ),    ]);    assert_eq!(        env_overlay_for_exec_server(&request_env, &local_policy_env),        HashMap::from([            ("PATH".to_string(), "/sandbox-path".to_string()),            ("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),            (                "CODEX_SANDBOX_NETWORK_DISABLED".to_string(),                "1".to_string()            ),        ])    );}#[test]fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {    let cwd: codex_utils_absolute_path::AbsolutePathBuf = std::env::current_dir()        .expect("current dir")        .try_into()        .expect("absolute path");    let file_system_sandbox_policy =        codex_protocol::permissions::FileSystemSandboxPolicy::unrestricted();    let network_sandbox_policy = codex_protocol::permissions::NetworkSandboxPolicy::Restricted;    let permission_profile = codex_protocol::models::PermissionProfile::Disabled;    let request = ExecRequest {        command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()],        cwd: cwd.clone(),        env: HashMap::from([            ("HOME".to_string(), "/client-home".to_string()),            ("PATH".to_string(), "/sandbox-path".to_string()),            ("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),        ]),        exec_server_env_config: Some(ExecServerEnvConfig {            policy: codex_exec_server::ExecEnvPolicy {                inherit: codex_protocol::config_types::ShellEnvironmentPolicyInherit::Core,                ignore_default_excludes: false,                exclude: Vec::new(),                r#set: HashMap::new(),                include_only: Vec::new(),            },            local_policy_env: HashMap::from([                ("HOME".to_string(), "/client-home".to_string()),                ("PATH".to_string(), "/client-path".to_string()),            ]),        }),        network: None,        expiration: crate::exec::ExecExpiration::DefaultTimeout,        capture_policy: crate::exec::ExecCapturePolicy::ShellTool,        sandbox: codex_sandboxing::SandboxType::None,        windows_sandbox_policy_cwd: cwd.clone(),        windows_sandbox_workspace_roots: vec![cwd],        windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,        windows_sandbox_private_desktop: false,        permission_profile,        file_system_sandbox_policy,        network_sandbox_policy,        windows_sandbox_filesystem_overrides: None,        arg0: None,    };    let params =        exec_server_params_for_request(/*process_id*/ 123, &request, /*tty*/ true);    assert_eq!(params.process_id.as_str(), "123");    assert_eq!(params.cwd, PathUri::from_abs_path(&request.cwd));    assert!(params.env_policy.is_some());    assert_eq!(        params.env,        HashMap::from([            ("PATH".to_string(), "/sandbox-path".to_string()),            ("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),        ])    );}#[test]fn exec_server_process_id_matches_unified_exec_process_id() {    assert_eq!(exec_server_process_id(/*process_id*/ 4321), "4321");}#[cfg(windows)]#[test]fn initial_exec_yield_time_uses_windows_floor() {    let above_max_yield_time_ms = crate::unified_exec::MAX_YIELD_TIME_MS + 1;    assert_eq!(        clamp_yield_time(/*yield_time_ms*/ 1_000),        crate::unified_exec::WINDOWS_INITIAL_EXEC_YIELD_TIME_FLOOR_MS    );    assert_eq!(clamp_yield_time(/*yield_time_ms*/ 10_000), 10_000);    assert_eq!(        clamp_yield_time(/*yield_time_ms*/ above_max_yield_time_ms),        crate::unified_exec::MAX_YIELD_TIME_MS    );}#[cfg(not(windows))]#[test]fn initial_exec_yield_time_has_no_platform_floor() {    assert_eq!(clamp_yield_time(/*yield_time_ms*/ 1_000), 1_000);    assert_eq!(        clamp_yield_time(/*yield_time_ms*/ 1),        crate::unified_exec::MIN_YIELD_TIME_MS    );}#[tokio::test]async fn network_denial_fallback_message_names_sandbox_network_proxy() {    let message = network_denial_message_for_session(/*session*/ None, /*deferred*/ None).await;    assert_eq!(        message,        "Network access was denied by the Codex sandbox network proxy."    );}#[tokio::test]async fn late_network_denial_grace_observes_cancellation_after_exit() {    let cancellation = CancellationToken::new();    let cancellation_for_task = cancellation.clone();    tokio::spawn(async move {        tokio::time::sleep(Duration::from_millis(10)).await;        cancellation_for_task.cancel();    });    assert!(wait_for_late_network_denial(Some(cancellation)).await);}#[tokio::test]async fn failed_initial_end_for_unstored_process_uses_fallback_output() {    let (session, turn, rx_event) = crate::session::tests::make_session_and_context_with_rx().await;    let context = UnifiedExecContext::new(        Arc::clone(&session),        Arc::clone(&turn),        "call-unified-denied".to_string(),    );    let request = ExecCommandRequest {        command: vec![            "sh".to_string(),            "-lc".to_string(),            "echo before".to_string(),        ],        shell_type: crate::shell::ShellType::Sh,        hook_command: "echo before".to_string(),        process_id: 123,        yield_time_ms: 1000,        max_output_tokens: None,        #[allow(deprecated)]        cwd: turn.cwd.clone(),        #[allow(deprecated)]        sandbox_cwd: turn.cwd.clone(),        turn_environment: turn            .environments            .primary()            .cloned()            .expect("primary environment"),        shell_mode: codex_tools::UnifiedExecShellMode::Direct,        network: None,        tty: true,        sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,        additional_permissions: None,        additional_permissions_preapproved: false,        justification: None,        prefix_rule: None,    };    let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default()));    transcript        .lock()        .await        .push_chunk(b"PARTIAL_TRANSCRIPT".to_vec());    emit_failed_initial_exec_end_if_unstored(        /*process_started_alive*/ false,        &context,        &request,        #[allow(deprecated)]        turn.cwd.clone(),        transcript,        "PRE_DENIAL_MARKER".to_string(),        "Network access denied".to_string(),        Duration::from_millis(7),    )    .await;    let event = tokio::time::timeout(Duration::from_secs(1), rx_event.recv())        .await        .expect("timed out waiting for failed exec end event")        .expect("event channel closed");    let codex_protocol::protocol::EventMsg::ExecCommandEnd(end_event) = event.msg else {        panic!("expected ExecCommandEnd event");    };    assert_eq!(end_event.call_id, "call-unified-denied");    assert_eq!(        end_event.status,        codex_protocol::protocol::ExecCommandStatus::Failed    );    assert_eq!(end_event.exit_code, -1);    assert_eq!(end_event.process_id.as_deref(), Some("123"));    assert_eq!(        end_event.aggregated_output,        "PRE_DENIAL_MARKER\nNetwork access denied"    );}#[test]fn pruning_prefers_exited_processes_outside_recently_used() {    let now = Instant::now();    let meta = vec![        (1, now - Duration::from_secs(40), false),        (2, now - Duration::from_secs(30), true),        (3, now - Duration::from_secs(20), false),        (4, now - Duration::from_secs(19), false),        (5, now - Duration::from_secs(18), false),        (6, now - Duration::from_secs(17), false),        (7, now - Duration::from_secs(16), false),        (8, now - Duration::from_secs(15), false),        (9, now - Duration::from_secs(14), false),        (10, now - Duration::from_secs(13), false),    ];    let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);    assert_eq!(candidate, Some(2));}#[test]fn pruning_falls_back_to_lru_when_no_exited() {    let now = Instant::now();    let meta = vec![        (1, now - Duration::from_secs(40), false),        (2, now - Duration::from_secs(30), false),        (3, now - Duration::from_secs(20), false),        (4, now - Duration::from_secs(19), false),        (5, now - Duration::from_secs(18), false),        (6, now - Duration::from_secs(17), false),        (7, now - Duration::from_secs(16), false),        (8, now - Duration::from_secs(15), false),        (9, now - Duration::from_secs(14), false),        (10, now - Duration::from_secs(13), false),    ];    let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);    assert_eq!(candidate, Some(1));}#[test]fn pruning_protects_recent_processes_even_if_exited() {    let now = Instant::now();    let meta = vec![        (1, now - Duration::from_secs(40), false),        (2, now - Duration::from_secs(30), false),        (3, now - Duration::from_secs(20), true),        (4, now - Duration::from_secs(19), false),        (5, now - Duration::from_secs(18), false),        (6, now - Duration::from_secs(17), false),        (7, now - Duration::from_secs(16), false),        (8, now - Duration::from_secs(15), false),        (9, now - Duration::from_secs(14), false),        (10, now - Duration::from_secs(13), true),    ];    let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);    // (10) is exited but among the last 8; we should drop the LRU outside that set.    assert_eq!(candidate, Some(1));}