Codex Handbook
tui/src/chatwidget/tests/review_mode.rs 1477 lines
use super::*;use pretty_assertions::assert_eq;use std::collections::VecDeque;#[tokio::test]async fn interrupted_turn_restores_queued_messages_with_images_and_elements() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let first_placeholder = "[Image #1]";    let first_text = format!("{first_placeholder} first");    let first_elements = vec![TextElement::new(        (0..first_placeholder.len()).into(),        Some(first_placeholder.to_string()),    )];    let first_images = [PathBuf::from("/tmp/first.png")];    let second_placeholder = "[Image #1]";    let second_text = format!("{second_placeholder} second");    let second_elements = vec![TextElement::new(        (0..second_placeholder.len()).into(),        Some(second_placeholder.to_string()),    )];    let second_images = [PathBuf::from("/tmp/second.png")];    let existing_placeholder = "[Image #1]";    let existing_text = format!("{existing_placeholder} existing");    let existing_elements = vec![TextElement::new(        (0..existing_placeholder.len()).into(),        Some(existing_placeholder.to_string()),    )];    let existing_images = vec![PathBuf::from("/tmp/existing.png")];    chat.input_queue.queued_user_messages.push_back(        UserMessage {            text: first_text,            local_images: vec![LocalImageAttachment {                placeholder: first_placeholder.to_string(),                path: first_images[0].clone(),            }],            remote_image_urls: Vec::new(),            text_elements: first_elements,            mention_bindings: Vec::new(),        }        .into(),    );    chat.input_queue.queued_user_messages.push_back(        UserMessage {            text: second_text,            local_images: vec![LocalImageAttachment {                placeholder: second_placeholder.to_string(),                path: second_images[0].clone(),            }],            remote_image_urls: Vec::new(),            text_elements: second_elements,            mention_bindings: Vec::new(),        }        .into(),    );    chat.refresh_pending_input_preview();    chat.bottom_pane        .set_composer_text(existing_text, existing_elements, existing_images.clone());    // When interrupted, queued messages are merged into the composer; image placeholders    // must be renumbered to match the combined local image list.    handle_turn_interrupted(&mut chat, "turn-1");    let first = "[Image #1] first".to_string();    let second = "[Image #2] second".to_string();    let third = "[Image #3] existing".to_string();    let expected_text = format!("{first}\n{second}\n{third}");    assert_eq!(chat.bottom_pane.composer_text(), expected_text);    let first_start = 0;    let second_start = first.len() + 1;    let third_start = second_start + second.len() + 1;    let expected_elements = vec![        TextElement::new(            (first_start..first_start + "[Image #1]".len()).into(),            Some("[Image #1]".to_string()),        ),        TextElement::new(            (second_start..second_start + "[Image #2]".len()).into(),            Some("[Image #2]".to_string()),        ),        TextElement::new(            (third_start..third_start + "[Image #3]".len()).into(),            Some("[Image #3]".to_string()),        ),    ];    assert_eq!(chat.bottom_pane.composer_text_elements(), expected_elements);    assert_eq!(        chat.bottom_pane.composer_local_image_paths(),        vec![            first_images[0].clone(),            second_images[0].clone(),            existing_images[0].clone(),        ]    );}/// Entering review mode uses the hint provided by the review request.#[tokio::test]async fn entered_review_mode_uses_request_hint() {    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;    handle_entered_review_mode(&mut chat, "feature branch");    let cells = drain_insert_history(&mut rx);    let banner = lines_to_single_string(cells.last().expect("review banner"));    assert_eq!(banner, ">> Code review started: feature branch <<\n");    assert!(chat.review.is_review_mode);}/// Entering review mode renders the current changes banner when requested.#[tokio::test]async fn entered_review_mode_defaults_to_current_changes_banner() {    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;    handle_entered_review_mode(&mut chat, "current changes");    let cells = drain_insert_history(&mut rx);    let banner = lines_to_single_string(cells.last().expect("review banner"));    assert_eq!(banner, ">> Code review started: current changes <<\n");    assert!(chat.review.is_review_mode);}#[tokio::test]async fn live_review_prompt_item_is_not_rendered() {    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;    handle_entered_review_mode(&mut chat, "changes against 'main'");    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1);    assert!(lines_to_single_string(&cells[0]).contains("Code review started"));    complete_user_message(        &mut chat,        "review-prompt",        "Review the code changes against the base branch 'main'.",    );    assert!(drain_insert_history(&mut rx).is_empty());}#[tokio::test]async fn live_app_server_review_prompt_item_is_not_rendered() {    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;    let review_mode_item = AppServerThreadItem::EnteredReviewMode {        id: "review-start".to_string(),        review: "changes against 'main'".to_string(),    };    chat.handle_server_notification(        ServerNotification::ItemStarted(ItemStartedNotification {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            started_at_ms: 0,            item: review_mode_item.clone(),        }),        /*replay_kind*/ None,    );    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1);    assert!(lines_to_single_string(&cells[0]).contains("Code review started"));    chat.handle_server_notification(        ServerNotification::ItemCompleted(ItemCompletedNotification {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            completed_at_ms: 0,            item: review_mode_item,        }),        /*replay_kind*/ None,    );    assert!(drain_insert_history(&mut rx).is_empty());    chat.handle_server_notification(        ServerNotification::ItemCompleted(ItemCompletedNotification {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            completed_at_ms: 0,            item: AppServerThreadItem::UserMessage {                id: "review-prompt".to_string(),                client_id: None,                content: vec![AppServerUserInput::Text {                    text: "Review the code changes against the base branch 'main'.".to_string(),                    text_elements: Vec::new(),                }],            },        }),        /*replay_kind*/ None,    );    assert!(drain_insert_history(&mut rx).is_empty());}#[tokio::test]async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    handle_turn_started(&mut chat, "turn-1");    handle_entered_review_mode(&mut chat, "feature branch");    let _ = drain_insert_history(&mut rx);    chat.input_queue        .queued_user_messages        .push_back(UserMessage::from("queued later").into());    chat.submit_user_message(UserMessage::from("review follow-up one"));    chat.submit_user_message(UserMessage::from("review follow-up two"));    assert_eq!(chat.input_queue.pending_steers.len(), 2);    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "review follow-up one".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected running-turn steer submit, got {other:?}"),    }    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "review follow-up two".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected second running-turn steer submit, got {other:?}"),    }    handle_error(        &mut chat,        "cannot steer a review turn",        Some(CodexErrorInfo::ActiveTurnNotSteerable {            turn_kind: NonSteerableTurnKind::Review,        }),    );    handle_error(        &mut chat,        "cannot steer a review turn",        Some(CodexErrorInfo::ActiveTurnNotSteerable {            turn_kind: NonSteerableTurnKind::Review,        }),    );    assert!(chat.input_queue.pending_steers.is_empty());    assert_eq!(        chat.queued_user_message_texts(),        vec![            "review follow-up one",            "review follow-up two",            "queued later"        ]    );    assert!(drain_insert_history(&mut rx).is_empty());    handle_exited_review_mode(&mut chat);    handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "review follow-up one\nreview follow-up two".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected merged rejected-steer follow-up submit, got {other:?}"),    }    handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "queued later".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected queued draft submit after rejected steers, got {other:?}"),    }}#[tokio::test]async fn esc_with_review_queued_steers_shows_warning_and_does_not_interrupt() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    handle_turn_started(&mut chat, "turn-1");    handle_entered_review_mode(&mut chat, "feature branch");    let _ = drain_insert_history(&mut rx);    chat.input_queue        .pending_steers        .push_back(pending_steer("review follow-up"));    chat.refresh_pending_input_preview();    chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    assert!(!chat.input_queue.submit_pending_steers_after_interrupt);    assert_eq!(chat.input_queue.pending_steers.len(), 1);    assert_no_submit_op(&mut op_rx);    let cells = drain_insert_history(&mut rx);    let last = lines_to_single_string(cells.last().expect("review warning"));    assert_chatwidget_snapshot!("review_submission_warning_snapshot", last);}#[tokio::test]async fn live_agent_message_renders_during_review_mode() {    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;    handle_entered_review_mode(&mut chat, "current changes");    let _ = drain_insert_history(&mut rx);    complete_assistant_message(        &mut chat,        "review-message",        "Review progress update",        /*phase*/ None,    );    let inserted = drain_insert_history(&mut rx);    assert_eq!(inserted.len(), 1);    assert!(lines_to_single_string(&inserted[0]).contains("Review progress update"));}/// Exiting review restores the pre-review context window indicator.#[tokio::test]async fn review_restores_context_window_indicator() {    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;    let context_window = 13_000;    let pre_review_tokens = 12_700; // ~30% remaining after subtracting baseline.    let review_tokens = 12_030; // ~97% remaining after subtracting baseline.    handle_token_count(        &mut chat,        Some(make_token_info(pre_review_tokens, context_window)),    );    assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));    handle_entered_review_mode(&mut chat, "feature branch");    handle_token_count(        &mut chat,        Some(make_token_info(review_tokens, context_window)),    );    assert_eq!(chat.bottom_pane.context_window_percent(), Some(97));    handle_exited_review_mode(&mut chat);    let _ = drain_insert_history(&mut rx);    assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));    assert!(!chat.review.is_review_mode);}#[tokio::test]async fn restore_thread_input_state_restores_pending_steers_without_downgrading_them() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let mut pending_steers = VecDeque::new();    pending_steers.push_back(UserMessage::from("pending steer"));    let expected_compare_key = PendingSteerCompareKey {        message: "hidden IDE context\npending steer".to_string(),        image_count: 0,    };    let mut pending_steer_compare_keys = VecDeque::new();    pending_steer_compare_keys.push_back(expected_compare_key.clone());    let mut rejected_steers_queue = VecDeque::new();    rejected_steers_queue.push_back(UserMessage::from("already rejected"));    let mut queued_user_messages = VecDeque::new();    queued_user_messages.push_back(UserMessage::from("queued draft").into());    chat.restore_thread_input_state(Some(ThreadInputState {        composer: None,        pending_steers,        pending_steer_history_records: VecDeque::new(),        pending_steer_compare_keys,        rejected_steers_queue,        rejected_steer_history_records: VecDeque::new(),        queued_user_messages,        queued_user_message_history_records: VecDeque::new(),        user_turn_pending_start: false,        current_collaboration_mode: chat.current_collaboration_mode.clone(),        active_collaboration_mask: chat.active_collaboration_mask.clone(),        task_running: false,        agent_turn_running: false,    }));    assert_eq!(        chat.queued_user_message_texts(),        vec!["already rejected", "queued draft"]    );    assert_eq!(chat.input_queue.pending_steers.len(), 1);    assert_eq!(        chat.input_queue            .pending_steers            .front()            .unwrap()            .user_message            .text,        "pending steer"    );    assert_eq!(        chat.input_queue.pending_steers.front().unwrap().compare_key,        expected_compare_key    );}#[tokio::test]async fn steer_enter_queues_while_plan_stream_is_active() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);    let plan_mask = collaboration_modes::mask_for_kind(chat.model_catalog.as_ref(), ModeKind::Plan)        .expect("expected plan collaboration mask");    chat.set_collaboration_mask(plan_mask);    chat.on_task_started();    chat.on_plan_delta("- Step 1".to_string());    let _ = drain_insert_history(&mut rx);    chat.bottom_pane        .set_composer_text("queued submission".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan);    assert_eq!(chat.input_queue.queued_user_messages.len(), 1);    assert_eq!(        chat.input_queue.queued_user_messages.front().unwrap().text,        "queued submission"    );    assert!(chat.input_queue.pending_steers.is_empty());    assert_no_submit_op(&mut op_rx);    assert!(drain_insert_history(&mut rx).is_empty());}#[tokio::test]async fn steer_enter_uses_pending_steers_while_turn_is_running_without_streaming() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    chat.bottom_pane        .set_composer_text("queued while running".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert!(chat.input_queue.queued_user_messages.is_empty());    assert_eq!(chat.input_queue.pending_steers.len(), 1);    assert_eq!(        chat.input_queue            .pending_steers            .front()            .unwrap()            .user_message            .text,        "queued while running"    );    match next_submit_op(&mut op_rx) {        Op::UserTurn { .. } => {}        other => panic!("expected Op::UserTurn, got {other:?}"),    }    assert!(drain_insert_history(&mut rx).is_empty());    complete_user_message(&mut chat, "user-1", "queued while running");    assert!(chat.input_queue.pending_steers.is_empty());    let inserted = drain_insert_history(&mut rx);    assert_eq!(inserted.len(), 1);    assert!(lines_to_single_string(&inserted[0]).contains("queued while running"));}#[tokio::test]async fn steer_enter_uses_pending_steers_while_final_answer_stream_is_active() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    // Keep the assistant stream open (no commit tick/finalize) to model the repro window:    // user presses Enter while the final answer is still streaming.    chat.on_agent_message_delta("Final answer line\n".to_string());    chat.bottom_pane.set_composer_text(        "queued while streaming".to_string(),        Vec::new(),        Vec::new(),    );    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert!(chat.input_queue.queued_user_messages.is_empty());    assert_eq!(chat.input_queue.pending_steers.len(), 1);    assert_eq!(        chat.input_queue            .pending_steers            .front()            .unwrap()            .user_message            .text,        "queued while streaming"    );    match next_submit_op(&mut op_rx) {        Op::UserTurn { .. } => {}        other => panic!("expected Op::UserTurn, got {other:?}"),    }    assert!(drain_insert_history(&mut rx).is_empty());    complete_user_message(&mut chat, "user-1", "queued while streaming");    assert!(chat.input_queue.pending_steers.is_empty());    let inserted = drain_insert_history(&mut rx);    assert_eq!(inserted.len(), 1);    assert!(lines_to_single_string(&inserted[0]).contains("queued while streaming"));}#[tokio::test]async fn failed_pending_steer_submit_does_not_add_pending_preview() {    let (mut chat, mut rx, op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    drop(op_rx);    chat.bottom_pane.set_composer_text(        "queued while streaming".to_string(),        Vec::new(),        Vec::new(),    );    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert!(chat.input_queue.pending_steers.is_empty());    assert!(chat.input_queue.queued_user_messages.is_empty());    assert!(drain_insert_history(&mut rx).is_empty());}#[tokio::test]async fn item_completed_only_pops_front_pending_steer() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.input_queue        .pending_steers        .push_back(pending_steer("first"));    chat.input_queue        .pending_steers        .push_back(pending_steer("second"));    chat.refresh_pending_input_preview();    complete_user_message(&mut chat, "user-other", "other");    assert_eq!(chat.input_queue.pending_steers.len(), 2);    assert_eq!(        chat.input_queue            .pending_steers            .front()            .unwrap()            .user_message            .text,        "first"    );    let inserted = drain_insert_history(&mut rx);    assert_eq!(inserted.len(), 1);    assert!(lines_to_single_string(&inserted[0]).contains("other"));    complete_user_message(&mut chat, "user-first", "first");    assert_eq!(chat.input_queue.pending_steers.len(), 1);    assert_eq!(        chat.input_queue            .pending_steers            .front()            .unwrap()            .user_message            .text,        "second"    );    let inserted = drain_insert_history(&mut rx);    assert_eq!(inserted.len(), 1);    assert!(lines_to_single_string(&inserted[0]).contains("first"));}#[tokio::test(flavor = "multi_thread")]async fn item_completed_pops_pending_steer_with_local_image_and_text_elements() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    let temp = tempdir().expect("tempdir");    let image_path = temp.path().join("pending-steer.png");    const TINY_PNG_BYTES: &[u8] = &[        137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6,        0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 156, 99, 96, 0, 2, 0, 0, 5, 0,        1, 122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,    ];    std::fs::write(&image_path, TINY_PNG_BYTES).expect("write image");    let text = "note".to_string();    let text_elements = vec![TextElement::new((0..4).into(), Some("note".to_string()))];    chat.submit_user_message(UserMessage {        text: text.clone(),        local_images: vec![LocalImageAttachment {            placeholder: "[Image #1]".to_string(),            path: image_path,        }],        remote_image_urls: Vec::new(),        text_elements,        mention_bindings: Vec::new(),    });    match next_submit_op(&mut op_rx) {        Op::UserTurn { .. } => {}        other => panic!("expected Op::UserTurn, got {other:?}"),    }    assert_eq!(chat.input_queue.pending_steers.len(), 1);    let pending = chat.input_queue.pending_steers.front().unwrap();    assert_eq!(pending.user_message.local_images.len(), 1);    assert_eq!(pending.user_message.text_elements.len(), 1);    assert_eq!(pending.compare_key.message, text);    assert_eq!(pending.compare_key.image_count, 1);    complete_user_message_for_inputs(        &mut chat,        "user-1",        vec![            UserInput::Image {                url: "data:image/png;base64,placeholder".to_string(),                detail: None,            },            UserInput::Text {                text,                text_elements: Vec::new(),            },        ],    );    assert!(chat.input_queue.pending_steers.is_empty());    let mut user_cell = None;    while let Ok(ev) = rx.try_recv() {        if let AppEvent::InsertHistoryCell(cell) = ev            && let Some(cell) = cell.as_any().downcast_ref::<UserHistoryCell>()        {            user_cell = Some((                cell.message.clone(),                cell.text_elements.clone(),                cell.local_image_paths.clone(),                cell.remote_image_urls.clone(),            ));            break;        }    }    let (stored_message, stored_elements, stored_images, stored_remote_image_urls) =        user_cell.expect("expected pending steer user history cell");    assert_eq!(stored_message, "note");    assert_eq!(        stored_elements,        vec![TextElement::new((0..4).into(), Some("note".to_string()))]    );    assert_eq!(stored_images.len(), 1);    assert!(stored_images[0].ends_with("pending-steer.png"));    assert!(stored_remote_image_urls.is_empty());}#[tokio::test]async fn steer_enter_during_final_stream_preserves_follow_up_prompts_in_order() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    // Simulate "dead mode" repro timing by keeping a final-answer stream active while the    // user submits multiple follow-up prompts.    chat.on_agent_message_delta("Final answer line\n".to_string());    chat.bottom_pane        .set_composer_text("first follow-up".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    chat.bottom_pane        .set_composer_text("second follow-up".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert!(chat.input_queue.queued_user_messages.is_empty());    assert_eq!(chat.input_queue.pending_steers.len(), 2);    assert_eq!(        chat.input_queue            .pending_steers            .front()            .unwrap()            .user_message            .text,        "first follow-up"    );    assert_eq!(        chat.input_queue            .pending_steers            .back()            .unwrap()            .user_message            .text,        "second follow-up"    );    let first_items = match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => items,        other => panic!("expected Op::UserTurn, got {other:?}"),    };    assert_eq!(        first_items,        vec![UserInput::Text {            text: "first follow-up".to_string(),            text_elements: Vec::new(),        }]    );    let second_items = match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => items,        other => panic!("expected Op::UserTurn, got {other:?}"),    };    assert_eq!(        second_items,        vec![UserInput::Text {            text: "second follow-up".to_string(),            text_elements: Vec::new(),        }]    );    assert!(drain_insert_history(&mut rx).is_empty());    complete_user_message(&mut chat, "user-1", "first follow-up");    assert_eq!(chat.input_queue.pending_steers.len(), 1);    assert_eq!(        chat.input_queue            .pending_steers            .front()            .unwrap()            .user_message            .text,        "second follow-up"    );    let first_insert = drain_insert_history(&mut rx);    assert_eq!(first_insert.len(), 1);    assert!(lines_to_single_string(&first_insert[0]).contains("first follow-up"));    complete_user_message(&mut chat, "user-2", "second follow-up");    assert!(chat.input_queue.pending_steers.is_empty());    let second_insert = drain_insert_history(&mut rx);    assert_eq!(second_insert.len(), 1);    assert!(lines_to_single_string(&second_insert[0]).contains("second follow-up"));}#[tokio::test]async fn manual_interrupt_restores_pending_steers_to_composer() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    chat.on_agent_message_delta(        "Final answer line"        .to_string(),    );    chat.bottom_pane.set_composer_text(        "queued while streaming".to_string(),        Vec::new(),        Vec::new(),    );    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert_eq!(chat.input_queue.pending_steers.len(), 1);    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "queued while streaming".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected Op::UserTurn, got {other:?}"),    }    assert!(drain_insert_history(&mut rx).is_empty());    chat.on_interrupted_turn(TurnAbortReason::Interrupted);    assert!(chat.input_queue.pending_steers.is_empty());    assert_eq!(chat.bottom_pane.composer_text(), "queued while streaming");    assert_no_submit_op(&mut op_rx);    let inserted = drain_insert_history(&mut rx);    assert!(        inserted            .iter()            .all(|cell| !lines_to_single_string(cell).contains("queued while streaming"))    );}#[tokio::test]async fn esc_interrupt_sends_all_pending_steers_immediately_and_keeps_existing_draft() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    chat.on_agent_message_delta("Final answer line\n".to_string());    chat.bottom_pane        .set_composer_text("first pending steer".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "first pending steer".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected Op::UserTurn, got {other:?}"),    }    chat.bottom_pane        .set_composer_text("second pending steer".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "second pending steer".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected Op::UserTurn, got {other:?}"),    }    chat.input_queue        .queued_user_messages        .push_back(UserMessage::from("queued draft".to_string()).into());    chat.refresh_pending_input_preview();    chat.bottom_pane        .set_composer_text("still editing".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    next_interrupt_op(&mut op_rx);    chat.on_interrupted_turn(TurnAbortReason::Interrupted);    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "first pending steer\nsecond pending steer".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected merged pending steers to submit, got {other:?}"),    }    assert!(chat.input_queue.pending_steers.is_empty());    assert_eq!(chat.bottom_pane.composer_text(), "still editing");    assert_eq!(chat.input_queue.queued_user_messages.len(), 1);    assert_eq!(        chat.input_queue.queued_user_messages.front().unwrap().text,        "queued draft"    );    let inserted = drain_insert_history(&mut rx);    assert!(        inserted            .iter()            .any(|cell| lines_to_single_string(cell).contains("first pending steer"))    );    assert!(        inserted            .iter()            .any(|cell| lines_to_single_string(cell).contains("second pending steer"))    );}#[tokio::test]async fn esc_with_pending_steers_overrides_agent_command_interrupt_behavior() {    let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    chat.bottom_pane        .set_composer_text("pending steer".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    match next_submit_op(&mut op_rx) {        Op::UserTurn { .. } => {}        other => panic!("expected Op::UserTurn, got {other:?}"),    }    chat.bottom_pane        .set_composer_text("/agent ".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    next_interrupt_op(&mut op_rx);    assert_eq!(chat.bottom_pane.composer_text(), "/agent ");}#[tokio::test]async fn manual_interrupt_restores_pending_steer_mention_bindings_to_composer() {    let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    chat.on_agent_message_delta("Final answer line\n".to_string());    let mention_bindings = vec![MentionBinding {        sigil: '$',        mention: "figma".to_string(),        path: "/tmp/skills/figma/SKILL.md".to_string(),    }];    chat.bottom_pane.set_composer_text_with_mention_bindings(        "please use $figma".to_string(),        vec![TextElement::new(            (11..17).into(),            Some("$figma".to_string()),        )],        Vec::new(),        mention_bindings.clone(),    );    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "please use $figma".to_string(),                text_elements: vec![                    TextElement::new((11..17).into(), Some("$figma".to_string())).into()                ],            }]        ),        other => panic!("expected Op::UserTurn, got {other:?}"),    }    chat.on_interrupted_turn(TurnAbortReason::Interrupted);    assert_eq!(chat.bottom_pane.composer_text(), "please use $figma");    assert_eq!(chat.bottom_pane.take_mention_bindings(), mention_bindings);    assert_no_submit_op(&mut op_rx);}#[tokio::test]async fn manual_interrupt_restores_pending_steers_before_queued_messages() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.on_task_started();    chat.on_agent_message_delta(        "Final answer line"        .to_string(),    );    chat.bottom_pane        .set_composer_text("pending steer".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    chat.input_queue        .queued_user_messages        .push_back(UserMessage::from("queued draft".to_string()).into());    chat.refresh_pending_input_preview();    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "pending steer".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected Op::UserTurn, got {other:?}"),    }    assert!(drain_insert_history(&mut rx).is_empty());    chat.on_interrupted_turn(TurnAbortReason::Interrupted);    assert!(chat.input_queue.pending_steers.is_empty());    assert!(chat.input_queue.queued_user_messages.is_empty());    assert_eq!(        chat.bottom_pane.composer_text(),        "pending steerqueued draft"    );    assert_no_submit_op(&mut op_rx);}#[tokio::test]async fn ctrl_c_shutdown_works_with_caps_lock() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.handle_key_event(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::CONTROL));    assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst)));}#[tokio::test]async fn ctrl_c_interrupts_without_arming_quit_when_double_press_disabled() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.bottom_pane.set_task_running(/*running*/ true);    chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));    next_interrupt_op(&mut op_rx);    assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));    assert!(!chat.bottom_pane.quit_shortcut_hint_visible());    chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));    next_interrupt_op(&mut op_rx);    assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));    assert!(!chat.bottom_pane.quit_shortcut_hint_visible());}#[tokio::test]async fn ctrl_c_cleared_prompt_is_recoverable_via_history() {    let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.bottom_pane.insert_str("draft message ");    chat.bottom_pane        .attach_image(PathBuf::from("/tmp/preview.png"));    let placeholder = "[Image #1]";    assert!(        chat.bottom_pane.composer_text().ends_with(placeholder),        "expected placeholder {placeholder:?} in composer text"    );    chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));    assert!(chat.bottom_pane.composer_text().is_empty());    assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));    assert!(!chat.bottom_pane.quit_shortcut_hint_visible());    chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));    let restored_text = chat.bottom_pane.composer_text();    assert!(        restored_text.ends_with(placeholder),        "expected placeholder {placeholder:?} after history recall"    );    assert!(restored_text.starts_with("draft message "));    assert!(!chat.bottom_pane.quit_shortcut_hint_visible());    let images = chat.bottom_pane.take_recent_submission_images();    assert_eq!(vec![PathBuf::from("/tmp/preview.png")], images);}/// Selecting the custom prompt option from the review popup sends/// OpenReviewCustomPrompt to the app event channel.#[tokio::test]async fn review_popup_custom_prompt_action_sends_event() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Open the preset selection popup    chat.open_review_popup();    // Move selection down to the fourth item: "Custom review instructions"    chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));    chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));    chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));    // Activate    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    // Drain events and ensure we saw the OpenReviewCustomPrompt request    let mut found = false;    while let Ok(ev) = rx.try_recv() {        if let AppEvent::OpenReviewCustomPrompt = ev {            found = true;            break;        }    }    assert!(found, "expected OpenReviewCustomPrompt event to be sent");}/// The commit picker shows only commit subjects (no timestamps).#[tokio::test]async fn review_commit_picker_shows_subjects_without_timestamps() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Open the Review presets parent popup.    chat.open_review_popup();    // Show commit picker with synthetic entries.    let entries = vec![        CommitLogEntry {            sha: "1111111deadbeef".to_string(),            timestamp: 0,            subject: "Add new feature X".to_string(),        },        CommitLogEntry {            sha: "2222222cafebabe".to_string(),            timestamp: 0,            subject: "Fix bug Y".to_string(),        },    ];    super::show_review_commit_picker_with_entries(&mut chat, entries);    // Render the bottom pane and inspect the lines for subjects and absence of time words.    let width = 72;    let height = chat.desired_height(width);    let area = ratatui::layout::Rect::new(0, 0, width, height);    let mut buf = ratatui::buffer::Buffer::empty(area);    chat.render(area, &mut buf);    let mut blob = String::new();    for y in 0..area.height {        for x in 0..area.width {            let s = buf[(x, y)].symbol();            if s.is_empty() {                blob.push(' ');            } else {                blob.push_str(s);            }        }        blob.push('\n');    }    assert!(        blob.contains("Add new feature X"),        "expected subject in output"    );    assert!(blob.contains("Fix bug Y"), "expected subject in output");    // Ensure no relative-time phrasing is present.    let lowered = blob.to_lowercase();    assert!(        !lowered.contains("ago")            && !lowered.contains(" second")            && !lowered.contains(" minute")            && !lowered.contains(" hour")            && !lowered.contains(" day"),        "expected no relative time in commit picker output: {blob:?}"    );}/// Submitting the custom prompt view sends Op::Review with the typed prompt/// and uses the same text for the user-facing hint.#[tokio::test]async fn custom_prompt_submit_sends_review_op() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_review_custom_prompt();    // Paste prompt text via ChatWidget handler, then submit    chat.handle_paste("  please audit dependencies  ".to_string());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    // Expect AppEvent::CodexOp(Op::Review { .. }) with trimmed prompt    let evt = rx.try_recv().expect("expected one app event");    match evt {        AppEvent::CodexOp(Op::Review { target }) => {            assert_eq!(                target,                ReviewTarget::Custom {                    instructions: "please audit dependencies".to_string(),                }            );        }        other => panic!("unexpected app event: {other:?}"),    }}/// Hitting Enter on an empty custom prompt view does not submit.#[tokio::test]async fn custom_prompt_enter_empty_does_not_send() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_review_custom_prompt();    // Enter without any text    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    // No AppEvent::CodexOp should be sent    assert!(rx.try_recv().is_err(), "no app event should be sent");}// Snapshot test: interrupting a running exec finalizes the active cell with a red ✗// marker (replacing the spinner) and flushes it into history.#[tokio::test]async fn interrupt_exec_marks_failed_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Begin a long-running command so we have an active exec cell with a spinner.    begin_exec(&mut chat, "call-int", "sleep 1");    // Simulate the task being aborted (as if ESC was pressed), which should    // cause the active exec cell to be finalized as failed and flushed.    handle_turn_interrupted(&mut chat, "turn-1");    let cells = drain_insert_history(&mut rx);    assert!(        !cells.is_empty(),        "expected finalized exec cell to be inserted into history"    );    // The first inserted cell should be the finalized exec; snapshot its text.    let exec_blob = lines_to_single_string(&cells[0]);    assert_chatwidget_snapshot!("interrupt_exec_marks_failed", exec_blob);}// Snapshot test: after an interrupted turn, a gentle error message is inserted// suggesting the user to tell the model what to do differently and to use /feedback.#[tokio::test]async fn interrupted_turn_error_message_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Simulate an in-progress task so the widget is in a running state.    handle_turn_started(&mut chat, "turn-1");    // Abort the turn (like pressing Esc) and drain inserted history.    handle_turn_interrupted(&mut chat, "turn-1");    let cells = drain_insert_history(&mut rx);    assert!(        !cells.is_empty(),        "expected error message to be inserted after interruption"    );    let last = lines_to_single_string(cells.last().unwrap());    assert_chatwidget_snapshot!("interrupted_turn_error_message", last);}#[tokio::test]async fn interrupted_turn_after_goal_budget_limited_uses_budget_message_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.set_feature_enabled(Feature::Goals, /*enabled*/ true);    chat.handle_server_notification(        codex_app_server_protocol::ServerNotification::TurnStarted(            codex_app_server_protocol::TurnStartedNotification {                thread_id: "thread-1".to_string(),                turn: codex_app_server_protocol::Turn {                    id: "turn-1".to_string(),                    items_view: codex_app_server_protocol::TurnItemsView::Full,                    items: Vec::new(),                    status: codex_app_server_protocol::TurnStatus::InProgress,                    error: None,                    started_at: None,                    completed_at: None,                    duration_ms: None,                },            },        ),        /*replay_kind*/ None,    );    chat.handle_server_notification(        codex_app_server_protocol::ServerNotification::ThreadGoalUpdated(            codex_app_server_protocol::ThreadGoalUpdatedNotification {                thread_id: "thread-1".to_string(),                turn_id: Some("turn-1".to_string()),                goal: codex_app_server_protocol::ThreadGoal {                    thread_id: "thread-1".to_string(),                    objective: "Run until the token budget is limited".to_string(),                    status: codex_app_server_protocol::ThreadGoalStatus::BudgetLimited,                    token_budget: Some(10_000),                    tokens_used: 10_500,                    time_used_seconds: 0,                    created_at: 0,                    updated_at: 1,                },            },        ),        /*replay_kind*/ None,    );    chat.handle_server_notification(        codex_app_server_protocol::ServerNotification::TurnCompleted(            codex_app_server_protocol::TurnCompletedNotification {                thread_id: "thread-1".to_string(),                turn: codex_app_server_protocol::Turn {                    id: "turn-1".to_string(),                    items_view: codex_app_server_protocol::TurnItemsView::Full,                    items: Vec::new(),                    status: codex_app_server_protocol::TurnStatus::Interrupted,                    error: None,                    started_at: None,                    completed_at: None,                    duration_ms: None,                },            },        ),        /*replay_kind*/ None,    );    let cells = drain_insert_history(&mut rx);    let last = lines_to_single_string(cells.last().unwrap());    assert_chatwidget_snapshot!("interrupted_turn_goal_budget_limited_message", last);}#[tokio::test]async fn direct_budget_limited_turn_uses_budget_message_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_turn_started(&mut chat, "turn-1");    handle_budget_limited_turn(&mut chat, "turn-1");    let cells = drain_insert_history(&mut rx);    let last = lines_to_single_string(cells.last().unwrap());    assert_chatwidget_snapshot!("direct_budget_limited_turn_message", last);}#[tokio::test]async fn budget_limited_turn_restores_queued_input_without_submitting() {    let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.input_queue        .queued_user_messages        .push_back(UserMessage::from("follow-up after budget stop").into());    chat.refresh_pending_input_preview();    handle_turn_started(&mut chat, "turn-1");    handle_budget_limited_turn(&mut chat, "turn-1");    assert!(chat.input_queue.queued_user_messages.is_empty());    assert_eq!(        chat.bottom_pane.composer_text(),        "follow-up after budget stop"    );    assert_no_submit_op(&mut op_rx);}// Snapshot test: interrupting specifically to submit pending steers shows an// informational banner instead of the generic "tell the model what to do// differently" error prompt.#[tokio::test]async fn interrupted_turn_pending_steers_message_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    chat.input_queue        .pending_steers        .push_back(pending_steer("steer 1"));    chat.input_queue.submit_pending_steers_after_interrupt = true;    handle_turn_started(&mut chat, "turn-1");    handle_turn_interrupted(&mut chat, "turn-1");    let cells = drain_insert_history(&mut rx);    let info = cells        .iter()        .map(|cell| lines_to_single_string(cell))        .find(|line| line.contains("Model interrupted to submit steer instructions."))        .expect("expected steer interrupt info message to be inserted");    assert_chatwidget_snapshot!("interrupted_turn_pending_steers_message", info);}/// Opening custom prompt from the review popup, pressing Esc returns to the/// parent popup, pressing Esc again dismisses all panels (back to normal mode).#[tokio::test]async fn review_custom_prompt_escape_navigates_back_then_dismisses() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Open the Review presets parent popup.    chat.open_review_popup();    // Open the custom prompt submenu (child view) directly.    chat.show_review_custom_prompt();    // Verify child view is on top.    let header = render_bottom_first_row(&chat, /*width*/ 60);    assert!(        header.contains("Custom review instructions"),        "expected custom prompt view header: {header:?}"    );    // Esc once: child view closes, parent (review presets) remains.    chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    let header = render_bottom_first_row(&chat, /*width*/ 60);    assert!(        header.contains("Select a review preset"),        "expected to return to parent review popup: {header:?}"    );    // Esc again: parent closes; back to normal composer state.    chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    assert!(        chat.is_normal_backtrack_mode(),        "expected to be back in normal composer mode"    );}/// Opening base-branch picker from the review popup, pressing Esc returns to the/// parent popup, pressing Esc again dismisses all panels (back to normal mode).#[tokio::test]async fn review_branch_picker_escape_navigates_back_then_dismisses() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Open the Review presets parent popup.    chat.open_review_popup();    // Open the branch picker submenu (child view). Using a temp cwd with no git repo is fine.    let cwd = std::env::temp_dir();    chat.show_review_branch_picker(&cwd).await;    // Verify child view header.    let header = render_bottom_first_row(&chat, /*width*/ 60);    assert!(        header.contains("Select a base branch"),        "expected branch picker header: {header:?}"    );    // Esc once: child view closes, parent remains.    chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    let header = render_bottom_first_row(&chat, /*width*/ 60);    assert!(        header.contains("Select a review preset"),        "expected to return to parent review popup: {header:?}"    );    // Esc again: parent closes; back to normal composer state.    chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    assert!(        chat.is_normal_backtrack_mode(),        "expected to be back in normal composer mode"    );}#[tokio::test]async fn enter_submits_steer_while_review_is_running() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    handle_turn_started(&mut chat, "turn-1");    handle_entered_review_mode(&mut chat, "current changes");    let _ = drain_insert_history(&mut rx);    chat.bottom_pane.set_composer_text(        "Steer submitted while /review was running.".to_string(),        Vec::new(),        Vec::new(),    );    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert!(chat.input_queue.queued_user_messages.is_empty());    assert_eq!(chat.input_queue.pending_steers.len(), 1);    assert_eq!(        chat.input_queue            .pending_steers            .front()            .unwrap()            .user_message            .text,        "Steer submitted while /review was running."    );    match next_submit_op(&mut op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "Steer submitted while /review was running.".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected running-turn steer submit, got {other:?}"),    }    assert!(drain_insert_history(&mut rx).is_empty());}#[tokio::test]async fn review_queues_user_messages_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    handle_turn_started(&mut chat, "turn-1");    handle_entered_review_mode(&mut chat, "current changes");    let _ = drain_insert_history(&mut rx);    chat.submit_user_message(UserMessage::from(        "Steer submitted while /review was running.".to_string(),    ));    handle_error(        &mut chat,        "cannot steer a review turn",        Some(CodexErrorInfo::ActiveTurnNotSteerable {            turn_kind: NonSteerableTurnKind::Review,        }),    );    let width: u16 = 80;    let height: u16 = 18;    let backend = VT100Backend::new(width, height);    let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");    let desired_height = chat.desired_height(width).min(height);    term.set_viewport_area(Rect::new(0, height - desired_height, width, desired_height));    term.draw(|f| {        chat.render(f.area(), f.buffer_mut());    })    .unwrap();    assert_chatwidget_snapshot!(        "review_queues_user_messages_snapshot",        normalize_snapshot_paths(term.backend().vt100().screen().contents())    );}