Revert "Option to Notify Workspace Owner When Usage Limit is Reached" (#17391)

Reverts openai/codex#16969

#sev3-2026-04-10-accountscheckversion-500s-for-openai-workspace-7300
This commit is contained in:
Shijie Rao
2026-04-10 16:33:13 -07:00
committed by GitHub
Unverified
parent a3be74143a
commit 930e5adb7e
82 changed files with 60 additions and 3233 deletions
@@ -1,6 +0,0 @@
---
source: tui/src/chatwidget/tests/app_server.rs
assertion_line: 608
expression: rendered
---
■ Your workspace is out of credits. Request more from your workspace owner? [y/N]
-1
View File
@@ -128,7 +128,6 @@ pub(super) use codex_protocol::parse_command::ParsedCommand;
pub(super) use codex_protocol::plan_tool::PlanItemArg;
pub(super) use codex_protocol::plan_tool::StepStatus;
pub(super) use codex_protocol::plan_tool::UpdatePlanArgs;
pub(super) use codex_protocol::protocol::AddCreditsNudgeEmailStatus;
pub(super) use codex_protocol::protocol::AgentMessageDeltaEvent;
pub(super) use codex_protocol::protocol::AgentMessageEvent;
pub(super) use codex_protocol::protocol::AgentReasoningDeltaEvent;
@@ -569,117 +569,6 @@ async fn live_app_server_server_overloaded_error_renders_warning() {
assert!(!chat.bottom_pane.is_task_running());
}
#[tokio::test]
async fn live_app_server_usage_limit_error_shows_notify_owner_hint() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.update_account_state(
/*status_account_display*/ None,
/*workspace_role*/ None,
Some(false),
Some(PlanType::SelfServeBusinessUsageBased),
/*has_chatgpt_account*/ true,
);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
limit_id: Some("codex".to_string()),
limit_name: Some("codex".to_string()),
primary: None,
secondary: None,
credits: Some(CreditsSnapshot {
has_credits: false,
unlimited: false,
balance: None,
}),
spend_control: None,
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
}));
chat.handle_server_notification(
ServerNotification::Error(ErrorNotification {
error: AppServerTurnError {
message: "The usage limit has been reached".to_string(),
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded.into()),
additional_details: None,
},
will_retry: false,
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
}),
/*replay_kind*/ None,
);
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Your workspace is out of credits."),
"expected usage-limit error, got {rendered:?}"
);
assert!(
rendered.contains("Request more from your workspace owner? [y/N]"),
"expected workspace-owner prompt, got {rendered:?}"
);
let popup = render_bottom_popup(&chat, /*width*/ 80);
assert!(
popup.contains("Request more credits from your workspace owner?"),
"expected workspace-owner confirmation popup, got {popup:?}"
);
assert_chatwidget_snapshot!(
"live_app_server_usage_limit_error_shows_notify_owner_hint",
rendered
);
}
#[tokio::test]
async fn live_app_server_usage_limit_error_shows_spend_cap_hint() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.update_account_state(
/*status_account_display*/ None,
/*workspace_role*/ None,
Some(false),
Some(PlanType::SelfServeBusinessUsageBased),
/*has_chatgpt_account*/ true,
);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
limit_id: Some("codex".to_string()),
limit_name: Some("codex".to_string()),
primary: None,
secondary: None,
credits: Some(CreditsSnapshot {
has_credits: true,
unlimited: false,
balance: None,
}),
spend_control: Some(codex_protocol::protocol::SpendControlSnapshot { reached: true }),
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
}));
chat.handle_server_notification(
ServerNotification::Error(ErrorNotification {
error: AppServerTurnError {
message: "The usage limit has been reached".to_string(),
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded.into()),
additional_details: None,
},
will_retry: false,
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
}),
/*replay_kind*/ None,
);
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Your workspace has reached its spend cap."),
"expected spend-cap error, got {rendered:?}"
);
assert!(
!rendered.contains("Request more from your workspace owner? [y/N]"),
"expected spend-cap guidance instead of workspace-owner prompt, got {rendered:?}"
);
}
#[tokio::test]
async fn live_app_server_invalid_thread_name_update_is_ignored() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -104,7 +104,6 @@ pub(super) fn snapshot(percent: f64) -> RateLimitSnapshot {
}),
secondary: None,
credits: None,
spend_control: None,
plan_type: None,
}
}
@@ -190,8 +189,6 @@ pub(super) async fn make_chatwidget_manual(
current_collaboration_mode,
active_collaboration_mask,
has_chatgpt_account: false,
workspace_role: None,
is_workspace_owner: None,
model_catalog,
session_telemetry,
session_header: SessionHeader::new(resolved_model.clone()),
@@ -202,8 +199,6 @@ pub(super) async fn make_chatwidget_manual(
refreshing_status_outputs: Vec::new(),
next_status_refresh_request_id: 0,
plan_type: None,
notify_workspace_owner_in_flight: false,
pending_workspace_owner_notification_prompt: false,
rate_limit_warnings: RateLimitWarningState::default(),
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
adaptive_chunking: crate::streaming::chunking::AdaptiveChunkingPolicy::default(),
@@ -1245,8 +1245,6 @@ async fn collaboration_modes_defaults_to_code_on_startup() {
feedback: codex_feedback::CodexFeedback::new(),
is_first_run: true,
status_account_display: None,
initial_workspace_role: None,
initial_is_workspace_owner: None,
initial_plan_type: None,
model: Some(resolved_model.clone()),
startup_tooltip_override: None,
@@ -71,8 +71,6 @@ async fn experimental_mode_plan_is_ignored_on_startup() {
feedback: codex_feedback::CodexFeedback::new(),
is_first_run: true,
status_account_display: None,
initial_workspace_role: None,
initial_is_workspace_owner: None,
initial_plan_type: None,
model: Some(resolved_model.clone()),
startup_tooltip_override: None,
@@ -130,8 +130,6 @@ async fn helpers_are_available_and_do_not_panic() {
feedback: codex_feedback::CodexFeedback::new(),
is_first_run: true,
status_account_display: None,
initial_workspace_role: None,
initial_is_workspace_owner: None,
initial_plan_type: None,
model: Some(resolved_model),
startup_tooltip_override: None,
@@ -236,7 +234,6 @@ async fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
unlimited: false,
balance: Some("17.5".to_string()),
}),
spend_control: None,
plan_type: None,
}));
let initial_balance = chat
@@ -256,7 +253,6 @@ async fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
}),
secondary: None,
credits: None,
spend_control: None,
plan_type: None,
}));
@@ -295,7 +291,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
resets_at: None,
}),
credits: None,
spend_control: None,
plan_type: Some(PlanType::Plus),
}));
assert_eq!(chat.plan_type, Some(PlanType::Plus));
@@ -314,7 +309,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
resets_at: Some(234),
}),
credits: None,
spend_control: None,
plan_type: Some(PlanType::Pro),
}));
assert_eq!(chat.plan_type, Some(PlanType::Pro));
@@ -333,7 +327,6 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
resets_at: Some(567),
}),
credits: None,
spend_control: None,
plan_type: None,
}));
assert_eq!(chat.plan_type, Some(PlanType::Pro));
@@ -357,7 +350,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
unlimited: false,
balance: Some("5.00".to_string()),
}),
spend_control: None,
plan_type: Some(PlanType::Pro),
}));
@@ -371,7 +363,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
}),
secondary: None,
credits: None,
spend_control: None,
plan_type: Some(PlanType::Pro),
}));
@@ -396,101 +387,6 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
assert!(other.credits.is_none());
}
#[tokio::test]
async fn core_usage_limit_error_shows_notify_owner_hint() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.update_account_state(
/*status_account_display*/ None,
/*workspace_role*/ None,
Some(false),
Some(PlanType::SelfServeBusinessUsageBased),
/*has_chatgpt_account*/ true,
);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
limit_id: Some("codex".to_string()),
limit_name: Some("codex".to_string()),
primary: None,
secondary: None,
credits: Some(CreditsSnapshot {
has_credits: false,
unlimited: false,
balance: None,
}),
spend_control: None,
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
}));
chat.handle_codex_event(Event {
id: "usage-limit".to_string(),
msg: EventMsg::Error(ErrorEvent {
message: "The usage limit has been reached".to_string(),
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
}),
});
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Your workspace is out of credits."),
"expected usage-limit error, got {rendered:?}"
);
assert!(
rendered.contains("Request more from your workspace owner? [y/N]"),
"expected workspace-owner prompt, got {rendered:?}"
);
let popup = render_bottom_popup(&chat, /*width*/ 80);
assert!(
popup.contains("Request more credits from your workspace owner?"),
"expected workspace-owner confirmation popup, got {popup:?}"
);
}
#[tokio::test]
async fn core_usage_limit_error_shows_spend_cap_hint_when_reached() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.update_account_state(
/*status_account_display*/ None,
/*workspace_role*/ None,
Some(false),
Some(PlanType::SelfServeBusinessUsageBased),
/*has_chatgpt_account*/ true,
);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
limit_id: Some("codex".to_string()),
limit_name: Some("codex".to_string()),
primary: None,
secondary: None,
credits: Some(CreditsSnapshot {
has_credits: true,
unlimited: false,
balance: None,
}),
spend_control: Some(codex_protocol::protocol::SpendControlSnapshot { reached: true }),
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
}));
chat.handle_codex_event(Event {
id: "usage-limit".to_string(),
msg: EventMsg::Error(ErrorEvent {
message: "The usage limit has been reached".to_string(),
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
}),
});
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Your workspace has reached its spend cap."),
"expected spend-cap hint, got {rendered:?}"
);
assert!(
!rendered.contains("Request more from your workspace owner? [y/N]"),
"expected spend-cap hint instead of workspace-owner prompt, got {rendered:?}"
);
}
#[tokio::test]
async fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() {
let (mut chat, _, _) = make_chatwidget_manual(Some(NUDGE_MODEL_SLUG)).await;
@@ -519,7 +415,6 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() {
}),
secondary: None,
credits: None,
spend_control: None,
plan_type: None,
}));
@@ -120,212 +120,3 @@ async fn status_command_overlapping_refreshes_update_matching_cells_only() {
chat.finish_status_rate_limit_refresh(second_request_id);
assert!(chat.refreshing_status_outputs.is_empty());
}
#[tokio::test]
async fn usage_limit_error_requests_background_rate_limit_refresh() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
chat.update_account_state(
/*status_account_display*/ None,
/*workspace_role*/ None,
Some(false),
Some(PlanType::SelfServeBusinessUsageBased),
/*has_chatgpt_account*/ true,
);
chat.handle_codex_event(Event {
id: "usage-limit".to_string(),
msg: EventMsg::Error(ErrorEvent {
message: "The usage limit has been reached".to_string(),
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
}),
});
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
assert!(
events
.iter()
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
"expected usage-limit errors to trigger a background rate-limit refresh; events: {events:?}"
);
}
#[tokio::test]
async fn usage_limit_error_opens_workspace_owner_prompt_after_rate_limits_refresh() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
chat.update_account_state(
/*status_account_display*/ None,
/*workspace_role*/ None,
Some(false),
Some(PlanType::SelfServeBusinessUsageBased),
/*has_chatgpt_account*/ true,
);
chat.handle_codex_event(Event {
id: "usage-limit".to_string(),
msg: EventMsg::Error(ErrorEvent {
message: "The usage limit has been reached".to_string(),
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
}),
});
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
assert!(
events
.iter()
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
"expected usage-limit errors to request a refresh when rate limits are missing; events: {events:?}"
);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
limit_id: Some("codex".to_string()),
limit_name: Some("codex".to_string()),
primary: None,
secondary: None,
credits: Some(CreditsSnapshot {
has_credits: false,
unlimited: false,
balance: None,
}),
spend_control: None,
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
}));
let popup = render_bottom_popup(&chat, /*width*/ 80);
assert!(
popup.contains("Request more credits from your workspace owner?"),
"expected workspace-owner prompt after refresh, got: {popup}"
);
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::NotifyWorkspaceOwner));
}
#[tokio::test]
async fn usage_limit_error_opens_workspace_owner_prompt_after_async_workspace_role() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
chat.update_account_state(
/*status_account_display*/ None,
/*workspace_role*/ None,
/*is_workspace_owner*/ None,
Some(PlanType::SelfServeBusinessUsageBased),
/*has_chatgpt_account*/ true,
);
chat.handle_codex_event(Event {
id: "usage-limit".to_string(),
msg: EventMsg::Error(ErrorEvent {
message: "The usage limit has been reached".to_string(),
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
}),
});
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
assert!(
events
.iter()
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
"expected usage-limit errors to request a refresh when rate limits are missing; events: {events:?}"
);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
limit_id: Some("codex".to_string()),
limit_name: Some("codex".to_string()),
primary: None,
secondary: None,
credits: Some(CreditsSnapshot {
has_credits: false,
unlimited: false,
balance: None,
}),
spend_control: None,
plan_type: Some(PlanType::SelfServeBusinessUsageBased),
}));
let popup = render_bottom_popup(&chat, /*width*/ 80);
assert!(
!popup.contains("Request more credits from your workspace owner?"),
"expected no prompt before the async workspace role update, got: {popup}"
);
chat.update_account_state(
/*status_account_display*/ None,
Some(AppServerWorkspaceRole::StandardUser),
/*is_workspace_owner*/ None,
Some(PlanType::SelfServeBusinessUsageBased),
/*has_chatgpt_account*/ true,
);
let popup = render_bottom_popup(&chat, /*width*/ 80);
assert!(
popup.contains("Request more credits from your workspace owner?"),
"expected workspace-owner prompt after async workspace role, got: {popup}"
);
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::NotifyWorkspaceOwner));
}
#[tokio::test]
async fn notify_workspace_owner_success_adds_confirmation_message() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.start_notify_workspace_owner();
chat.finish_notify_workspace_owner(Ok(AddCreditsNudgeEmailStatus::Sent));
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1, "expected one confirmation message");
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Workspace owner notified."),
"expected success message, got {rendered:?}"
);
assert!(
!chat.notify_workspace_owner_in_flight,
"notify-owner state should clear after success"
);
}
#[tokio::test]
async fn notify_workspace_owner_cooldown_adds_info_message() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.start_notify_workspace_owner();
chat.finish_notify_workspace_owner(Ok(AddCreditsNudgeEmailStatus::CooldownActive));
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1, "expected one cooldown message");
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Workspace owner was already notified recently."),
"expected cooldown message, got {rendered:?}"
);
assert!(
!chat.notify_workspace_owner_in_flight,
"notify-owner state should clear after cooldown"
);
}
#[tokio::test]
async fn notify_workspace_owner_error_adds_retry_message() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.start_notify_workspace_owner();
chat.finish_notify_workspace_owner(Err("backend failed".to_string()));
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1, "expected one error message");
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Could not notify your workspace owner. Please try again."),
"expected retry message, got {rendered:?}"
);
assert!(
!chat.notify_workspace_owner_in_flight,
"notify-owner state should clear after errors"
);
}