mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
## What changed - Group consecutive successful agent and unified-exec startup commands into a compact `Ran N commands` entry while preserving their full transcript. - Flush command groups at interaction boundaries, after failures, and after 32 completed commands. Keep manual shell commands and failed or declined commands visible. - Apply the same grouping rules when replaying thread history without duplicating command starts or disturbing unrelated active commands. ## Testing - Add TUI coverage for live and replayed grouping, transcript preservation, failures, overlapping commands, group limits, and interaction boundaries. GitOrigin-RevId: d6602a20201644012fcff3d6cecf4690024383a9
1223 lines
45 KiB
Rust
1223 lines
45 KiB
Rust
use super::*;
|
||
use crate::legacy_core::config::PermissionProfileCatalogEntry;
|
||
use codex_protocol::models::ActivePermissionProfile;
|
||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||
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,
|
||
missing_path_behavior: None,
|
||
},
|
||
FileSystemSandboxEntry {
|
||
path: FileSystemPath::Special {
|
||
value: FileSystemSpecialPath::ProjectRoots { subpath: None },
|
||
},
|
||
access: FileSystemAccessMode::Write,
|
||
missing_path_behavior: None,
|
||
},
|
||
FileSystemSandboxEntry {
|
||
path: FileSystemPath::Special {
|
||
value: FileSystemSpecialPath::SlashTmp,
|
||
},
|
||
access: FileSystemAccessMode::Write,
|
||
missing_path_behavior: None,
|
||
},
|
||
FileSystemSandboxEntry {
|
||
path: FileSystemPath::Special {
|
||
value: FileSystemSpecialPath::Tmpdir,
|
||
},
|
||
access: FileSystemAccessMode::Write,
|
||
missing_path_behavior: None,
|
||
},
|
||
FileSystemSandboxEntry {
|
||
path: FileSystemPath::Path { path: extra_root },
|
||
access: FileSystemAccessMode::Write,
|
||
missing_path_behavior: None,
|
||
},
|
||
],
|
||
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),
|
||
sandbox_private_desktop: None,
|
||
}),
|
||
..Default::default()
|
||
};
|
||
requirements_stack(requirements_toml)
|
||
}
|
||
|
||
fn requirements_stack(requirements_toml: codex_config::ConfigRequirementsToml) -> ConfigLayerStack {
|
||
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.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_disallowed_full_access_snapshot() {
|
||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||
chat.config.explicit_permission_profile_mode = true;
|
||
chat.config.config_layer_stack = requirements_stack(codex_config::ConfigRequirementsToml {
|
||
allowed_sandbox_modes: Some(vec![
|
||
codex_config::SandboxModeRequirement::ReadOnly,
|
||
codex_config::SandboxModeRequirement::WorkspaceWrite,
|
||
]),
|
||
..Default::default()
|
||
});
|
||
|
||
chat.open_permissions_popup();
|
||
|
||
assert_chatwidget_snapshot!(
|
||
"profile_permissions_selection_popup_with_disallowed_full_access",
|
||
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![
|
||
PermissionProfileCatalogEntry {
|
||
id: "locked-down".to_string(),
|
||
description: Some("Inspect and patch only approved workspace files.".to_string()),
|
||
allowed: true,
|
||
},
|
||
PermissionProfileCatalogEntry {
|
||
id: "web-enabled".to_string(),
|
||
description: Some("Workspace profile with network access.".to_string()),
|
||
allowed: false,
|
||
},
|
||
];
|
||
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![PermissionProfileCatalogEntry {
|
||
id: "locked-down".to_string(),
|
||
description: None,
|
||
allowed: true,
|
||
}];
|
||
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_always_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 = Some(true);
|
||
|
||
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 == BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS
|
||
&& 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.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,
|
||
¤t_profile,
|
||
cwd.as_path(),
|
||
&preset
|
||
),
|
||
"WorkspaceWrite with extra roots should still match the Ask for approval preset"
|
||
);
|
||
assert!(
|
||
!ChatWidget::preset_matches_current(
|
||
AskForApproval::Never,
|
||
¤t_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,
|
||
missing_path_behavior: None,
|
||
},
|
||
FileSystemSandboxEntry {
|
||
path: FileSystemPath::Path {
|
||
path: test_path_buf("/tmp/writable").abs(),
|
||
},
|
||
access: FileSystemAccessMode::Write,
|
||
missing_path_behavior: None,
|
||
},
|
||
],
|
||
glob_scan_max_depth: None,
|
||
},
|
||
};
|
||
let cwd = test_path_buf("/tmp/project").abs();
|
||
|
||
assert!(
|
||
!ChatWidget::preset_matches_current(
|
||
AskForApproval::OnRequest,
|
||
¤t_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
|
||
.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.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true);
|
||
chat.on_task_started();
|
||
chat.dispatch_command(SlashCommand::Permissions);
|
||
let command = begin_exec(&mut chat, "call-permissions", "printf before");
|
||
end_exec(&mut chat, command, "before\n", "", /*exit_code*/ 0);
|
||
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(),
|
||
2,
|
||
"expected command and permissions selection history cells"
|
||
);
|
||
assert!(lines_to_single_string(&cells[0]).contains("Ran printf before"));
|
||
let rendered = lines_to_single_string(&cells[1]);
|
||
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.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 (preset, return_to_permissions, profile_selection) =
|
||
std::iter::from_fn(|| rx.try_recv().ok())
|
||
.find_map(|event| match event {
|
||
AppEvent::OpenFullAccessConfirmation {
|
||
preset,
|
||
return_to_permissions,
|
||
profile_selection,
|
||
} => Some((preset, return_to_permissions, profile_selection)),
|
||
_ => None,
|
||
})
|
||
.expect("expected full access confirmation event");
|
||
chat.open_full_access_confirmation(preset, return_to_permissions, profile_selection);
|
||
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
|
||
.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.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.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));
|
||
}
|
||
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));
|
||
}
|
||
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.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::AutoReview);
|
||
|
||
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.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.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}"
|
||
);
|
||
}
|