Skip to content

Commit 89c2fe4

Browse files
committed
test(tui): cover usage-limit prompt queue recovery
1 parent cfbec3c commit 89c2fe4

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

codex-rs/tui/src/app/test_support.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ pub(super) async fn make_test_app() -> App {
5959
primary_session_configured: None,
6060
pending_primary_events: VecDeque::new(),
6161
pending_app_server_requests: PendingAppServerRequests::default(),
62+
_auth_watch: None,
63+
rate_limit_poll_task: None,
6264
pending_startup_thread_start: false,
6365
pending_plugin_enabled_writes: HashMap::new(),
6466
pending_hook_enabled_writes: HashMap::new(),

codex-rs/tui/src/app/tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3801,6 +3801,8 @@ async fn make_test_app() -> App {
38013801
primary_session_configured: None,
38023802
pending_primary_events: VecDeque::new(),
38033803
pending_app_server_requests: PendingAppServerRequests::default(),
3804+
_auth_watch: None,
3805+
rate_limit_poll_task: None,
38043806
pending_startup_thread_start: false,
38053807
pending_plugin_enabled_writes: HashMap::new(),
38063808
pending_hook_enabled_writes: HashMap::new(),
@@ -3864,6 +3866,8 @@ async fn make_test_app_with_channels() -> (
38643866
primary_session_configured: None,
38653867
pending_primary_events: VecDeque::new(),
38663868
pending_app_server_requests: PendingAppServerRequests::default(),
3869+
_auth_watch: None,
3870+
rate_limit_poll_task: None,
38673871
pending_startup_thread_start: false,
38683872
pending_plugin_enabled_writes: HashMap::new(),
38693873
pending_hook_enabled_writes: HashMap::new(),

codex-rs/tui/src/chatwidget/tests/app_server.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
use super::*;
22
use pretty_assertions::assert_eq;
33

4+
#[derive(Debug, PartialEq, Eq)]
5+
enum PromptQueueTrace {
6+
ClientTurnStart(String),
7+
ServerTurnCompletedUsageLimit,
8+
UserQueue(String),
9+
ServerRateLimitRestored,
10+
}
11+
412
fn thread_settings_for_test(
513
model: &str,
614
thread_id: ThreadId,
@@ -60,6 +68,114 @@ fn configured_thread_session(thread_id: ThreadId) -> crate::session_state::Threa
6068
}
6169
}
6270

71+
fn submit_composer_text(chat: &mut ChatWidget, text: &str) {
72+
chat.bottom_pane
73+
.set_composer_text(text.to_string(), Vec::new(), Vec::new());
74+
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
75+
}
76+
77+
fn queue_composer_text_with_tab(chat: &mut ChatWidget, text: &str) {
78+
chat.bottom_pane
79+
.set_composer_text(text.to_string(), Vec::new(), Vec::new());
80+
chat.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
81+
}
82+
83+
fn next_client_turn_start(
84+
op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>,
85+
) -> PromptQueueTrace {
86+
let Op::UserTurn { items, .. } = next_submit_op(op_rx) else {
87+
unreachable!("next_submit_op only returns user turns");
88+
};
89+
let [UserInput::Text { text, .. }] = items.as_slice() else {
90+
panic!("expected text-only turn/start input, got {items:?}");
91+
};
92+
PromptQueueTrace::ClientTurnStart(text.clone())
93+
}
94+
95+
fn usage_limit_completed_notification(chat: &ChatWidget, turn_id: &str) -> ServerNotification {
96+
ServerNotification::TurnCompleted(TurnCompletedNotification {
97+
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
98+
turn: app_server_turn(
99+
turn_id,
100+
AppServerTurnStatus::Failed,
101+
/*duration_ms*/ None,
102+
Some(AppServerTurnError {
103+
message: "Usage limit reached.".to_string(),
104+
codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded),
105+
additional_details: None,
106+
}),
107+
),
108+
})
109+
}
110+
111+
fn restored_rate_limit_snapshot() -> RateLimitSnapshot {
112+
RateLimitSnapshot {
113+
limit_id: Some("codex".to_string()),
114+
limit_name: Some("codex".to_string()),
115+
primary: Some(RateLimitWindow {
116+
used_percent: 20,
117+
window_duration_mins: Some(5 * 60),
118+
resets_at: None,
119+
}),
120+
secondary: None,
121+
credits: None,
122+
plan_type: None,
123+
rate_limit_reached_type: None,
124+
}
125+
}
126+
127+
#[tokio::test]
128+
async fn usage_limit_pauses_prompt_queue_until_rate_limit_recovers() {
129+
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
130+
chat.handle_thread_session(configured_thread_session(ThreadId::new()));
131+
let _ = drain_insert_history(&mut rx);
132+
133+
let mut trace = Vec::new();
134+
135+
submit_composer_text(&mut chat, "initial prompt");
136+
trace.push(next_client_turn_start(&mut op_rx));
137+
138+
handle_turn_started(&mut chat, "turn-initial");
139+
chat.handle_server_notification(
140+
usage_limit_completed_notification(&chat, "turn-initial"),
141+
/*replay_kind*/ None,
142+
);
143+
trace.push(PromptQueueTrace::ServerTurnCompletedUsageLimit);
144+
145+
queue_composer_text_with_tab(&mut chat, "follow up 1");
146+
trace.push(PromptQueueTrace::UserQueue("follow up 1".to_string()));
147+
queue_composer_text_with_tab(&mut chat, "follow up 2");
148+
trace.push(PromptQueueTrace::UserQueue("follow up 2".to_string()));
149+
150+
assert_no_submit_op(&mut op_rx);
151+
152+
chat.on_rate_limit_snapshot(Some(restored_rate_limit_snapshot()));
153+
trace.push(PromptQueueTrace::ServerRateLimitRestored);
154+
trace.push(next_client_turn_start(&mut op_rx));
155+
156+
assert_no_submit_op(&mut op_rx);
157+
158+
assert_eq!(
159+
trace,
160+
vec![
161+
PromptQueueTrace::ClientTurnStart("initial prompt".to_string()),
162+
PromptQueueTrace::ServerTurnCompletedUsageLimit,
163+
PromptQueueTrace::UserQueue("follow up 1".to_string()),
164+
PromptQueueTrace::UserQueue("follow up 2".to_string()),
165+
PromptQueueTrace::ServerRateLimitRestored,
166+
PromptQueueTrace::ClientTurnStart("follow up 1".to_string()),
167+
]
168+
);
169+
assert_eq!(chat.queued_user_message_texts(), vec!["follow up 2"]);
170+
171+
handle_turn_started(&mut chat, "turn-follow-up-1");
172+
handle_turn_completed(&mut chat, "turn-follow-up-1", /*duration_ms*/ None);
173+
assert_eq!(
174+
next_client_turn_start(&mut op_rx),
175+
PromptQueueTrace::ClientTurnStart("follow up 2".to_string())
176+
);
177+
}
178+
63179
#[tokio::test]
64180
async fn invalid_url_elicitation_is_declined() {
65181
let (mut chat, _app_event_tx, mut rx, _op_rx) = make_chatwidget_manual_with_sender().await;

0 commit comments

Comments
 (0)