Codex Handbook
tui/src/chatwidget/tests/approval_requests.rs 449 lines
//! Approval-request focused tests extracted from the main chatwidget test file//! to keep the primary module under blob-size policy limits.use super::*;use pretty_assertions::assert_eq;#[tokio::test]async fn exec_approval_emits_proposed_command_and_decision_history() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Trigger an exec approval request with a short, single-line command.    let ev = ExecApprovalRequestEvent {        call_id: "call-short".into(),        approval_id: Some("call-short".into()),        turn_id: "turn-short".into(),        command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: Some(            "this is a test reason such as one that would be produced by the model".into(),        ),        network_approval_context: None,        proposed_execpolicy_amendment: None,        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-short", ev);    let proposed_cells = drain_insert_history(&mut rx);    assert!(        proposed_cells.is_empty(),        "expected approval request to render via modal without emitting history cells"    );    let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));    let mut buf = ratatui::buffer::Buffer::empty(area);    chat.render(area, &mut buf);    assert_snapshot!("exec_approval_modal_exec", format!("{buf:?}"));    chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    let decision = drain_insert_history(&mut rx)        .pop()        .expect("expected decision cell in history");    assert_snapshot!(        "exec_approval_history_decision_approved_short",        lines_to_single_string(&decision)    );}#[test]fn app_server_exec_approval_request_splits_shell_wrapped_command() {    let script = r#"python3 -c 'print("Hello, world!")'"#;    let request = exec_approval_request_from_params(        AppServerCommandExecutionRequestApprovalParams {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            item_id: "item-1".to_string(),            started_at_ms: 0,            approval_id: Some("approval-1".to_string()),            reason: None,            network_approval_context: None,            command: Some(                shlex::try_join(["/bin/zsh", "-lc", script])                    .expect("round-trippable shell wrapper"),            ),            cwd: Some(test_path_buf("/tmp").abs()),            command_actions: None,            additional_permissions: None,            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            available_decisions: None,        },        &test_path_buf("/tmp").abs(),    );    assert_eq!(        request.command,        vec![            "/bin/zsh".to_string(),            "-lc".to_string(),            script.to_string(),        ]    );}#[test]fn app_server_exec_approval_request_preserves_permissions_context() {    let read_path = AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp/read-only")))        .expect("absolute read path");    let write_path = AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp/write")))        .expect("absolute write path");    let read_api_path = LegacyAppPathString::from_abs_path(&read_path);    let write_api_path = LegacyAppPathString::from_abs_path(&write_path);    let request = exec_approval_request_from_params(        AppServerCommandExecutionRequestApprovalParams {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            item_id: "item-1".to_string(),            started_at_ms: 0,            approval_id: Some("approval-1".to_string()),            reason: None,            network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext {                host: "example.com".to_string(),                protocol: codex_app_server_protocol::NetworkApprovalProtocol::Socks5Tcp,            }),            command: Some("ls".to_string()),            cwd: Some(test_path_buf("/tmp").abs()),            command_actions: None,            additional_permissions: Some(AppServerAdditionalPermissionProfile {                network: Some(AppServerAdditionalNetworkPermissions {                    enabled: Some(true),                }),                file_system: Some(AppServerAdditionalFileSystemPermissions {                    read: Some(vec![read_api_path.clone()]),                    write: Some(vec![write_api_path.clone()]),                    glob_scan_max_depth: None,                    entries: None,                }),            }),            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            available_decisions: None,        },        &test_path_buf("/tmp").abs(),    );    assert_eq!(        request.network_approval_context,        Some(codex_app_server_protocol::NetworkApprovalContext {            host: "example.com".to_string(),            protocol: codex_app_server_protocol::NetworkApprovalProtocol::Socks5Tcp,        })    );    assert_eq!(        request.additional_permissions,        Some(AppServerAdditionalPermissionProfile {            network: Some(AppServerAdditionalNetworkPermissions {                enabled: Some(true),            }),            file_system: Some(AppServerAdditionalFileSystemPermissions {                read: Some(vec![read_api_path]),                write: Some(vec![write_api_path]),                glob_scan_max_depth: None,                entries: None,            }),        })    );}#[tokio::test]async fn network_exec_approval_history_describes_session_host_allowance() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let request = exec_approval_request_from_params(        AppServerCommandExecutionRequestApprovalParams {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            item_id: "item-1".to_string(),            started_at_ms: 0,            approval_id: Some("approval-1".to_string()),            reason: None,            network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext {                host: "example.com".to_string(),                protocol: codex_app_server_protocol::NetworkApprovalProtocol::Https,            }),            command: Some("network-access https://example.com:8443".to_string()),            cwd: None,            command_actions: None,            additional_permissions: None,            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            available_decisions: Some(vec![                codex_app_server_protocol::CommandExecutionApprovalDecision::AcceptForSession,                codex_app_server_protocol::CommandExecutionApprovalDecision::Cancel,            ]),        },        &test_path_buf("/tmp").abs(),    );    handle_exec_approval_request(&mut chat, "sub-network", request);    chat.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));    let decision = drain_insert_history(&mut rx)        .pop()        .expect("expected decision cell in history");    assert_snapshot!(        "network_exec_approval_history_session_host_allowance",        lines_to_single_string(&decision)    );}#[tokio::test]async fn network_exec_approval_history_describes_one_time_host_allowance() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let request = exec_approval_request_from_params(        AppServerCommandExecutionRequestApprovalParams {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            item_id: "item-1".to_string(),            started_at_ms: 0,            approval_id: Some("approval-1".to_string()),            reason: None,            network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext {                host: "example.com".to_string(),                protocol: codex_app_server_protocol::NetworkApprovalProtocol::Http,            }),            command: None,            cwd: None,            command_actions: None,            additional_permissions: None,            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            available_decisions: Some(vec![                codex_app_server_protocol::CommandExecutionApprovalDecision::Accept,                codex_app_server_protocol::CommandExecutionApprovalDecision::Cancel,            ]),        },        &test_path_buf("/tmp").abs(),    );    handle_exec_approval_request(&mut chat, "sub-network", request);    chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    let decision = drain_insert_history(&mut rx)        .pop()        .expect("expected decision cell in history");    assert_snapshot!(        "network_exec_approval_history_one_time_host_allowance",        lines_to_single_string(&decision)    );}#[tokio::test]async fn network_exec_approval_history_describes_canceled_host_request() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let request = exec_approval_request_from_params(        AppServerCommandExecutionRequestApprovalParams {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            item_id: "item-1".to_string(),            started_at_ms: 0,            approval_id: Some("approval-1".to_string()),            reason: None,            network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext {                host: "example.com".to_string(),                protocol: codex_app_server_protocol::NetworkApprovalProtocol::Socks5Tcp,            }),            command: Some("network-access socks5-tcp://example.com:1080".to_string()),            cwd: None,            command_actions: None,            additional_permissions: None,            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            available_decisions: Some(vec![                codex_app_server_protocol::CommandExecutionApprovalDecision::Accept,                codex_app_server_protocol::CommandExecutionApprovalDecision::Cancel,            ]),        },        &test_path_buf("/tmp").abs(),    );    handle_exec_approval_request(&mut chat, "sub-network", request);    chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));    let decision = drain_insert_history(&mut rx)        .pop()        .expect("expected decision cell in history");    assert_snapshot!(        "network_exec_approval_history_canceled_host_request",        lines_to_single_string(&decision)    );}#[test]fn app_server_request_permissions_preserves_file_system_permissions() {    let read_path = AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp/read-only")))        .expect("absolute read path");    let write_path = AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp/write")))        .expect("absolute write path");    let read_api_path = LegacyAppPathString::from_abs_path(&read_path);    let write_api_path = LegacyAppPathString::from_abs_path(&write_path);    let cwd =        AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp"))).expect("absolute cwd");    let request = request_permissions_from_params(AppServerPermissionsRequestApprovalParams {        thread_id: "thread-1".to_string(),        turn_id: "turn-1".to_string(),        item_id: "item-1".to_string(),        environment_id: Some("remote".to_string()),        started_at_ms: 0,        cwd: cwd.clone(),        reason: Some("Select a workspace root".to_string()),        permissions: codex_app_server_protocol::RequestPermissionProfile {            network: Some(AppServerAdditionalNetworkPermissions {                enabled: Some(true),            }),            file_system: Some(AppServerAdditionalFileSystemPermissions {                read: Some(vec![read_api_path]),                write: Some(vec![write_api_path]),                glob_scan_max_depth: None,                entries: None,            }),        },    })    .expect("API paths should convert to native paths");    assert_eq!(        request.permissions,        RequestPermissionProfile {            network: Some(NetworkPermissions {                enabled: Some(true),            }),            file_system: Some(FileSystemPermissions::from_read_write_roots(                Some(vec![read_path]),                Some(vec![write_path]),            )),        }    );    assert_eq!(request.cwd, Some(cwd));    assert_eq!(request.environment_id.as_deref(), Some("remote"));}#[tokio::test]async fn exec_approval_uses_approval_id_when_present() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_exec_approval_request(        &mut chat,        "sub-short",        ExecApprovalRequestEvent {            call_id: "call-parent".into(),            approval_id: Some("approval-subcommand".into()),            turn_id: "turn-short".into(),            command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],            cwd: AbsolutePathBuf::current_dir().expect("current dir"),            reason: Some(                "this is a test reason such as one that would be produced by the model".into(),            ),            network_approval_context: None,            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            additional_permissions: None,            available_decisions: None,        },    );    chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    let mut found = false;    while let Ok(app_ev) = rx.try_recv() {        if let AppEvent::SubmitThreadOp {            op: Op::ExecApproval { id, decision, .. },            ..        } = app_ev        {            assert_eq!(id, "approval-subcommand");            assert_matches!(                decision,                codex_app_server_protocol::CommandExecutionApprovalDecision::Accept            );            found = true;            break;        }    }    assert!(found, "expected ExecApproval op to be sent");}#[tokio::test]async fn exec_approval_decision_truncates_multiline_and_long_commands() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let ev_multi = ExecApprovalRequestEvent {        call_id: "call-multi".into(),        approval_id: Some("call-multi".into()),        turn_id: "turn-multi".into(),        command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()],        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: Some(            "this is a test reason such as one that would be produced by the model".into(),        ),        network_approval_context: None,        proposed_execpolicy_amendment: None,        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);    let proposed_multi = drain_insert_history(&mut rx);    assert!(        proposed_multi.is_empty(),        "expected multiline approval request to render via modal without emitting history cells"    );    let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));    let mut buf = ratatui::buffer::Buffer::empty(area);    chat.render(area, &mut buf);    let mut saw_first_line = false;    for y in 0..area.height {        let mut row = String::new();        for x in 0..area.width {            row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));        }        if row.contains("echo line1") {            saw_first_line = true;            break;        }    }    assert!(        saw_first_line,        "expected modal to show first line of multiline snippet"    );    chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));    let aborted_multi = drain_insert_history(&mut rx)        .pop()        .expect("expected aborted decision cell (multiline)");    assert_snapshot!(        "exec_approval_history_decision_aborted_multiline",        lines_to_single_string(&aborted_multi)    );    let long = format!("echo {}", "a".repeat(200));    let ev_long = ExecApprovalRequestEvent {        call_id: "call-long".into(),        approval_id: Some("call-long".into()),        turn_id: "turn-long".into(),        command: vec!["bash".into(), "-lc".into(), long],        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: None,        network_approval_context: None,        proposed_execpolicy_amendment: None,        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-long", ev_long);    let proposed_long = drain_insert_history(&mut rx);    assert!(        proposed_long.is_empty(),        "expected long approval request to avoid emitting history cells before decision"    );    chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));    let aborted_long = drain_insert_history(&mut rx)        .pop()        .expect("expected aborted decision cell (long)");    assert_snapshot!(        "exec_approval_history_decision_aborted_long",        lines_to_single_string(&aborted_long)    );}