Codex Handbook
tui/src/chatwidget/tests/permissions.rs 1183 lines
use super::*;use crate::legacy_core::config::CustomPermissionProfileSummary;use codex_protocol::models::ActivePermissionProfile;use codex_protocol::models::ManagedFileSystemPermissions;use codex_protocol::permissions::FileSystemAccessMode;use codex_protocol::permissions::FileSystemPath;use codex_protocol::permissions::FileSystemSandboxEntry;use codex_protocol::permissions::FileSystemSpecialPath;use codex_protocol::permissions::NetworkSandboxPolicy;use pretty_assertions::assert_eq;fn app_server_workspace_write_profile(extra_root: AbsolutePathBuf) -> PermissionProfile {    PermissionProfile::Managed {        network: NetworkSandboxPolicy::Restricted,        file_system: ManagedFileSystemPermissions::Restricted {            entries: vec![                FileSystemSandboxEntry {                    path: FileSystemPath::Special {                        value: FileSystemSpecialPath::Root,                    },                    access: FileSystemAccessMode::Read,                },                FileSystemSandboxEntry {                    path: FileSystemPath::Special {                        value: FileSystemSpecialPath::ProjectRoots { subpath: None },                    },                    access: FileSystemAccessMode::Write,                },                FileSystemSandboxEntry {                    path: FileSystemPath::Special {                        value: FileSystemSpecialPath::SlashTmp,                    },                    access: FileSystemAccessMode::Write,                },                FileSystemSandboxEntry {                    path: FileSystemPath::Special {                        value: FileSystemSpecialPath::Tmpdir,                    },                    access: FileSystemAccessMode::Write,                },                FileSystemSandboxEntry {                    path: FileSystemPath::Path { path: extra_root },                    access: FileSystemAccessMode::Write,                },            ],            glob_scan_max_depth: None,        },    }}fn windows_sandbox_requirements_stack(    allowed_sandbox_implementations: Vec<WindowsSandboxModeToml>,) -> ConfigLayerStack {    let requirements_toml = codex_config::ConfigRequirementsToml {        windows: Some(codex_config::WindowsRequirementsToml {            allowed_sandbox_implementations: Some(allowed_sandbox_implementations),        }),        ..Default::default()    };    let mut requirements_with_sources = codex_config::ConfigRequirementsWithSources::default();    requirements_with_sources        .merge_unset_fields(RequirementSource::Unknown, requirements_toml.clone());    let requirements = codex_config::ConfigRequirements::try_from(requirements_with_sources)        .expect("windows sandbox requirements");    ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)        .expect("test config layer stack")}#[tokio::test]async fn approvals_selection_popup_snapshot() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false);    chat.config.notices.hide_full_access_warning = None;    chat.open_approvals_popup();    let popup = render_bottom_popup(&chat, /*width*/ 80);    #[cfg(target_os = "windows")]    insta::with_settings!({ snapshot_suffix => "windows" }, {        assert_chatwidget_snapshot!("approvals_selection_popup", popup);    });    #[cfg(not(target_os = "windows"))]    assert_chatwidget_snapshot!("approvals_selection_popup", popup);}#[tokio::test]async fn profile_permissions_selection_popup_snapshot() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.explicit_permission_profile_mode = true;    chat.config        .permissions        .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(            PermissionProfile::workspace_write(),            ActivePermissionProfile::new(":workspace"),        ))        .expect("set active profile");    chat.open_permissions_popup();    assert_chatwidget_snapshot!(        "profile_permissions_selection_popup",        render_bottom_popup(&chat, /*width*/ 80)    );}#[tokio::test]async fn profile_permissions_selection_popup_with_custom_profiles_snapshot() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.explicit_permission_profile_mode = true;    chat.config.custom_permission_profiles = vec![        CustomPermissionProfileSummary {            id: "locked-down".to_string(),            description: Some("Inspect and patch only approved workspace files.".to_string()),        },        CustomPermissionProfileSummary {            id: "web-enabled".to_string(),            description: Some("Workspace profile with network access.".to_string()),        },    ];    chat.config        .permissions        .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(            PermissionProfile::workspace_write(),            ActivePermissionProfile::new("locked-down"),        ))        .expect("set active profile");    chat.open_permissions_popup();    assert_chatwidget_snapshot!(        "profile_permissions_selection_popup_with_custom_profiles",        render_bottom_popup(&chat, /*width*/ 80)    );}#[tokio::test]async fn profile_permissions_selection_emits_named_profile_event_only() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config.explicit_permission_profile_mode = true;    chat.config        .permissions        .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(            PermissionProfile::workspace_write(),            ActivePermissionProfile::new(":workspace"),        ))        .expect("set active profile");    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();    assert_eq!(events.len(), 1);    assert!(matches!(        &events[0],        AppEvent::SelectPermissionProfile(PermissionProfileSelection {            profile_id,            approval_policy: Some(AskForApproval::OnRequest),            approvals_reviewer: Some(ApprovalsReviewer::User),            display_label,        }) if profile_id == ":workspace" && display_label == "Ask for approval"    ));}#[tokio::test]async fn profile_permissions_selection_emits_active_custom_profile() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.explicit_permission_profile_mode = true;    chat.config.custom_permission_profiles = vec![CustomPermissionProfileSummary {        id: "locked-down".to_string(),        description: None,    }];    chat.config        .permissions        .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(            PermissionProfile::workspace_write(),            ActivePermissionProfile::new("locked-down"),        ))        .expect("set active profile");    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();    assert_eq!(events.len(), 1);    assert!(matches!(        &events[0],        AppEvent::SelectPermissionProfile(PermissionProfileSelection {            profile_id,            approval_policy: None,            approvals_reviewer: None,            display_label,        }) if profile_id == "locked-down" && display_label == "locked-down"    ));}#[tokio::test]async fn profile_permissions_selection_emits_auto_review_mode_event() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config.explicit_permission_profile_mode = true;    chat.config        .permissions        .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(            PermissionProfile::workspace_write(),            ActivePermissionProfile::new(":workspace"),        ))        .expect("set active profile");    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Down));    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();    assert_eq!(events.len(), 1);    assert!(matches!(        &events[0],        AppEvent::SelectPermissionProfile(PermissionProfileSelection {            profile_id,            approval_policy: Some(AskForApproval::OnRequest),            approvals_reviewer: Some(ApprovalsReviewer::AutoReview),            display_label,        }) if profile_id == ":workspace" && display_label == "Approve for me"    ));}#[tokio::test]async fn profile_permissions_full_access_opens_confirmation() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.explicit_permission_profile_mode = true;    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false);    chat.config.notices.hide_full_access_warning = None;    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Up));    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();    assert_eq!(events.len(), 1);    assert!(matches!(        &events[0],        AppEvent::OpenFullAccessConfirmation {            preset,            return_to_permissions: true,            profile_selection: Some(PermissionProfileSelection {                profile_id,                approval_policy: Some(AskForApproval::Never),                approvals_reviewer: Some(ApprovalsReviewer::User),                display_label,            }),        } if preset.id == "full-access"            && profile_id == ":danger-no-sandbox"            && display_label == "Full Access"    ));}#[cfg(target_os = "windows")]#[tokio::test]#[serial]async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.notices.hide_full_access_warning = None;    chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ true);    chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false);    chat.open_approvals_popup();    let popup = render_bottom_popup(&chat, /*width*/ 80);    assert!(        popup.contains("Ask for approval (non-admin sandbox)"),        "expected degraded sandbox label in approvals popup: {popup}"    );    assert!(        popup.contains("/setup-default-sandbox"),        "expected setup hint in approvals popup: {popup}"    );    assert!(        popup.contains("non-admin sandbox"),        "expected degraded sandbox note in approvals popup: {popup}"    );}#[tokio::test]async fn preset_matching_accepts_workspace_write_with_extra_roots() {    let preset = builtin_approval_presets()        .into_iter()        .find(|p| p.id == "auto")        .expect("auto preset exists");    let current_profile = app_server_workspace_write_profile(test_path_buf("/tmp/extra").abs());    let cwd = test_path_buf("/tmp/project").abs();    assert!(        ChatWidget::preset_matches_current(            AskForApproval::OnRequest,            &current_profile,            cwd.as_path(),            &preset        ),        "WorkspaceWrite with extra roots should still match the Ask for approval preset"    );    assert!(        !ChatWidget::preset_matches_current(            AskForApproval::Never,            &current_profile,            cwd.as_path(),            &preset        ),        "approval mismatch should prevent matching the preset"    );}#[tokio::test]async fn preset_matching_does_not_treat_non_cwd_writable_profile_as_read_only() {    let preset = builtin_approval_presets()        .into_iter()        .find(|p| p.id == "read-only")        .expect("read-only preset exists");    let current_profile: PermissionProfile = PermissionProfile::Managed {        network: NetworkSandboxPolicy::Restricted,        file_system: ManagedFileSystemPermissions::Restricted {            entries: vec![                FileSystemSandboxEntry {                    path: FileSystemPath::Special {                        value: FileSystemSpecialPath::Root,                    },                    access: FileSystemAccessMode::Read,                },                FileSystemSandboxEntry {                    path: FileSystemPath::Path {                        path: test_path_buf("/tmp/writable").abs(),                    },                    access: FileSystemAccessMode::Write,                },            ],            glob_scan_max_depth: None,        },    };    let cwd = test_path_buf("/tmp/project").abs();    assert!(        !ChatWidget::preset_matches_current(            AskForApproval::OnRequest,            &current_profile,            cwd.as_path(),            &preset        ),        "profiles with any writable root should not be classified as Read Only"    );}#[tokio::test]async fn full_access_confirmation_popup_snapshot() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let preset = builtin_approval_presets()        .into_iter()        .find(|preset| preset.id == "full-access")        .expect("full access preset");    chat.open_full_access_confirmation(        preset, /*return_to_permissions*/ false, /*profile_selection*/ None,    );    let popup = render_bottom_popup(&chat, /*width*/ 80);    assert_chatwidget_snapshot!("full_access_confirmation_popup", popup);}#[cfg(target_os = "windows")]#[tokio::test]async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let preset = builtin_approval_presets()        .into_iter()        .find(|preset| preset.id == "auto")        .expect("auto preset");    chat.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        popup.contains("requires Administrator permissions"),        "expected auto mode prompt to mention Administrator permissions, popup: {popup}"    );    assert!(        popup.contains("Use non-admin sandbox"),        "expected auto mode prompt to include non-admin fallback option, popup: {popup}"    );}#[cfg(target_os = "windows")]#[tokio::test]async fn startup_prompts_for_windows_sandbox_when_agent_requested() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false);    chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false);    chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ true);    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        popup.contains("requires Administrator permissions"),        "expected startup prompt to mention Administrator permissions: {popup}"    );    assert!(        popup.contains("Set up default sandbox"),        "expected startup prompt to offer default sandbox setup: {popup}"    );    assert!(        popup.contains("Use non-admin sandbox"),        "expected startup prompt to offer non-admin fallback: {popup}"    );    assert!(        popup.contains("Quit"),        "expected startup prompt to offer quit action: {popup}"    );}#[cfg(target_os = "windows")]#[tokio::test]async fn startup_windows_sandbox_prompt_blocks_disallowed_unelevated_fallback() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false);    chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false);    chat.config.config_layer_stack =        windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]);    chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ true);    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        popup.contains("Your organization requires the default Codex agent sandbox"),        "expected required sandbox prompt copy: {popup}"    );    assert!(        !popup.contains("Use non-admin sandbox"),        "expected required sandbox prompt to hide non-admin fallback: {popup}"    );}#[tokio::test]async fn windows_sandbox_required_enable_prompt_snapshot() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.config_layer_stack =        windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]);    let preset = builtin_approval_presets()        .into_iter()        .find(|preset| preset.id == "auto")        .expect("auto preset");    chat.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);    assert_chatwidget_snapshot!(        "windows_sandbox_required_enable_prompt",        render_bottom_popup(&chat, /*width*/ 120)    );}#[tokio::test]async fn windows_sandbox_required_enable_prompt_reopens_on_cancel_when_unelevated_allowed() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated);    chat.config.config_layer_stack = windows_sandbox_requirements_stack(vec![        WindowsSandboxModeToml::Elevated,        WindowsSandboxModeToml::Unelevated,    ]);    let preset = builtin_approval_presets()        .into_iter()        .find(|preset| preset.id == "auto")        .expect("auto preset");    chat.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);    chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    assert!(matches!(        rx.try_recv(),        Ok(AppEvent::OpenWindowsSandboxEnablePrompt { .. })    ));}#[tokio::test]async fn required_windows_sandbox_setup_defers_configured_initial_prompt() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let initial_prompt = "fix required sandbox startup".to_string();    chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated);    chat.config.config_layer_stack = windows_sandbox_requirements_stack(vec![        WindowsSandboxModeToml::Elevated,        WindowsSandboxModeToml::Unelevated,    ]);    chat.initial_user_message =        create_initial_user_message(Some(initial_prompt.clone()), Vec::new(), Vec::new());    chat.handle_thread_session(crate::session_state::ThreadSessionState {        thread_id: ThreadId::new(),        forked_from_id: None,        fork_parent_title: None,        thread_name: None,        model: "gpt-test".to_string(),        model_provider_id: "test-provider".to_string(),        service_tier: None,        approval_policy: AskForApproval::OnRequest,        approvals_reviewer: ApprovalsReviewer::User,        permission_profile: PermissionProfile::workspace_write(),        active_permission_profile: None,        cwd: test_project_path().abs(),        runtime_workspace_roots: Vec::new(),        instruction_source_paths: Vec::new(),        reasoning_effort: None,        collaboration_mode: None,        personality: None,        message_history: None,        network_proxy: None,        rollout_path: Some(PathBuf::new()),    });    drain_insert_history(&mut rx);    assert!(chat.initial_user_message.is_some());    while let Ok(op) = op_rx.try_recv() {        assert!(            !matches!(op, Op::UserTurn { .. }),            "required sandbox setup should hold the configured initial prompt"        );    }    chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    chat.submit_initial_user_message_if_pending();    let Op::UserTurn { items, .. } = next_submit_op(&mut op_rx) else {        panic!("expected initial prompt submission after setup is no longer required");    };    assert_eq!(        items,        vec![UserInput::Text {            text: initial_prompt,            text_elements: Vec::new(),        }]    );}#[tokio::test]async fn windows_sandbox_required_fallback_prompt_snapshot() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.config_layer_stack =        windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]);    let preset = builtin_approval_presets()        .into_iter()        .find(|preset| preset.id == "auto")        .expect("auto preset");    chat.open_windows_sandbox_fallback_prompt(preset, /*profile_selection*/ None);    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert_chatwidget_snapshot!("windows_sandbox_required_fallback_prompt", popup);}#[cfg(target_os = "windows")]#[tokio::test]async fn startup_does_not_prompt_for_windows_sandbox_when_not_requested() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false);    chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false);    chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ false);    assert!(        chat.bottom_pane.no_modal_or_popup_active(),        "expected no startup sandbox NUX popup when startup trigger is false"    );}#[tokio::test]async fn approvals_popup_shows_disabled_presets() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config.permissions.approval_policy = Constrained::new(        AskForApproval::OnRequest.to_core(),        |candidate| match AskForApproval::from(*candidate) {            AskForApproval::OnRequest => Ok(()),            _ => Err(invalid_value(                candidate.to_string(),                "this message should be printed in the description",            )),        },    )    .expect("construct constrained approval policy");    chat.open_approvals_popup();    let width = 80;    let height = chat.desired_height(width);    let mut terminal =        ratatui::Terminal::new(VT100Backend::new(width, height)).expect("create terminal");    terminal.set_viewport_area(Rect::new(0, 0, width, height));    terminal        .draw(|f| chat.render(f.area(), f.buffer_mut()))        .expect("render approvals popup");    let screen = terminal.backend().vt100().screen().contents();    let collapsed = screen.split_whitespace().collect::<Vec<_>>().join(" ");    assert!(        collapsed.contains("(disabled)"),        "disabled preset label should be shown"    );    assert!(        collapsed.contains("this message should be printed in the description"),        "disabled preset reason should be shown"    );}#[tokio::test]async fn approvals_popup_navigation_skips_disabled() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false);    chat.config.permissions.approval_policy = Constrained::new(        AskForApproval::OnRequest.to_core(),        |candidate| match AskForApproval::from(*candidate) {            AskForApproval::OnRequest => Ok(()),            _ => Err(invalid_value(candidate.to_string(), "[on-request]")),        },    )    .expect("construct constrained approval policy");    chat.open_approvals_popup();    let popup = render_bottom_popup(&chat, /*width*/ 80);    let mut disabled_shortcut = None;    let mut row_number = 0;    for line in popup.lines() {        let row = line            .trim_start()            .strip_prefix('\u{203a}')            .unwrap_or_else(|| line.trim_start())            .trim_start();        let mut chars = row.chars();        let has_numeric_shortcut =            chars.next().is_some_and(|ch| ch.is_ascii_digit()) && chars.next() == Some('.');        if has_numeric_shortcut || row.contains("(disabled)") {            row_number += 1;            if row.contains("(disabled)") {                disabled_shortcut = char::from_digit(row_number, 10);                break;            }        }    }    let disabled_shortcut = disabled_shortcut        .unwrap_or_else(|| panic!("expected at least one disabled selection row: {popup}"));    for _ in 0..10 {        chat.handle_key_event(KeyEvent::from(KeyCode::Down));        let popup = render_bottom_popup(&chat, /*width*/ 80);        let selected_disabled = popup            .lines()            .find(|line| line.trim_start().starts_with('\u{203a}'))            .expect("expected a selected selection row")            .contains("(disabled)");        assert!(            !selected_disabled,            "navigation should skip disabled rows: {popup}"        );    }    // Press the hidden numeric shortcut for a disabled row; it should not close    // the popup or accept the preset.    chat.handle_key_event(KeyEvent::from(KeyCode::Char(disabled_shortcut)));    // Ensure the popup remains open and no selection actions were sent.    let screen = render_bottom_popup(&chat, /*width*/ 80);    assert!(        screen.contains("Update Model Permissions"),        "popup should remain open after selecting a disabled entry"    );    assert!(        op_rx.try_recv().is_err(),        "no actions should be dispatched yet"    );    assert!(rx.try_recv().is_err(), "no history should be emitted");    // Press Enter; selection should land on an enabled preset and dispatch updates.    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let mut app_events = Vec::new();    while let Ok(ev) = rx.try_recv() {        app_events.push(ev);    }    assert!(        app_events.iter().any(|ev| matches!(            ev,            AppEvent::CodexOp(Op::OverrideTurnContext {                approval_policy: Some(AskForApproval::OnRequest),                personality: None,                ..            })        )),        "enter should select an enabled preset"    );    assert!(        !app_events.iter().any(|ev| matches!(            ev,            AppEvent::CodexOp(Op::OverrideTurnContext {                approval_policy: Some(AskForApproval::Never),                personality: None,                ..            })        )),        "disabled preset should not be selected"    );}#[tokio::test]async fn permissions_selection_emits_history_cell_when_selection_changes() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config.notices.hide_full_access_warning = Some(true);    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Down));    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let cells = drain_insert_history(&mut rx);    assert_eq!(        cells.len(),        1,        "expected one permissions selection history cell"    );    let rendered = lines_to_single_string(&cells[0]);    assert!(        rendered.contains("Permissions updated to"),        "expected permissions selection history message, got: {rendered}"    );}#[tokio::test]async fn permissions_selection_history_snapshot_after_mode_switch() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false);    chat.config.notices.hide_full_access_warning = Some(true);    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Down));    #[cfg(target_os = "windows")]    chat.handle_key_event(KeyEvent::from(KeyCode::Down));    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1, "expected one mode-switch history cell");    assert_chatwidget_snapshot!(        "permissions_selection_history_after_mode_switch",        lines_to_single_string(&cells[0])    );}#[tokio::test]async fn permissions_selection_history_snapshot_full_access_to_default() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config.notices.hide_full_access_warning = Some(true);    chat.config        .permissions        .approval_policy        .set(AskForApproval::Never.to_core())        .expect("set approval policy");    chat.config        .permissions        .set_permission_profile(PermissionProfile::Disabled)        .expect("set permission profile");    chat.open_permissions_popup();    let popup = render_bottom_popup(&chat, /*width*/ 120);    chat.handle_key_event(KeyEvent::from(KeyCode::Up));    if popup.contains("Approve for me") {        chat.handle_key_event(KeyEvent::from(KeyCode::Up));    }    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1, "expected one mode-switch history cell");    #[cfg(target_os = "windows")]    insta::with_settings!({ snapshot_suffix => "windows" }, {        assert_chatwidget_snapshot!(            "permissions_selection_history_full_access_to_default",            lines_to_single_string(&cells[0])        );    });    #[cfg(not(target_os = "windows"))]    assert_chatwidget_snapshot!(        "permissions_selection_history_full_access_to_default",        lines_to_single_string(&cells[0])    );}#[tokio::test]async fn permissions_selection_emits_history_cell_when_current_is_selected() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())        .expect("set approval policy");    chat.config        .permissions        .set_permission_profile(PermissionProfile::workspace_write())        .expect("set permission profile");    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let cells = drain_insert_history(&mut rx);    assert_eq!(        cells.len(),        1,        "expected history cell even when selecting current permissions"    );    let rendered = lines_to_single_string(&cells[0]);    assert!(        rendered.contains("Permissions updated to"),        "expected permissions update history message, got: {rendered}"    );}#[tokio::test]async fn permissions_selection_hides_auto_review_when_feature_disabled() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false);    chat.config.notices.hide_full_access_warning = Some(true);    chat.open_permissions_popup();    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        !popup.contains("Approve for me"),        "expected Approve for me to stay hidden until the feature is enabled: {popup}"    );}#[tokio::test]async fn permissions_selection_hides_auto_review_when_feature_disabled_even_if_auto_review_is_active() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false);    chat.config.notices.hide_full_access_warning = Some(true);    chat.config.approvals_reviewer = ApprovalsReviewer::AutoReview;    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())        .expect("set approval policy");    chat.config        .permissions        .set_permission_profile(PermissionProfile::workspace_write())        .expect("set permission profile");    chat.open_permissions_popup();    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        !popup.contains("Approve for me"),        "expected Approve for me to stay hidden when the feature is disabled: {popup}"    );}#[tokio::test]async fn permissions_selection_marks_auto_review_current_after_session_configured() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config.notices.hide_full_access_warning = Some(true);    let _ = chat        .config        .features        .set_enabled(Feature::GuardianApproval, /*enabled*/ true);    chat.handle_thread_session(crate::session_state::ThreadSessionState {        thread_id: ThreadId::new(),        forked_from_id: None,        fork_parent_title: None,        thread_name: None,        model: "gpt-test".to_string(),        model_provider_id: "test-provider".to_string(),        service_tier: None,        approval_policy: AskForApproval::OnRequest,        approvals_reviewer: ApprovalsReviewer::AutoReview,        permission_profile: PermissionProfile::workspace_write(),        active_permission_profile: None,        cwd: test_project_path().abs(),        runtime_workspace_roots: Vec::new(),        instruction_source_paths: Vec::new(),        reasoning_effort: None,        collaboration_mode: None,        personality: None,        message_history: None,        network_proxy: None,        rollout_path: Some(PathBuf::new()),    });    chat.open_permissions_popup();    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        popup.contains("Approve for me (current)"),        "expected Approve for me to be current after SessionConfigured sync: {popup}"    );}#[tokio::test]async fn permissions_selection_marks_auto_review_current_with_custom_workspace_write_details() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config.notices.hide_full_access_warning = Some(true);    let _ = chat        .config        .features        .set_enabled(Feature::GuardianApproval, /*enabled*/ true);    let extra_root = test_path_buf("/tmp/guardian-approvals-extra").abs();    let cwd = test_project_path().abs();    let permission_profile = app_server_workspace_write_profile(extra_root);    chat.handle_thread_session(crate::session_state::ThreadSessionState {        thread_id: ThreadId::new(),        forked_from_id: None,        fork_parent_title: None,        thread_name: None,        model: "gpt-test".to_string(),        model_provider_id: "test-provider".to_string(),        service_tier: None,        approval_policy: AskForApproval::OnRequest,        approvals_reviewer: ApprovalsReviewer::AutoReview,        permission_profile,        active_permission_profile: None,        cwd,        runtime_workspace_roots: Vec::new(),        instruction_source_paths: Vec::new(),        reasoning_effort: None,        collaboration_mode: None,        personality: None,        message_history: None,        network_proxy: None,        rollout_path: Some(PathBuf::new()),    });    chat.open_permissions_popup();    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        popup.contains("Approve for me (current)"),        "expected Approve for me to be current even with custom workspace-write details: {popup}"    );}#[tokio::test]async fn permissions_selection_can_disable_auto_review() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config.notices.hide_full_access_warning = Some(true);    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true);    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())        .expect("set approval policy");    chat.config        .permissions        .set_permission_profile(PermissionProfile::workspace_write())        .expect("set permission profile");    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Up));    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();    assert!(        events.iter().any(|event| matches!(            event,            AppEvent::UpdateApprovalsReviewer(ApprovalsReviewer::User)        )),        "expected selecting Ask for approval from Approve for me to switch back to manual approval review: {events:?}"    );    assert!(        !events            .iter()            .any(|event| matches!(event, AppEvent::UpdateFeatureFlags { .. })),        "expected permissions selection to leave feature flags unchanged: {events:?}"    );}#[tokio::test]async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.config.notices.hide_full_access_warning = Some(true);    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true);    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())        .expect("set approval policy");    chat.config        .permissions        .set_permission_profile(PermissionProfile::workspace_write())        .expect("set permission profile");    chat.set_approvals_reviewer(ApprovalsReviewer::User);    chat.open_permissions_popup();    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        popup            .lines()            .any(|line| line.contains("(current)") && line.contains('›')),        "expected permissions popup to open with the current preset selected: {popup}"    );    chat.handle_key_event(KeyEvent::from(KeyCode::Down));    let popup = render_bottom_popup(&chat, /*width*/ 120);    assert!(        popup            .lines()            .any(|line| line.contains("Approve for me") && line.contains('›')),        "expected one Down from Ask for approval to select Approve for me: {popup}"    );    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let op = std::iter::from_fn(|| rx.try_recv().ok())        .find_map(|event| match event {            AppEvent::CodexOp(op @ Op::OverrideTurnContext { .. }) => Some(op),            _ => None,        })        .expect("expected OverrideTurnContext op");    assert_eq!(        op,        Op::OverrideTurnContext {            cwd: None,            approval_policy: Some(AskForApproval::OnRequest),            approvals_reviewer: Some(ApprovalsReviewer::AutoReview),            permission_profile: Some(PermissionProfile::workspace_write()),            active_permission_profile: Some(ActivePermissionProfile::new(                BUILT_IN_PERMISSION_PROFILE_WORKSPACE,            )),            windows_sandbox_level: None,            model: None,            effort: None,            summary: None,            service_tier: None,            collaboration_mode: None,            personality: None,        }    );    let active_permission_profile_update = std::iter::from_fn(|| rx.try_recv().ok())        .find_map(|event| match event {            AppEvent::UpdateActivePermissionProfile(active_permission_profile) => {                Some(active_permission_profile)            }            _ => None,        })        .expect("expected UpdateActivePermissionProfile event");    assert_eq!(        active_permission_profile_update,        ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)    );}#[tokio::test]async fn permissions_full_access_history_cell_emitted_only_after_confirmation() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    #[cfg(target_os = "windows")]    {        chat.config.notices.hide_world_writable_warning = Some(true);        chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));    }    chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false);    chat.config.notices.hide_full_access_warning = None;    chat.open_permissions_popup();    chat.handle_key_event(KeyEvent::from(KeyCode::Down));    #[cfg(target_os = "windows")]    chat.handle_key_event(KeyEvent::from(KeyCode::Down));    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let mut open_confirmation_event = None;    let mut cells_before_confirmation = Vec::new();    while let Ok(event) = rx.try_recv() {        match event {            AppEvent::InsertHistoryCell(cell) => {                cells_before_confirmation.push(cell.display_lines(/*width*/ 80));            }            AppEvent::OpenFullAccessConfirmation {                preset,                return_to_permissions,                profile_selection,            } => {                open_confirmation_event = Some((preset, return_to_permissions, profile_selection));            }            _ => {}        }    }    if cfg!(not(target_os = "windows")) {        assert!(            cells_before_confirmation.is_empty(),            "did not expect history cell before confirming full access"        );    }    let (preset, return_to_permissions, profile_selection) =        open_confirmation_event.expect("expected full access confirmation event");    chat.open_full_access_confirmation(preset, return_to_permissions, profile_selection);    let popup = render_bottom_popup(&chat, /*width*/ 80);    assert!(        popup.contains("Enable full access?"),        "expected full access confirmation popup, got: {popup}"    );    chat.handle_key_event(KeyEvent::from(KeyCode::Enter));    let cells_after_confirmation = drain_insert_history(&mut rx);    let total_history_cells = cells_before_confirmation.len() + cells_after_confirmation.len();    assert_eq!(        total_history_cells, 1,        "expected one full access history cell total"    );    let rendered = if !cells_before_confirmation.is_empty() {        lines_to_single_string(&cells_before_confirmation[0])    } else {        lines_to_single_string(&cells_after_confirmation[0])    };    assert!(        rendered.contains("Permissions updated to Full Access"),        "expected full access update history message, got: {rendered}"    );}