|
1 | 1 | use super::*; |
2 | 2 | use pretty_assertions::assert_eq; |
3 | 3 |
|
| 4 | +#[derive(Debug, PartialEq, Eq)] |
| 5 | +enum PromptQueueTrace { |
| 6 | + ClientTurnStart(String), |
| 7 | + ServerTurnCompletedUsageLimit, |
| 8 | + UserQueue(String), |
| 9 | + ServerRateLimitRestored, |
| 10 | +} |
| 11 | + |
4 | 12 | fn thread_settings_for_test( |
5 | 13 | model: &str, |
6 | 14 | thread_id: ThreadId, |
@@ -60,6 +68,114 @@ fn configured_thread_session(thread_id: ThreadId) -> crate::session_state::Threa |
60 | 68 | } |
61 | 69 | } |
62 | 70 |
|
| 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 | + |
63 | 179 | #[tokio::test] |
64 | 180 | async fn invalid_url_elicitation_is_declined() { |
65 | 181 | let (mut chat, _app_event_tx, mut rx, _op_rx) = make_chatwidget_manual_with_sender().await; |
|
0 commit comments