use super::*;use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditOutcome;use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse;use codex_app_server_protocol::RateLimitResetCreditsSummary;use uuid::Uuid;const TEST_OVERLAY_VIEW_ID: &str = "usage-test-overlay";#[tokio::test]async fn usage_command_opens_menu_when_reset_is_available_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); chat.dispatch_command(SlashCommand::Usage); assert_chatwidget_snapshot!( "usage_command_menu", render_bottom_popup(&chat, /*width*/ 80) ); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));}#[tokio::test]async fn usage_command_can_recheck_reset_availability_after_cached_zero_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 0 }), )); chat.dispatch_command(SlashCommand::Usage); assert_chatwidget_snapshot!( "usage_command_menu_without_resets", render_bottom_popup(&chat, /*width*/ 80) ); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));}#[tokio::test]async fn usage_command_can_check_reset_availability_before_startup_refresh_finishes_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); chat.start_rate_limit_reset_startup_check(); chat.dispatch_command(SlashCommand::Usage); assert_chatwidget_snapshot!( "usage_command_menu_before_reset_refresh", render_bottom_popup(&chat, /*width*/ 80) ); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));}#[tokio::test]async fn usage_command_disables_reset_for_workspace_accounts() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); chat.plan_type = Some(PlanType::Business); chat.dispatch_command(SlashCommand::Usage); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));}#[tokio::test]async fn usage_menu_rate_limit_reset_entry_opens_reset_flow() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); chat.dispatch_command(SlashCommand::Usage); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));}#[tokio::test]async fn rate_limit_reset_popup_states_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let mut states = Vec::new(); let loading_request_id = chat.show_rate_limit_reset_loading_popup(); record_popup(&chat, &mut states); assert!(chat.finish_rate_limit_reset_credits_refresh( loading_request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); record_popup(&chat, &mut states); dismiss_popup(&mut chat); let empty_request_id = chat.show_rate_limit_reset_loading_popup(); assert!(chat.finish_rate_limit_reset_credits_refresh( empty_request_id, Ok(RateLimitResetCreditsSummary { available_count: 0 }), )); record_popup(&chat, &mut states); dismiss_popup(&mut chat); let load_error_request_id = chat.show_rate_limit_reset_loading_popup(); assert!(chat.finish_rate_limit_reset_credits_refresh( load_error_request_id, Err("backend unavailable".to_string()), )); record_popup(&chat, &mut states); dismiss_popup(&mut chat); let consuming_request_id = chat.show_rate_limit_reset_consuming_popup(); record_popup(&chat, &mut states); assert!(!chat.finish_rate_limit_reset_consume( consuming_request_id, "redeem-1".to_string(), Err("request timed out".to_string()), )); record_popup(&chat, &mut states); dismiss_popup(&mut chat); let nothing_request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(!finish_reset_consume_outcome( &mut chat, nothing_request_id, "redeem-2", ConsumeAccountRateLimitResetCreditOutcome::NothingToReset, )); record_popup(&chat, &mut states); dismiss_popup(&mut chat); let no_credit_request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(!finish_reset_consume_outcome( &mut chat, no_credit_request_id, "redeem-3", ConsumeAccountRateLimitResetCreditOutcome::NoCredit, )); record_popup(&chat, &mut states); dismiss_popup(&mut chat); let success_request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(finish_reset_consume_outcome( &mut chat, success_request_id, "redeem-4", ConsumeAccountRateLimitResetCreditOutcome::Reset, )); record_popup(&chat, &mut states); assert!(chat.finish_post_consume_reset_credits_refresh( success_request_id, Ok(RateLimitResetCreditsSummary { available_count: 1 }), )); record_popup(&chat, &mut states); assert_chatwidget_snapshot!("rate_limit_reset_popup_states", states.join("\n---\n"));}#[tokio::test]async fn rate_limit_reset_confirmation_selects_cancel_by_default() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let request_id = chat.show_rate_limit_reset_loading_popup(); assert!(chat.finish_rate_limit_reset_credits_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 1 }), )); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert!(chat.bottom_pane.no_modal_or_popup_active()); assert!(rx.try_recv().is_err());}#[tokio::test]async fn rate_limit_reset_confirmation_can_use_reset() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let request_id = chat.show_rate_limit_reset_loading_popup(); assert!(chat.finish_rate_limit_reset_credits_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 1 }), )); chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!( rx.try_recv(), Ok(AppEvent::ConsumeRateLimitResetCredit { idempotency_key }) if Uuid::parse_str(&idempotency_key).is_ok() );}#[tokio::test]async fn rate_limit_reset_retry_reuses_idempotency_key() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(!chat.finish_rate_limit_reset_consume( request_id, "stable-redeem-id".to_string(), Err("response lost".to_string()), )); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!( rx.try_recv(), Ok(AppEvent::ConsumeRateLimitResetCredit { idempotency_key }) if idempotency_key == "stable-redeem-id" );}#[tokio::test]async fn no_credit_outcome_allows_reset_availability_recheck() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let startup_request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( startup_request_id, Ok(RateLimitResetCreditsSummary { available_count: 1 }), )); let consume_request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(!finish_reset_consume_outcome( &mut chat, consume_request_id, "redeem-1", ConsumeAccountRateLimitResetCreditOutcome::NoCredit, )); dismiss_popup(&mut chat); chat.dispatch_command(SlashCommand::Usage); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));}#[tokio::test]async fn rate_limit_reset_redemption_cannot_be_dismissed_while_in_flight() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let request_id = chat.show_rate_limit_reset_consuming_popup(); dismiss_popup(&mut chat); assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Using a reset...")); assert!(finish_reset_consume_outcome( &mut chat, request_id, "redeem-123", ConsumeAccountRateLimitResetCreditOutcome::Reset, )); dismiss_popup(&mut chat); assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Refreshing...")); assert!(chat.finish_post_consume_reset_credits_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 1 }), )); dismiss_popup(&mut chat); assert!(chat.bottom_pane.no_modal_or_popup_active());}#[tokio::test]async fn rate_limit_reset_redemption_allows_ctrl_c_to_quit_while_in_flight() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.show_rate_limit_reset_consuming_popup(); chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst))); assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Using a reset..."));}#[tokio::test]async fn already_redeemed_is_an_idempotent_success() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(finish_reset_consume_outcome( &mut chat, request_id, "stable-redeem-id", ConsumeAccountRateLimitResetCreditOutcome::AlreadyRedeemed, )); assert!(chat.finish_post_consume_reset_credits_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 0 }), )); assert!( render_bottom_popup(&chat, /*width*/ 80) .contains("Usage reset. You have 0 rate-limit resets left.") );}#[tokio::test]async fn failed_post_consume_refresh_does_not_keep_stale_reset_count() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let startup_request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( startup_request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); let consume_request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(finish_reset_consume_outcome( &mut chat, consume_request_id, "redeem-with-refresh-error", ConsumeAccountRateLimitResetCreditOutcome::Reset, )); assert!(chat.finish_post_consume_reset_credits_refresh( consume_request_id, Err("backend unavailable".to_string()), )); dismiss_popup(&mut chat); chat.dispatch_command(SlashCommand::Usage); let rendered = render_bottom_popup(&chat, /*width*/ 80); assert!(rendered.contains("Check reset availability.")); assert!(!rendered.contains("You have 2 rate-limit resets available."));}#[tokio::test]async fn account_change_invalidates_pending_reset_requests() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let request_id = chat.show_rate_limit_reset_loading_popup(); chat.update_account_state( /*status_account_display*/ None, /*plan_type*/ None, /*has_chatgpt_account*/ false, /*has_codex_backend_auth*/ false, ); assert!(!chat.finish_rate_limit_reset_credits_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); assert!(chat.bottom_pane.no_modal_or_popup_active());}#[tokio::test]async fn clearing_pending_reset_hint_preserves_in_flight_redemption() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let consume_request_id = chat.show_rate_limit_reset_consuming_popup(); let hint_request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); chat.clear_pending_rate_limit_reset_hint(); assert!(chat.pending_rate_limit_reset_hint().is_none()); assert!(finish_reset_consume_outcome( &mut chat, consume_request_id, "redeem-after-rollback", ConsumeAccountRateLimitResetCreditOutcome::Reset, ));}#[tokio::test]async fn rate_limit_reset_load_result_updates_popup_beneath_overlay() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let request_id = chat.show_rate_limit_reset_loading_popup(); show_usage_test_overlay(&mut chat); assert!(chat.finish_rate_limit_reset_credits_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); assert_eq!( chat.bottom_pane.active_view_id(), Some(TEST_OVERLAY_VIEW_ID) ); chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert!( render_bottom_popup(&chat, /*width*/ 80) .contains("You have 2 rate-limit resets available.") );}#[tokio::test]async fn rate_limit_reset_success_updates_popup_beneath_overlay() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let request_id = chat.show_rate_limit_reset_consuming_popup(); show_usage_test_overlay(&mut chat); assert!(finish_reset_consume_outcome( &mut chat, request_id, "redeem-covered", ConsumeAccountRateLimitResetCreditOutcome::Reset, )); assert!(chat.finish_post_consume_reset_credits_refresh( request_id, Ok(RateLimitResetCreditsSummary { available_count: 1 }), )); assert_eq!( chat.bottom_pane.active_view_id(), Some(TEST_OVERLAY_VIEW_ID) ); chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert!( render_bottom_popup(&chat, /*width*/ 80) .contains("Usage reset. You have 1 rate-limit reset left.") );}#[tokio::test]async fn account_change_dismisses_reset_popup_beneath_overlay() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); chat.show_rate_limit_reset_loading_popup(); show_usage_test_overlay(&mut chat); chat.update_account_state( /*status_account_display*/ None, /*plan_type*/ None, /*has_chatgpt_account*/ false, /*has_codex_backend_auth*/ false, ); assert_eq!( chat.bottom_pane.active_view_id(), Some(TEST_OVERLAY_VIEW_ID) ); chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert!(chat.bottom_pane.no_modal_or_popup_active());}#[tokio::test]async fn startup_check_shows_available_reset_hint_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let hint_request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); let rendered = lines_to_single_string( &chat .pending_rate_limit_reset_hint() .expect("pending reset hint") .display_lines(/*width*/ 80), ); assert_chatwidget_snapshot!("rate_limit_reset_available_hint", rendered);}#[tokio::test]async fn startup_reset_hint_waits_for_active_output_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let hint_request_id = chat.start_rate_limit_reset_startup_check(); chat.transcript.active_cell = Some(Box::new(PlainHistoryCell::new(vec![Line::from( "active tool", )]))); assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); assert!(chat.usage_history_insertion_blocked()); assert!(drain_insert_history(&mut rx).is_empty()); assert_chatwidget_snapshot!( "rate_limit_reset_hint_waits_for_active_output", lines_to_single_string( &chat .active_cell_transcript_lines(/*width*/ 80) .expect("active output with reset hint"), ) ); chat.flush_active_cell(); assert_matches!(rx.try_recv(), Ok(AppEvent::InsertHistoryCell(_))); assert_matches!(rx.try_recv(), Ok(AppEvent::CommitPendingUsageOutput));}#[tokio::test]async fn opening_rate_limit_reset_flow_invalidates_in_flight_startup_hint() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let hint_request_id = chat.start_rate_limit_reset_startup_check(); chat.show_rate_limit_reset_loading_popup(); assert!(!chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); assert!(chat.pending_rate_limit_reset_hint().is_none());}#[tokio::test]async fn starting_rate_limit_reset_redemption_clears_deferred_startup_hint() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let hint_request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); assert!(chat.pending_rate_limit_reset_hint().is_some()); chat.show_rate_limit_reset_consuming_popup(); assert!(chat.pending_rate_limit_reset_hint().is_none());}#[tokio::test]async fn startup_check_omits_reset_hint_when_none_are_available() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); let hint_request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Ok(RateLimitResetCreditsSummary { available_count: 0 }), )); assert!(chat.pending_rate_limit_reset_hint().is_none());}#[tokio::test]async fn startup_check_omits_reset_hint_for_workspace_accounts() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; set_chatgpt_auth(&mut chat); chat.plan_type = Some(PlanType::Business); let hint_request_id = chat.start_rate_limit_reset_startup_check(); assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Ok(RateLimitResetCreditsSummary { available_count: 2 }), )); assert!(chat.pending_rate_limit_reset_hint().is_none()); assert_eq!(chat.available_rate_limit_reset_credits, None);}fn consume_response( outcome: ConsumeAccountRateLimitResetCreditOutcome,) -> ConsumeAccountRateLimitResetCreditResponse { ConsumeAccountRateLimitResetCreditResponse { outcome }}fn finish_reset_consume_outcome( chat: &mut ChatWidget, request_id: u64, idempotency_key: &str, outcome: ConsumeAccountRateLimitResetCreditOutcome,) -> bool { chat.finish_rate_limit_reset_consume( request_id, idempotency_key.to_string(), Ok(consume_response(outcome)), )}fn record_popup(chat: &ChatWidget, states: &mut Vec<String>) { states.push(render_bottom_popup(chat, /*width*/ 80));}fn dismiss_popup(chat: &mut ChatWidget) { chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));}fn show_usage_test_overlay(chat: &mut ChatWidget) { chat.bottom_pane.show_selection_view(SelectionViewParams { view_id: Some(TEST_OVERLAY_VIEW_ID), title: Some("Covering overlay".to_string()), items: vec![SelectionItem { name: "Close".to_string(), dismiss_on_select: true, ..Default::default() }], ..Default::default() });}