From 6f6e80bcddab56a4e6518eb5adef0079ed30bb2b Mon Sep 17 00:00:00 2001 From: jackz Date: Thu, 8 Oct 2026 16:50:17 +0000 Subject: [PATCH] Show AWS GovCloud guidance at startup and remember acknowledgment (#52176) ## Why GovCloud guidance was tied to Bedrock credential setup, so users with an already configured provider could miss it. ## What changed Check for GovCloud during TUI startup using the app server's resolved provider, including when connecting to a server. Require acknowledgment before proceeding and save `notice.hide_gov_cloud_guidance = true` to suppress the screen on subsequent launches. Quitting the guidance exits startup without saving acknowledgment. ## Testing Add a terminal integration test covering acknowledgment persistence, quitting without saving, and suppression after restart. Retain coverage for explicit acknowledgment and scrolling in short panes. GitOrigin-RevId: f09fd73a66fb02f94ebdd3c3b0dbe5712dfa9be7 --- codex-rs/config/src/types.rs | 2 + codex-rs/core/config.schema.json | 4 + codex-rs/tui/src/lib.rs | 30 ++++- codex-rs/tui/src/onboarding/bedrock.rs | 116 +++++++----------- codex-rs/tui/src/onboarding/bedrock_tests.rs | 41 ++----- codex-rs/tui/src/onboarding/mod.rs | 1 + .../tui/src/onboarding/onboarding_screen.rs | 64 ++++++++-- ...g__bedrock__tests__gov_cloud_checking.snap | 5 - codex-rs/tui/tests/suite/focus_palette.rs | 2 + .../tests/suite/gov_cloud_guidance_tests.rs | 62 ++++++++++ 10 files changed, 209 insertions(+), 118 deletions(-) delete mode 100644 codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__bedrock__tests__gov_cloud_checking.snap create mode 100644 codex-rs/tui/tests/suite/gov_cloud_guidance_tests.rs diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index f72ad7ac98..ab2a0aaec6 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -1007,6 +1007,8 @@ pub struct ExternalConfigMigrationPrompts { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct Notice { + /// Tracks whether the user has acknowledged the AWS GovCloud guidance. + pub hide_gov_cloud_guidance: Option, /// Tracks whether the user has acknowledged the full access warning prompt. pub hide_full_access_warning: Option, /// Tracks whether the user has acknowledged the Windows world-writable directories warning. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index b3fb6323ce..eee26647fa 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -3279,6 +3279,10 @@ "description": "Tracks whether the user has acknowledged the full access warning prompt.", "type": "boolean" }, + "hide_gov_cloud_guidance": { + "description": "Tracks whether the user has acknowledged the AWS GovCloud guidance.", + "type": "boolean" + }, "hide_gpt-5.1-codex-max_migration_prompt": { "description": "Tracks whether the user has seen the gpt-5.1-codex-max migration prompt", "type": "boolean" diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 226ecd5488..934d732d1b 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -9,11 +9,14 @@ use crate::legacy_core::config::ConfigBuilder; use crate::legacy_core::config::ConfigOverrides; use crate::legacy_core::config::ConfigTomlLoadResult; use crate::legacy_core::config::bootstrap_auth_config; +use crate::legacy_core::config::edit::ConfigEdit; +use crate::legacy_core::config::edit::ConfigEditsBuilder; use crate::legacy_core::config::load_config_toml_with_layer_stack; #[cfg(test)] use crate::legacy_core::config::resolve_bootstrap_http_client_factory; use crate::legacy_core::config::resolve_oss_provider; use crate::legacy_core::config::resolve_profile_v2_config_path; +use crate::onboarding::check_gov_cloud; use crate::session_resume::ResolveCwdOutcome; use crate::session_resume::ResumeCwdContext; use crate::session_resume::effective_resume_cwd_mode; @@ -267,6 +270,7 @@ pub(crate) mod test_backend; pub(crate) mod test_support; use crate::onboarding::onboarding_screen::OnboardingScreenArgs; +use crate::onboarding::onboarding_screen::run_gov_cloud_guidance; use crate::onboarding::onboarding_screen::run_onboarding_app; use crate::startup_hooks_review::StartupHooksReviewOutcome; use crate::startup_hooks_review::load_startup_hooks_review_entry; @@ -2000,6 +2004,11 @@ async fn run_ratatui_app( &session_selection, &loader_overrides, ); + // Connected servers resolve their own provider; the local config may differ. + let check_gov_cloud_guidance = config.notices.hide_gov_cloud_guidance != Some(true) + && (config.model_provider.is_amazon_bedrock() + || !matches!(app_server_target, AppServerTarget::Embedded)); + let guidance_request_handle = app_server.request_handle(); let startup_prefetch_started_at = Instant::now(); let startup_prefetch = startup_draft .run_until(&mut tui, async { @@ -2015,10 +2024,13 @@ async fn run_ratatui_app( Ok(Some(bootstrap)) }, load_startup_hooks_review_entry(hooks_request_handle, hooks_cwd), + async { + check_gov_cloud_guidance && check_gov_cloud(guidance_request_handle).await + }, ) }) .await; - let (startup_bootstrap, startup_hooks_entry) = match startup_prefetch { + let (startup_bootstrap, startup_hooks_entry, show_gov_cloud_guidance) = match startup_prefetch { Ok(startup_prefetch) => startup_prefetch, Err(err) => { shutdown_startup_session(Some(app_server), &mut terminal_restore_guard).await; @@ -2037,6 +2049,22 @@ async fn run_ratatui_app( } }; let startup_elapsed_before_app = startup_prefetch_started_at.elapsed(); + if show_gov_cloud_guidance { + if let Err(err) = run_gov_cloud_guidance(&mut tui).await { + shutdown_startup_session(Some(app_server), &mut terminal_restore_guard).await; + return Err(err); + } + if let Err(err) = ConfigEditsBuilder::for_config(&config) + .with_edits([ConfigEdit::SetPath { + segments: vec!["notice".into(), "hide_gov_cloud_guidance".into()], + value: toml_edit::value(true), + }]) + .apply() + .await + { + tracing::warn!(%err, "Failed to save GovCloud guidance acknowledgment"); + } + } let startup_hooks_review = maybe_run_startup_hooks_review( &mut app_server, &mut tui, diff --git a/codex-rs/tui/src/onboarding/bedrock.rs b/codex-rs/tui/src/onboarding/bedrock.rs index be6fe7151d..d1ea4d2464 100644 --- a/codex-rs/tui/src/onboarding/bedrock.rs +++ b/codex-rs/tui/src/onboarding/bedrock.rs @@ -1,4 +1,4 @@ -//! Amazon Bedrock credential discovery, setup, and post-login checks within authentication. +//! Amazon Bedrock credential setup and the startup GovCloud guidance screen. use super::auth::AuthModeWidget; use super::auth::SignInState; @@ -6,6 +6,7 @@ use super::auth::onboarding_request_id; use super::keys; use crate::key_hint::KeyBindingListExt; use crate::wrapping::word_wrap_lines; +use codex_app_server_client::AppServerRequestHandle; use codex_app_server_protocol::AwsCredentialType; use codex_app_server_protocol::BedrockAwsProfile; use codex_app_server_protocol::BedrockCheckGovCloudRequirementsParams; @@ -61,7 +62,6 @@ enum BedrockView { }, EnvironmentInstructions, Configuring(RequestId), - CheckingGovCloud(RequestId), // Render-clamped scroll offset shared with the auth widget's cloned state. GovCloudWarning(Arc), } @@ -98,10 +98,31 @@ enum BedrockMethod { enum BedrockAction { BackToAuth, Configure(BedrockCredential, String), - ContinueAfterGovCloudWarning, } impl BedrockState { + pub(super) fn guidance() -> Self { + Self { + view: BedrockView::GovCloudWarning(Arc::default()), + highlighted: 0, + profiles: Vec::new(), + environment_credentials: Vec::new(), + } + } + + pub(super) fn handle_guidance_key_event(&mut self, key_event: &KeyEvent) -> bool { + let BedrockView::GovCloudWarning(scroll) = &self.view else { + return false; + }; + let offset = scroll.load(Ordering::Relaxed); + if keys::MOVE_UP.is_pressed(*key_event) { + scroll.store(offset.saturating_sub(1), Ordering::Relaxed); + } else if keys::MOVE_DOWN.is_pressed(*key_event) { + scroll.store(offset.saturating_add(1), Ordering::Relaxed); + } + key_event.kind == KeyEventKind::Press && keys::CONFIRM.is_pressed(*key_event) + } + fn discovering(request_id: RequestId) -> Self { Self { view: BedrockView::Discovering(request_id), @@ -225,21 +246,6 @@ impl BedrockState { } fn handle_key_event(&mut self, key_event: &KeyEvent) -> Option { - match &self.view { - BedrockView::CheckingGovCloud(_) => return None, - BedrockView::GovCloudWarning(scroll) => { - let offset = scroll.load(Ordering::Relaxed); - if keys::MOVE_UP.is_pressed(*key_event) { - scroll.store(offset.saturating_sub(1), Ordering::Relaxed); - } else if keys::MOVE_DOWN.is_pressed(*key_event) { - scroll.store(offset.saturating_add(1), Ordering::Relaxed); - } - return (key_event.kind == KeyEventKind::Press - && keys::CONFIRM.is_pressed(*key_event)) - .then_some(BedrockAction::ContinueAfterGovCloudWarning); - } - _ => {} - } if keys::CANCEL.is_pressed(*key_event) { let leave_wizard = matches!( self.view, @@ -372,9 +378,6 @@ impl BedrockState { ]; let mut footer = Vec::new(); match &self.view { - BedrockView::CheckingGovCloud(_) => { - lines = vec!["Checking for AWS GovCloud...".dim().into()]; - } BedrockView::GovCloudWarning(_) => { lines = vec!["Using Codex with AWS GovCloud".bold().into(), "".into()]; lines.push(Line::from(vec![ @@ -493,10 +496,7 @@ impl BedrockState { self.render_methods(&mut lines); } } - if !matches!( - self.view, - BedrockView::Discovering(_) | BedrockView::CheckingGovCloud(_) - ) { + if !matches!(self.view, BedrockView::Discovering(_)) { footer.push("".into()); if !matches!(self.view, BedrockView::Configuring(_)) { footer.push(Line::from(vec![ @@ -676,44 +676,21 @@ impl BedrockState { } } -fn start_gov_cloud_check( - request_handle: codex_app_server_client::AppServerRequestHandle, - sign_in_state: std::sync::Arc>, - request_frame: crate::tui::FrameRequester, - state: &mut BedrockState, -) { - let request_id = onboarding_request_id(); - state.highlighted = 0; - state.view = BedrockView::CheckingGovCloud(request_id.clone()); - tokio::spawn(async move { - let result = tokio::time::timeout( - std::time::Duration::from_secs(/*secs*/ 15), - request_handle.request_typed::( - ClientRequest::BedrockCheckGovCloudRequirements { - request_id: request_id.clone(), - params: BedrockCheckGovCloudRequirementsParams {}, - }, - ), - ) - .await; - let mut guard = sign_in_state - .write() - .unwrap_or_else(PoisonError::into_inner); - let SignInState::Bedrock(state) = &mut *guard else { - return; - }; - if !matches!(&state.view, BedrockView::CheckingGovCloud(active) if *active == request_id) { - return; - } - match result { - Ok(Ok(response)) if response.is_gov_cloud => { - state.view = BedrockView::GovCloudWarning(Arc::default()); - } - Ok(Ok(_)) | Ok(Err(_)) | Err(_) => *guard = SignInState::BedrockConfigured, - } - drop(guard); - request_frame.schedule_frame(); - }); +pub(crate) async fn check_gov_cloud(request_handle: AppServerRequestHandle) -> bool { + match tokio::time::timeout( + std::time::Duration::from_secs(/*secs*/ 15), + request_handle.request_typed::( + ClientRequest::BedrockCheckGovCloudRequirements { + request_id: onboarding_request_id(), + params: BedrockCheckGovCloudRequirementsParams {}, + }, + ), + ) + .await + { + Ok(Ok(response)) => response.is_gov_cloud, + Ok(Err(_)) | Err(_) => false, + } } impl AuthModeWidget { @@ -787,9 +764,6 @@ impl AuthModeWidget { drop(guard); self.start_bedrock_setup(credential, region, fallback); } - Some(BedrockAction::ContinueAfterGovCloudWarning) => { - *guard = SignInState::BedrockConfigured; - } None => {} } } @@ -906,14 +880,8 @@ impl AuthModeWidget { match result { Ok(()) => { *error.write().unwrap_or_else(PoisonError::into_inner) = None; - if let SignInState::Bedrock(state) = &mut *guard { - start_gov_cloud_check( - request_handle, - sign_in_state.clone(), - request_frame.clone(), - state, - ); - } + // Startup shows GovCloud guidance after the selected provider is reloaded. + *guard = SignInState::BedrockConfigured; } Err(err) => { *error.write().unwrap_or_else(PoisonError::into_inner) = diff --git a/codex-rs/tui/src/onboarding/bedrock_tests.rs b/codex-rs/tui/src/onboarding/bedrock_tests.rs index 9d18a8259c..8f9f240587 100644 --- a/codex-rs/tui/src/onboarding/bedrock_tests.rs +++ b/codex-rs/tui/src/onboarding/bedrock_tests.rs @@ -3,30 +3,14 @@ use pretty_assertions::assert_eq; #[test] fn gov_cloud_guidance_requires_acknowledgement() { - let mut state = BedrockState::discovering(RequestId::Integer(1)); - state.view = BedrockView::CheckingGovCloud(RequestId::Integer(2)); - insta::assert_snapshot!("gov_cloud_checking", render_visible(&state)); - assert!( - state - .handle_key_event(&KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)) - .is_none() - ); - - state.view = BedrockView::GovCloudWarning(Arc::default()); + let mut state = BedrockState::guidance(); insta::assert_snapshot!("gov_cloud_warning", render_visible(&state)); - assert!( - state - .handle_key_event(&KeyEvent::new_with_kind( - KeyCode::Enter, - KeyModifiers::NONE, - KeyEventKind::Repeat, - )) - .is_none() - ); - assert!(matches!( - state.handle_key_event(&KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), - Some(BedrockAction::ContinueAfterGovCloudWarning) - )); + assert!(!state.handle_guidance_key_event(&KeyEvent::new_with_kind( + KeyCode::Enter, + KeyModifiers::NONE, + KeyEventKind::Repeat, + ))); + assert!(state.handle_guidance_key_event(&KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))); let area = Rect::new(0, 0, 72, 24); let mut buffer = Buffer::empty(area); @@ -41,8 +25,7 @@ fn gov_cloud_guidance_requires_acknowledgement() { #[test] fn gov_cloud_guidance_scrolls_in_short_panes() { - let mut state = BedrockState::discovering(RequestId::Integer(1)); - state.view = BedrockView::GovCloudWarning(Arc::default()); + let mut state = BedrockState::guidance(); let area = Rect::new(0, 0, 24, 6); let initial = render_visible_at(&state, area); insta::assert_snapshot!("gov_cloud_warning_short", initial); @@ -57,22 +40,20 @@ fn gov_cloud_guidance_scrolls_in_short_panes() { seen.extend(buffer.content.iter().map(ratatui::buffer::Cell::symbol)); assert!(render_visible_at(&state, area).contains("Acknowledge")); assert!( - state - .handle_key_event(&KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)) - .is_none() + !state.handle_guidance_key_event(&KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)) ); } assert!(seen.contains("Using Codex with AWS")); assert!(seen.contains("proceeding.")); assert!(seen.contains(GOV_CLOUD_GUIDANCE_URL)); for _ in 0..20 { - state.handle_key_event(&KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); + state.handle_guidance_key_event(&KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); render_visible_at(&state, area); } assert!(render_visible_at(&state, area).starts_with("Using Codex with AWS")); } assert_eq!(render_visible_at(&state, area), initial); - state.handle_key_event(&KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + state.handle_guidance_key_event(&KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); render_visible(&state); assert_eq!(render_visible_at(&state, area), initial); } diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs index 6a9ea96124..b1301991f1 100644 --- a/codex-rs/tui/src/onboarding/mod.rs +++ b/codex-rs/tui/src/onboarding/mod.rs @@ -1,5 +1,6 @@ mod auth; mod bedrock; +pub(crate) use bedrock::check_gov_cloud; mod keys; pub(crate) mod onboarding_screen; mod trust_directory; diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index 0ec1006eca..626e545637 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -1,7 +1,7 @@ //! Onboarding screen orchestration and top-level keyboard routing. //! //! The onboarding flow is a small state machine over visible steps -//! (welcome/auth/trust). This module decides which step receives key/paste +//! (welcome/auth/trust/GovCloud). This module decides which step receives key/paste //! events and enforces flow-level safety rules that cut across individual step //! widgets. Folder-entry consent reuses this same event loop for startup and in-app navigation. //! @@ -45,10 +45,12 @@ use crate::legacy_core::config::Config; use crate::onboarding::auth::AuthModeWidget; use crate::onboarding::auth::SignInOption; use crate::onboarding::auth::SignInState; +use crate::onboarding::bedrock::BedrockState; use crate::onboarding::keys; use crate::onboarding::trust_directory::TrustDirectorySelection; use crate::onboarding::trust_directory::TrustDirectoryWidget; use crate::onboarding::welcome::WelcomeWidget; +use crate::startup_draft::StartupCancelled; use crate::tui::FrameRequester; use crate::tui::OverlayInput; use crate::tui::Tui; @@ -69,6 +71,10 @@ enum Step { Welcome(WelcomeWidget), Auth(AuthModeWidget), TrustDirectory(TrustDirectoryWidget), + GovCloudGuidance { + state: BedrockState, + acknowledged: bool, + }, } pub(crate) trait KeyboardHandler { @@ -258,7 +264,7 @@ impl OnboardingScreen { // material so terminal selection is not interrupted by redraws. self.current_steps().into_iter().any(|step| match step { Step::Auth(widget) => widget.should_suppress_animations(), - Step::Welcome(_) | Step::TrustDirectory(_) => false, + Step::Welcome(_) | Step::TrustDirectory(_) | Step::GovCloudGuidance { .. } => false, }) } @@ -299,7 +305,7 @@ impl OnboardingScreen { fn auth_widget_mut(&mut self) -> Option<&mut AuthModeWidget> { self.steps.iter_mut().find_map(|step| match step { Step::Auth(widget) => Some(widget), - Step::Welcome(_) | Step::TrustDirectory(_) => None, + Step::Welcome(_) | Step::TrustDirectory(_) | Step::GovCloudGuidance { .. } => None, }) } @@ -356,7 +362,12 @@ impl KeyboardHandler for OnboardingScreen { // If the user cancels the auth menu, exit the app rather than // leave the user at a prompt in an unauthed state. self.should_exit = true; - } else if self.is_trust_step_active() { + } else if self.is_trust_step_active() + || self + .steps + .iter() + .any(|step| matches!(step, Step::GovCloudGuidance { .. })) + { self.should_exit = true; } self.is_done = true; @@ -424,7 +435,7 @@ impl WidgetRef for &OnboardingScreen { match step { Step::Welcome(widget) => widget.set_presentation(logo_presentation), Step::Auth(widget) => widget.set_animations_suppressed(suppress_animations), - Step::TrustDirectory(_) => {} + Step::TrustDirectory(_) | Step::GovCloudGuidance { .. } => {} } } @@ -498,12 +509,18 @@ impl KeyboardHandler for Step { Step::Welcome(widget) => widget.handle_key_event(key_event), Step::Auth(widget) => widget.handle_key_event(key_event), Step::TrustDirectory(widget) => widget.handle_key_event(key_event), + Step::GovCloudGuidance { + state, + acknowledged, + } => { + *acknowledged = state.handle_guidance_key_event(&key_event); + } } } fn handle_paste(&mut self, pasted: String) { match self { - Step::Welcome(_) => {} + Step::Welcome(_) | Step::GovCloudGuidance { .. } => {} Step::Auth(widget) => widget.handle_paste(pasted), Step::TrustDirectory(widget) => widget.handle_paste(pasted), } @@ -516,6 +533,13 @@ impl StepStateProvider for Step { Step::Welcome(w) => w.get_step_state(), Step::Auth(w) => w.get_step_state(), Step::TrustDirectory(w) => w.get_step_state(), + Step::GovCloudGuidance { acknowledged, .. } => { + if *acknowledged { + StepState::Complete + } else { + StepState::InProgress + } + } } } } @@ -532,10 +556,32 @@ impl WidgetRef for Step { Step::TrustDirectory(widget) => { widget.render_ref(area, buf); } + Step::GovCloudGuidance { state, .. } => state.render(area, buf, /*error*/ None), } } } +pub(crate) async fn run_gov_cloud_guidance(tui: &mut Tui) -> Result<()> { + let screen = OnboardingScreen { + request_frame: tui.frame_requester(), + steps: vec![Step::GovCloudGuidance { + state: BedrockState::guidance(), + acknowledged: false, + }], + remote_trust_key: None, + is_done: false, + should_exit: false, + }; + let result = run_onboarding_screen( + screen, /*app_server_request_handle*/ None, /*app_server*/ None, tui, + ) + .await?; + if result.should_exit { + return Err(std::io::Error::new(std::io::ErrorKind::Interrupted, StartupCancelled).into()); + } + Ok(()) +} + pub(crate) async fn run_onboarding_app( args: OnboardingScreenArgs, app_server: Option<&mut AppServerSession>, @@ -701,12 +747,14 @@ async fn run_onboarding_screen_inner( let _ = tui.terminal.clear(); did_full_clear_after_success = true; } - let _ = tui.draw(u16::MAX, |frame| { + tui.draw(u16::MAX, |frame| { frame.render_widget_ref(&onboarding_screen, frame.area()); - }); + })?; } TuiEvent::Mouse(_) => {} } + } else { + return Err(std::io::Error::new(std::io::ErrorKind::Interrupted, StartupCancelled).into()); } } event = async { diff --git a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__bedrock__tests__gov_cloud_checking.snap b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__bedrock__tests__gov_cloud_checking.snap deleted file mode 100644 index 999544a7fd..0000000000 --- a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__bedrock__tests__gov_cloud_checking.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: tui/src/onboarding/bedrock_tests.rs -expression: render_visible(&state) ---- -Checking for AWS GovCloud... diff --git a/codex-rs/tui/tests/suite/focus_palette.rs b/codex-rs/tui/tests/suite/focus_palette.rs index fa47056984..645f115c35 100644 --- a/codex-rs/tui/tests/suite/focus_palette.rs +++ b/codex-rs/tui/tests/suite/focus_palette.rs @@ -24,6 +24,8 @@ const FOCUS_PROBE_INPUT: &str = "focus-palette-24527"; #[path = "external_editor_tests.rs"] mod external_editor; +#[path = "gov_cloud_guidance_tests.rs"] +mod gov_cloud_guidance; #[path = "tui_mode_picker_tests.rs"] mod tui_mode_picker; diff --git a/codex-rs/tui/tests/suite/gov_cloud_guidance_tests.rs b/codex-rs/tui/tests/suite/gov_cloud_guidance_tests.rs new file mode 100644 index 0000000000..ee79c7ca58 --- /dev/null +++ b/codex-rs/tui/tests/suite/gov_cloud_guidance_tests.rs @@ -0,0 +1,62 @@ +//! Verify startup acknowledgment, quit without saving, and restart suppression. + +use super::*; +use pretty_assertions::assert_eq; + +#[test] +fn gov_cloud_startup_persists_only_acknowledgment() -> Result<()> { + let cwd = codex_utils_cargo_bin::repo_root()?; + let args = [ + "--no-daemon", + "-c", + "features.api_key_model_discovery=false", + ]; + for action in ["quit", "acknowledge"] { + let home = tempfile::tempdir()?; + write_test_config(home.path(), &cwd)?; + let path = home.path().join("config.toml"); + let config = std::fs::read_to_string(&path)?.replace( + "model_provider = \"openai\"", + "model_provider = \"amazon-bedrock\"", + ); + let config = format!( + "{config}\n[model_providers.amazon-bedrock]\nbase_url = 'https://bedrock-mantle.us-gov-west-1.api.aws/openai/v1'\n" + ); + std::fs::write(&path, &config)?; + let mut terminal = PtyCodex::start(&cwd, home, &args)?; + terminal.wait_for_screen("Using Codex with AWS GovCloud")?; + let original = std::fs::read_to_string(&path)?; + let deadline = Instant::now() + STARTUP_TIMEOUT; + loop { + // Startup drains pending keys after drawing the guidance; retry until it handles one. + terminal.write_input(if action == "quit" { b"q" } else { b"\r" })?; + terminal.read_output(Duration::from_millis(/*millis*/ 50))?; + if action == "quit" { + if let Some(status) = terminal.child.try_wait()? { + ensure!(status.success(), "quit exited with {status}"); + break; + } + } else if terminal.screen_contains("OpenAI Codex") { + break; + } + ensure!(Instant::now() < deadline, "{action} did not complete"); + } + let saved = std::fs::read_to_string(&path)?; + if action == "acknowledge" { + let config: toml::Value = toml::from_str(&saved)?; + assert_eq!( + config["notice"]["hide_gov_cloud_guidance"].as_bool(), + Some(true) + ); + let home = std::mem::replace(&mut terminal._codex_home, tempfile::tempdir()?); + drop(terminal); + let mut restarted = PtyCodex::start(&cwd, home, &args)?; + // The startup draft says "loading"; the actual model appears after guidance. + restarted.wait_for_screen("gpt-5.6-terra")?; + assert!(!restarted.screen_contains("Using Codex with AWS GovCloud")); + } else { + assert_eq!(saved, original); + } + } + Ok(()) +}