mirror of
https://github.com/openai/codex.git
synced 2026-11-04 00:24:43 +00:00
## Why The reset flow introduced in #28154 still describes earned reset credits as "rate-limit resets" and uses generic reset-scope copy. It can also retain a stale available-credit count after redemption or an account change, leaving the reset action enabled after the last credit is used. This follow-up updates terminology only within that reset feature. Existing rate-limit wording elsewhere in the CLI and TUI is unchanged. ## What changed - Rename reset-specific `/usage` menu items, startup hints, and reset dialogs to "usage limit reset." - Describe monthly resets for Free, Go, and accounts that report a monthly usage window; otherwise describe the current 5-hour and weekly limits. - Recheck a cached zero balance when `/usage` is reopened, and refresh the balance after redemption so the final reset immediately disables the action. - Correlate async refresh results before updating snapshots and clear account-derived reset state, warnings, prompts, and status surfaces when the account changes. ## Validation - `just test -p codex-tui chatwidget::tests::usage` — 29 passed. - `just test -p codex-tui chatwidget::tests::status_command_tests` — 7 passed. - Account-boundary prompt and plan-mode prompt regression tests passed. - `cargo insta pending-snapshots` from `codex-rs/tui` — no pending snapshots.\ <img width="814" height="318" alt="image" src="https://github.com/user-attachments/assets/2a460e96-458b-4805-8d9f-c759382d21a4" /> view for monthly <img width="905" height="243" alt="image" src="https://github.com/user-attachments/assets/179f88e3-08fb-4af5-8dc6-ce6a944ed681" />
200 lines
7.6 KiB
Rust
200 lines
7.6 KiB
Rust
use super::*;
|
|
use assert_matches::assert_matches;
|
|
use codex_utils_path_uri::PathUri;
|
|
|
|
#[tokio::test]
|
|
async fn status_command_renders_immediately_and_refreshes_rate_limits_for_chatgpt_auth() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
set_chatgpt_auth(&mut chat);
|
|
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
|
|
let rendered = match rx.try_recv() {
|
|
Ok(AppEvent::InsertHistoryCell(cell)) => {
|
|
lines_to_single_string(&cell.display_lines(/*width*/ 80))
|
|
}
|
|
other => panic!("expected status output before refresh request, got {other:?}"),
|
|
};
|
|
assert!(
|
|
!rendered.contains("refreshing limits"),
|
|
"expected /status to avoid transient refresh text in terminal history, got: {rendered}"
|
|
);
|
|
let request_id = match rx.try_recv() {
|
|
Ok(AppEvent::RefreshRateLimits {
|
|
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
|
}) => request_id,
|
|
other => panic!("expected rate-limit refresh request, got {other:?}"),
|
|
};
|
|
pretty_assertions::assert_eq!(request_id, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_command_refresh_updates_cached_limits_for_future_status_outputs() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
set_chatgpt_auth(&mut chat);
|
|
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
|
|
match rx.try_recv() {
|
|
Ok(AppEvent::InsertHistoryCell(_)) => {}
|
|
other => panic!("expected status output before refresh request, got {other:?}"),
|
|
}
|
|
let first_request_id = match rx.try_recv() {
|
|
Ok(AppEvent::RefreshRateLimits {
|
|
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
|
}) => request_id,
|
|
other => panic!("expected rate-limit refresh request, got {other:?}"),
|
|
};
|
|
|
|
chat.finish_status_rate_limit_refresh(first_request_id, vec![snapshot(/*percent*/ 92.0)]);
|
|
drain_insert_history(&mut rx);
|
|
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
let refreshed = match rx.try_recv() {
|
|
Ok(AppEvent::InsertHistoryCell(cell)) => {
|
|
lines_to_single_string(&cell.display_lines(/*width*/ 80))
|
|
}
|
|
other => panic!("expected refreshed status output, got {other:?}"),
|
|
};
|
|
assert!(
|
|
refreshed.contains("8% left"),
|
|
"expected a future /status output to use refreshed cached limits, got: {refreshed}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_command_renders_immediately_without_rate_limit_refresh() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
|
|
assert_matches!(rx.try_recv(), Ok(AppEvent::InsertHistoryCell(_)));
|
|
assert!(
|
|
!std::iter::from_fn(|| rx.try_recv().ok())
|
|
.any(|event| matches!(event, AppEvent::RefreshRateLimits { .. })),
|
|
"non-ChatGPT sessions should not request a rate-limit refresh for /status"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_command_uses_catalog_default_reasoning_when_config_empty() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
|
chat.config.model_reasoning_effort = None;
|
|
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
|
|
let rendered = match rx.try_recv() {
|
|
Ok(AppEvent::InsertHistoryCell(cell)) => {
|
|
lines_to_single_string(&cell.display_lines(/*width*/ 80))
|
|
}
|
|
other => panic!("expected status output, got {other:?}"),
|
|
};
|
|
assert!(
|
|
rendered.contains("gpt-5.4 (reasoning medium, summaries auto)"),
|
|
"expected /status to render the catalog default reasoning effort, got: {rendered}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_command_renders_native_and_foreign_instruction_sources() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let (foreign_source, foreign_display) = if cfg!(windows) {
|
|
(
|
|
PathUri::parse("file:///remote/AGENTS.md").expect("POSIX instruction source"),
|
|
"/remote/AGENTS.md",
|
|
)
|
|
} else {
|
|
(
|
|
PathUri::parse("file:///C:/remote/AGENTS.md").expect("Windows instruction source"),
|
|
r"C:\remote\AGENTS.md",
|
|
)
|
|
};
|
|
chat.instruction_source_paths = vec![
|
|
PathUri::from_abs_path(&chat.config.cwd.join("AGENTS.md")),
|
|
foreign_source,
|
|
];
|
|
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
|
|
let rendered = match rx.try_recv() {
|
|
Ok(AppEvent::InsertHistoryCell(cell)) => {
|
|
lines_to_single_string(&cell.display_lines(/*width*/ 80))
|
|
}
|
|
other => panic!("expected status output, got {other:?}"),
|
|
};
|
|
assert!(
|
|
rendered.contains(&format!("AGENTS.md, {foreign_display}")),
|
|
"expected /status to show native-relative and environment-native foreign paths, got: {rendered}"
|
|
);
|
|
assert!(
|
|
!rendered.contains("Agents.md <none>"),
|
|
"expected /status to avoid stale <none> when app-server provided instruction sources, got: {rendered}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn status_command_overlapping_refreshes_update_matching_cells_only() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
set_chatgpt_auth(&mut chat);
|
|
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
match rx.try_recv() {
|
|
Ok(AppEvent::InsertHistoryCell(_)) => {}
|
|
other => panic!("expected first status output, got {other:?}"),
|
|
}
|
|
let first_request_id = match rx.try_recv() {
|
|
Ok(AppEvent::RefreshRateLimits {
|
|
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
|
}) => request_id,
|
|
other => panic!("expected first refresh request, got {other:?}"),
|
|
};
|
|
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
let second_rendered = match rx.try_recv() {
|
|
Ok(AppEvent::InsertHistoryCell(cell)) => {
|
|
lines_to_single_string(&cell.display_lines(/*width*/ 80))
|
|
}
|
|
other => panic!("expected second status output, got {other:?}"),
|
|
};
|
|
let second_request_id = match rx.try_recv() {
|
|
Ok(AppEvent::RefreshRateLimits {
|
|
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
|
}) => request_id,
|
|
other => panic!("expected second refresh request, got {other:?}"),
|
|
};
|
|
|
|
assert_ne!(first_request_id, second_request_id);
|
|
assert!(
|
|
!second_rendered.contains("refreshing limits"),
|
|
"expected /status to avoid transient refresh text in terminal history, got: {second_rendered}"
|
|
);
|
|
|
|
chat.finish_status_rate_limit_refresh(first_request_id, Vec::new());
|
|
pretty_assertions::assert_eq!(chat.refreshing_status_outputs.len(), 1);
|
|
|
|
chat.finish_status_rate_limit_refresh(second_request_id, vec![snapshot(/*percent*/ 92.0)]);
|
|
assert!(chat.refreshing_status_outputs.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn account_update_rejects_stale_status_rate_limit_snapshots() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
set_chatgpt_auth(&mut chat);
|
|
chat.dispatch_command(SlashCommand::Status);
|
|
assert_matches!(rx.try_recv(), Ok(AppEvent::InsertHistoryCell(_)));
|
|
let request_id = match rx.try_recv() {
|
|
Ok(AppEvent::RefreshRateLimits {
|
|
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
|
|
}) => request_id,
|
|
other => panic!("expected status refresh request, got {other:?}"),
|
|
};
|
|
|
|
chat.update_account_state(
|
|
/*status_account_display*/ None, /*plan_type*/ None,
|
|
/*has_chatgpt_account*/ true, /*has_codex_backend_auth*/ true,
|
|
);
|
|
chat.finish_status_rate_limit_refresh(request_id, vec![snapshot(/*percent*/ 92.0)]);
|
|
|
|
assert!(chat.rate_limit_snapshots_by_limit_id.is_empty());
|
|
}
|