From 8e2c1139068d803b44983b4e988ab153613017ac Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sun, 18 Jan 2026 22:23:22 -0800 Subject: [PATCH] tests --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 78 +++++++++++++ codex-rs/tui/src/bottom_pane/command_popup.rs | 36 ++++++ codex-rs/tui/src/bottom_pane/footer.rs | 30 ++--- ...__tests__footer_mode_shortcut_overlay.snap | 3 +- ...ests__footer_context_only_collab_hint.snap | 5 - ...shortcuts_collaboration_modes_enabled.snap | 11 ++ ...tests__footer_shortcuts_shift_and_esc.snap | 3 +- codex-rs/tui/src/chatwidget/tests.rs | 108 ++++++++++++++++++ 8 files changed, 252 insertions(+), 22 deletions(-) delete mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_collab_hint.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_collaboration_modes_enabled.snap diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index f9b836a464..3ab9071347 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2385,6 +2385,84 @@ mod tests { ); } + #[test] + fn footer_flash_overrides_footer_hint_override() { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_footer_hint_override(Some(vec![("K".to_string(), "label".to_string())])); + composer.show_footer_flash(Line::from("FLASH"), Duration::from_secs(10)); + + let area = Rect::new(0, 0, 60, 6); + let mut buf = Buffer::empty(area); + composer.render(area, &mut buf); + + let mut bottom_row = String::new(); + for x in 0..area.width { + bottom_row.push( + buf[(x, area.height - 1)] + .symbol() + .chars() + .next() + .unwrap_or(' '), + ); + } + assert!( + bottom_row.contains("FLASH"), + "expected flash content to render in footer row, saw: {bottom_row:?}", + ); + assert!( + !bottom_row.contains("K label"), + "expected flash to override hint override, saw: {bottom_row:?}", + ); + } + + #[test] + fn footer_flash_expires_and_falls_back_to_hint_override() { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_footer_hint_override(Some(vec![("K".to_string(), "label".to_string())])); + composer.show_footer_flash(Line::from("FLASH"), Duration::from_secs(10)); + composer.footer_flash.as_mut().unwrap().expires_at = + Instant::now() - Duration::from_secs(1); + + let area = Rect::new(0, 0, 60, 6); + let mut buf = Buffer::empty(area); + composer.render(area, &mut buf); + + let mut bottom_row = String::new(); + for x in 0..area.width { + bottom_row.push( + buf[(x, area.height - 1)] + .symbol() + .chars() + .next() + .unwrap_or(' '), + ); + } + assert!( + bottom_row.contains("K label"), + "expected hint override to render after flash expired, saw: {bottom_row:?}", + ); + assert!( + !bottom_row.contains("FLASH"), + "expected expired flash to be hidden, saw: {bottom_row:?}", + ); + } + fn snapshot_composer_state(name: &str, enhanced_keys_supported: bool, setup: F) where F: FnOnce(&mut ChatComposer), diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index bcf671eb53..83afdca30c 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -421,4 +421,40 @@ mod tests { "expected fuzzy search for '/ac' to include compact and feedback, got {cmds:?}" ); } + + #[test] + fn collab_command_hidden_when_collaboration_modes_disabled() { + let mut popup = CommandPopup::new(Vec::new(), CommandPopupFlags::default()); + popup.on_composer_text_change("/coll".to_string()); + + let cmds: Vec<&str> = popup + .filtered_items() + .into_iter() + .filter_map(|item| match item { + CommandItem::Builtin(cmd) => Some(cmd.command()), + CommandItem::UserPrompt(_) => None, + }) + .collect(); + assert!( + !cmds.contains(&"collab"), + "expected '/collab' to be hidden when collaboration modes are disabled, got {cmds:?}" + ); + } + + #[test] + fn collab_command_visible_when_collaboration_modes_enabled() { + let mut popup = CommandPopup::new( + Vec::new(), + CommandPopupFlags { + skills_enabled: false, + collaboration_modes_enabled: true, + }, + ); + popup.on_composer_text_change("/collab".to_string()); + + match popup.selected_item() { + Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), "collab"), + other => panic!("expected collab to be selected for exact match, got {other:?}"), + } + } } diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index da080e39a2..c2d12e8ff2 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -567,6 +567,21 @@ mod tests { }, ); + snapshot_footer( + "footer_shortcuts_collaboration_modes_enabled", + FooterProps { + mode: FooterMode::ShortcutOverlay, + esc_backtrack_hint: false, + use_shift_enter_hint: false, + is_task_running: false, + steer_enabled: false, + collaboration_modes_enabled: true, + quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), + context_window_percent: None, + context_window_used_tokens: None, + }, + ); + snapshot_footer( "footer_ctrl_c_quit_idle", FooterProps { @@ -686,20 +701,5 @@ mod tests { context_window_used_tokens: None, }, ); - - snapshot_footer( - "footer_context_only_collab_hint", - FooterProps { - mode: FooterMode::ContextOnly, - esc_backtrack_hint: false, - use_shift_enter_hint: false, - is_task_running: false, - steer_enabled: false, - collaboration_modes_enabled: true, - quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), - context_window_percent: None, - context_window_used_tokens: None, - }, - ); } } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap index 8486a9ec6f..462d348373 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 2492 expression: terminal.backend() --- " " @@ -15,4 +16,4 @@ expression: terminal.backend() " @ for file paths ctrl + v to paste images " " ctrl + g to edit in external editor esc again to edit previous message " " ctrl + c to exit " -" ctrl + t to view transcript " +" ctrl + t to view transcript " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_collab_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_collab_hint.snap deleted file mode 100644 index 95acd79602..0000000000 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_collab_hint.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: tui/src/bottom_pane/footer.rs -expression: terminal.backend() ---- -" 100% context left ยท shift + tab to change mode " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_collaboration_modes_enabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_collaboration_modes_enabled.snap new file mode 100644 index 0000000000..b6d87789ad --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_collaboration_modes_enabled.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/bottom_pane/footer.rs +assertion_line: 535 +expression: terminal.backend() +--- +" / for commands ! for shell commands " +" ctrl + j for newline tab to queue message " +" @ for file paths ctrl + v to paste images " +" ctrl + g to edit in external editor esc esc to edit previous message " +" ctrl + c to exit shift + tab to change mode " +" ctrl + t to view transcript " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap index c1f00d4437..07ea600aa0 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/footer.rs +assertion_line: 535 expression: terminal.backend() --- " / for commands ! for shell commands " @@ -7,4 +8,4 @@ expression: terminal.backend() " @ for file paths ctrl + v to paste images " " ctrl + g to edit in external editor esc again to edit previous message " " ctrl + c to exit " -" ctrl + t to view transcript " +" ctrl + t to view transcript " diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index a004340846..953741ca17 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1508,6 +1508,114 @@ async fn slash_init_skips_when_project_doc_exists() { ); } +#[test] +fn parse_collaboration_mode_selection_accepts_common_aliases() { + assert_eq!( + parse_collaboration_mode_selection("plan"), + Some(CollaborationModeSelection::Plan) + ); + assert_eq!( + parse_collaboration_mode_selection("PAIR"), + Some(CollaborationModeSelection::PairProgramming) + ); + assert_eq!( + parse_collaboration_mode_selection("pair_programming"), + Some(CollaborationModeSelection::PairProgramming) + ); + assert_eq!( + parse_collaboration_mode_selection("pp"), + Some(CollaborationModeSelection::PairProgramming) + ); + assert_eq!( + parse_collaboration_mode_selection(" exec "), + Some(CollaborationModeSelection::Execute) + ); + assert_eq!( + parse_collaboration_mode_selection("execute"), + Some(CollaborationModeSelection::Execute) + ); + assert_eq!(parse_collaboration_mode_selection("unknown"), None); +} + +#[tokio::test] +async fn collab_mode_shift_tab_cycles_only_when_enabled_and_idle() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; + chat.set_feature_enabled(Feature::CollaborationModes, false); + + let initial = chat.collaboration_mode; + chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); + assert_eq!(chat.collaboration_mode, initial); + assert_eq!(chat.pending_collaboration_mode, None); + + chat.set_feature_enabled(Feature::CollaborationModes, true); + + chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); + assert_eq!(chat.collaboration_mode, CollaborationModeSelection::Execute); + assert_eq!( + chat.pending_collaboration_mode, + Some(CollaborationModeSelection::Execute) + ); + + chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); + assert_eq!(chat.collaboration_mode, CollaborationModeSelection::Plan); + assert_eq!( + chat.pending_collaboration_mode, + Some(CollaborationModeSelection::Plan) + ); + + chat.on_task_started(); + chat.handle_key_event(KeyEvent::from(KeyCode::BackTab)); + assert_eq!(chat.collaboration_mode, CollaborationModeSelection::Plan); + assert_eq!( + chat.pending_collaboration_mode, + Some(CollaborationModeSelection::Plan) + ); +} + +#[tokio::test] +async fn collab_slash_command_sets_mode_and_next_submit_sends_user_turn() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.thread_id = Some(ThreadId::new()); + chat.set_feature_enabled(Feature::CollaborationModes, true); + + chat.dispatch_command_with_args(SlashCommand::Collab, "plan".to_string()); + assert_eq!(chat.collaboration_mode, CollaborationModeSelection::Plan); + assert_eq!( + chat.pending_collaboration_mode, + Some(CollaborationModeSelection::Plan) + ); + + fn next_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> Op { + loop { + match op_rx.try_recv() { + Ok(op @ Op::UserTurn { .. }) => return op, + Ok(op @ Op::UserInput { .. }) => return op, + Ok(_) => continue, + Err(TryRecvError::Empty) => panic!("expected a submit op but queue was empty"), + Err(TryRecvError::Disconnected) => panic!("expected submit op but channel closed"), + } + } + } + + chat.bottom_pane.set_composer_text("hello".to_string()); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + match next_submit_op(&mut op_rx) { + Op::UserTurn { + collaboration_mode: Some(CollaborationMode::Plan(_)), + .. + } => {} + other => panic!("expected Op::UserTurn with plan collab mode, got {other:?}"), + } + assert_eq!(chat.pending_collaboration_mode, None); + + chat.bottom_pane.set_composer_text("follow up".to_string()); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + match next_submit_op(&mut op_rx) { + Op::UserInput { .. } => {} + other => panic!("expected Op::UserInput after pending mode cleared, got {other:?}"), + } +} + #[tokio::test] async fn slash_quit_requests_exit() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;