Codex Handbook
tui/src/chatwidget/tests/exec_flow.rs 1725 lines
use super::*;use pretty_assertions::assert_eq;#[tokio::test]async fn exec_approval_emits_proposed_command_and_decision_history() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Trigger an exec approval request with a short, single-line command    let ev = ExecApprovalRequestEvent {        call_id: "call-short".into(),        approval_id: Some("call-short".into()),        turn_id: "turn-short".into(),        command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: Some(            "this is a test reason such as one that would be produced by the model".into(),        ),        network_approval_context: None,        proposed_execpolicy_amendment: None,        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-short", ev);    let proposed_cells = drain_insert_history(&mut rx);    assert!(        proposed_cells.is_empty(),        "expected approval request to render via modal without emitting history cells"    );    // The approval modal should display the command snippet for user confirmation.    let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));    let mut buf = ratatui::buffer::Buffer::empty(area);    chat.render(area, &mut buf);    assert_chatwidget_snapshot!("exec_approval_modal_exec", format!("{buf:?}"));    // Approve via keyboard and verify a concise decision history line is added    chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    let decision = drain_insert_history(&mut rx)        .pop()        .expect("expected decision cell in history");    assert_chatwidget_snapshot!(        "exec_approval_history_decision_approved_short",        lines_to_single_string(&decision)    );}#[test]fn app_server_exec_approval_request_splits_shell_wrapped_command() {    let script = r#"python3 -c 'print("Hello, world!")'"#;    let request = exec_approval_request_from_params(        AppServerCommandExecutionRequestApprovalParams {            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            item_id: "item-1".to_string(),            started_at_ms: 0,            approval_id: Some("approval-1".to_string()),            reason: None,            network_approval_context: None,            command: Some(                shlex::try_join(["/bin/zsh", "-lc", script])                    .expect("round-trippable shell wrapper"),            ),            cwd: Some(test_path_buf("/tmp").abs()),            command_actions: None,            additional_permissions: None,            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            available_decisions: None,        },        &test_path_buf("/tmp").abs(),    );    assert_eq!(        request.command,        vec![            "/bin/zsh".to_string(),            "-lc".to_string(),            script.to_string(),        ]    );}#[tokio::test]async fn exec_approval_uses_approval_id_when_present() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_exec_approval_request(        &mut chat,        "sub-short",        ExecApprovalRequestEvent {            call_id: "call-parent".into(),            approval_id: Some("approval-subcommand".into()),            turn_id: "turn-short".into(),            command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],            cwd: AbsolutePathBuf::current_dir().expect("current dir"),            reason: Some(                "this is a test reason such as one that would be produced by the model".into(),            ),            network_approval_context: None,            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            additional_permissions: None,            available_decisions: None,        },    );    chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    let mut found = false;    while let Ok(app_ev) = rx.try_recv() {        if let AppEvent::SubmitThreadOp {            op: Op::ExecApproval { id, decision, .. },            ..        } = app_ev        {            assert_eq!(id, "approval-subcommand");            assert_matches!(                decision,                codex_app_server_protocol::CommandExecutionApprovalDecision::Accept            );            found = true;            break;        }    }    assert!(found, "expected ExecApproval op to be sent");}#[tokio::test]async fn exec_approval_decision_truncates_multiline_and_long_commands() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Multiline command: modal should show full command, history records decision only    let ev_multi = ExecApprovalRequestEvent {        call_id: "call-multi".into(),        approval_id: Some("call-multi".into()),        turn_id: "turn-multi".into(),        command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()],        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: Some(            "this is a test reason such as one that would be produced by the model".into(),        ),        network_approval_context: None,        proposed_execpolicy_amendment: None,        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);    let proposed_multi = drain_insert_history(&mut rx);    assert!(        proposed_multi.is_empty(),        "expected multiline approval request to render via modal without emitting history cells"    );    let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));    let mut buf = ratatui::buffer::Buffer::empty(area);    chat.render(area, &mut buf);    let mut saw_first_line = false;    for y in 0..area.height {        let mut row = String::new();        for x in 0..area.width {            row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));        }        if row.contains("echo line1") {            saw_first_line = true;            break;        }    }    assert!(        saw_first_line,        "expected modal to show first line of multiline snippet"    );    // Deny via keyboard; decision snippet should be single-line and elided with " ..."    chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));    let aborted_multi = drain_insert_history(&mut rx)        .pop()        .expect("expected aborted decision cell (multiline)");    assert_chatwidget_snapshot!(        "exec_approval_history_decision_aborted_multiline",        lines_to_single_string(&aborted_multi)    );    // Very long single-line command: decision snippet should be truncated <= 80 chars with trailing ...    let long = format!("echo {}", "a".repeat(200));    let ev_long = ExecApprovalRequestEvent {        call_id: "call-long".into(),        approval_id: Some("call-long".into()),        turn_id: "turn-long".into(),        command: vec!["bash".into(), "-lc".into(), long],        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: None,        network_approval_context: None,        proposed_execpolicy_amendment: None,        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-long", ev_long);    let proposed_long = drain_insert_history(&mut rx);    assert!(        proposed_long.is_empty(),        "expected long approval request to avoid emitting history cells before decision"    );    chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));    let aborted_long = drain_insert_history(&mut rx)        .pop()        .expect("expected aborted decision cell (long)");    assert_chatwidget_snapshot!(        "exec_approval_history_decision_aborted_long",        lines_to_single_string(&aborted_long)    );}#[tokio::test]async fn preamble_keeps_working_status_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    // Regression sequence: a preamble line is committed to history before any exec/tool event.    // After commentary completes, the status row should be restored before subsequent work.    chat.on_task_started();    chat.on_agent_message_delta("Preamble line\n".to_string());    chat.on_commit_tick();    drain_insert_history(&mut rx);    complete_assistant_message(        &mut chat,        "msg-commentary-snapshot",        "Preamble line\n",        Some(MessagePhase::Commentary),    );    let height = chat.desired_height(/*width*/ 80);    let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height))        .expect("create terminal");    terminal        .draw(|f| chat.render(f.area(), f.buffer_mut()))        .expect("draw preamble + status widget");    assert_chatwidget_snapshot!(        "preamble_keeps_working_status",        normalized_backend_snapshot(terminal.backend())    );}#[tokio::test]async fn unified_exec_begin_restores_status_indicator_after_preamble() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    assert_eq!(chat.bottom_pane.status_indicator_visible(), true);    // Simulate a hidden status row during an active turn.    chat.bottom_pane.hide_status_indicator();    assert_eq!(chat.bottom_pane.status_indicator_visible(), false);    assert_eq!(chat.bottom_pane.is_task_running(), true);    begin_unified_exec_startup(&mut chat, "call-1", "proc-1", "sleep 2");    assert_eq!(chat.bottom_pane.status_indicator_visible(), true);}#[tokio::test]async fn unified_exec_begin_restores_working_status_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    chat.on_agent_message_delta("Preamble line\n".to_string());    chat.on_commit_tick();    drain_insert_history(&mut rx);    begin_unified_exec_startup(&mut chat, "call-1", "proc-1", "sleep 2");    let width: u16 = 80;    let height = chat.desired_height(width);    let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(width, height))        .expect("create terminal");    terminal.set_viewport_area(Rect::new(0, 0, width, height));    terminal        .draw(|f| chat.render(f.area(), f.buffer_mut()))        .expect("draw chatwidget");    assert_chatwidget_snapshot!(        "unified_exec_begin_restores_working_status",        normalized_backend_snapshot(terminal.backend())    );}#[tokio::test]async fn exec_history_cell_shows_working_then_completed() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Begin command    let begin = begin_exec(&mut chat, "call-1", "echo done");    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 0, "no exec cell should have been flushed yet");    // End command successfully    end_exec(&mut chat, begin, "done", "", /*exit_code*/ 0);    let cells = drain_insert_history(&mut rx);    // Exec end now finalizes and flushes the exec cell immediately.    assert_eq!(cells.len(), 1, "expected finalized exec cell to flush");    // Inspect the flushed exec cell rendering.    let lines = &cells[0];    let blob = lines_to_single_string(lines);    // New behavior: no glyph markers; ensure command is shown and no panic.    assert!(        blob.contains("• Ran"),        "expected summary header present: {blob:?}"    );    assert!(        blob.contains("echo done"),        "expected command text to be present: {blob:?}"    );}#[tokio::test]async fn exec_history_cell_shows_working_then_failed() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Begin command    let begin = begin_exec(&mut chat, "call-2", "false");    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 0, "no exec cell should have been flushed yet");    // End command with failure    end_exec(&mut chat, begin, "", "Bloop", /*exit_code*/ 2);    let cells = drain_insert_history(&mut rx);    // Exec end with failure should also flush immediately.    assert_eq!(cells.len(), 1, "expected finalized exec cell to flush");    let lines = &cells[0];    let blob = lines_to_single_string(lines);    assert!(        blob.contains("• Ran false"),        "expected command and header text present: {blob:?}"    );    assert!(blob.to_lowercase().contains("bloop"), "expected error text");}#[tokio::test]async fn exec_end_without_begin_uses_event_command() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let command = vec![        "bash".to_string(),        "-lc".to_string(),        "echo orphaned".to_string(),    ];    let command_actions = codex_shell_command::parse_command::parse_command(&command)        .into_iter()        .map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &chat.config.cwd))        .collect();    let cwd = chat.config.cwd.clone();    handle_exec_end(        &mut chat,        AppServerThreadItem::CommandExecution {            id: "call-orphan".to_string(),            command: codex_shell_command::parse_command::shlex_join(&command),            cwd,            process_id: None,            source: ExecCommandSource::Agent,            status: AppServerCommandExecutionStatus::Completed,            command_actions,            aggregated_output: Some("done".to_string()),            exit_code: Some(0),            duration_ms: Some(5),        },    );    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1, "expected finalized exec cell to flush");    let blob = lines_to_single_string(&cells[0]);    assert!(        blob.contains("• Ran echo orphaned"),        "expected command text to come from event: {blob:?}"    );    assert!(        !blob.contains("call-orphan"),        "call id should not be rendered when event has the command: {blob:?}"    );}#[tokio::test]async fn exec_end_without_begin_does_not_flush_unrelated_running_exploring_cell() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    begin_exec(&mut chat, "call-exploring", "cat /dev/null");    assert!(drain_insert_history(&mut rx).is_empty());    assert!(active_blob(&chat).contains("Read null"));    let orphan =        begin_unified_exec_startup(&mut chat, "call-orphan", "proc-1", "echo repro-marker");    assert!(drain_insert_history(&mut rx).is_empty());    end_exec(        &mut chat,        orphan,        "repro-marker\n",        "",        /*exit_code*/ 0,    );    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1, "only the orphan end should be inserted");    let orphan_blob = lines_to_single_string(&cells[0]);    assert!(        orphan_blob.contains("• Ran echo repro-marker"),        "expected orphan end to render a standalone entry: {orphan_blob:?}"    );    let active = active_blob(&chat);    assert!(        active.contains("• Exploring"),        "expected unrelated exploring call to remain active: {active:?}"    );    assert!(        active.contains("Read null"),        "expected active exploring command to remain visible: {active:?}"    );    assert!(        !active.contains("echo repro-marker"),        "orphaned end should not replace the active exploring cell: {active:?}"    );}#[tokio::test]async fn exec_end_without_begin_flushes_completed_unrelated_exploring_cell() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    let begin_ls = begin_exec(&mut chat, "call-ls", "ls -la");    end_exec(&mut chat, begin_ls, "", "", /*exit_code*/ 0);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(active_blob(&chat).contains("ls -la"));    let orphan = begin_unified_exec_startup(&mut chat, "call-after", "proc-1", "echo after");    end_exec(&mut chat, orphan, "after\n", "", /*exit_code*/ 0);    let cells = drain_insert_history(&mut rx);    assert_eq!(        cells.len(),        2,        "completed exploring cell should flush before the orphan entry"    );    let first = lines_to_single_string(&cells[0]);    let second = lines_to_single_string(&cells[1]);    assert!(        first.contains("• Explored"),        "expected flushed exploring cell: {first:?}"    );    assert!(        first.contains("List ls -la"),        "expected flushed exploring cell: {first:?}"    );    assert!(        second.contains("• Ran echo after"),        "expected orphan end entry after flush: {second:?}"    );    assert!(        chat.transcript.active_cell.is_none(),        "both entries should be finalized"    );}#[tokio::test]async fn overlapping_exploring_exec_end_is_not_misclassified_as_orphan() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let begin_ls = begin_exec(&mut chat, "call-ls", "ls -la");    let begin_cat = begin_exec(&mut chat, "call-cat", "cat foo.txt");    assert!(drain_insert_history(&mut rx).is_empty());    end_exec(&mut chat, begin_ls, "foo.txt\n", "", /*exit_code*/ 0);    let cells = drain_insert_history(&mut rx);    assert!(        cells.is_empty(),        "tracked end inside an exploring cell should not render as an orphan"    );    let active = active_blob(&chat);    assert!(        active.contains("List ls -la"),        "expected first command still grouped: {active:?}"    );    assert!(        active.contains("Read foo.txt"),        "expected second running command to stay in the same active cell: {active:?}"    );    assert!(        active.contains("• Exploring"),        "expected grouped exploring header to remain active: {active:?}"    );    end_exec(&mut chat, begin_cat, "hello\n", "", /*exit_code*/ 0);}#[tokio::test]async fn exec_history_shows_unified_exec_startup_commands() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    let begin = begin_exec_with_source(        &mut chat,        "call-startup",        "echo unified exec startup",        ExecCommandSource::UnifiedExecStartup,    );    assert!(        drain_insert_history(&mut rx).is_empty(),        "exec begin should not flush until completion"    );    end_exec(        &mut chat,        begin,        "echo unified exec startup\n",        "",        /*exit_code*/ 0,    );    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1, "expected finalized exec cell to flush");    let blob = lines_to_single_string(&cells[0]);    assert!(        blob.contains("• Ran echo unified exec startup"),        "expected startup command to render: {blob:?}"    );}#[tokio::test]async fn exec_history_shows_unified_exec_tool_calls() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    let begin = begin_exec_with_source(        &mut chat,        "call-startup",        "ls",        ExecCommandSource::UnifiedExecStartup,    );    end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);    let blob = active_blob(&chat);    assert_eq!(blob, "• Explored\n  └ List ls\n");}#[tokio::test]async fn unified_exec_unknown_end_with_active_exploring_cell_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    begin_exec(&mut chat, "call-exploring", "cat /dev/null");    let orphan =        begin_unified_exec_startup(&mut chat, "call-orphan", "proc-1", "echo repro-marker");    end_exec(        &mut chat,        orphan,        "repro-marker\n",        "",        /*exit_code*/ 0,    );    let cells = drain_insert_history(&mut rx);    let history = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    let active = active_blob(&chat);    let snapshot = format!("History:\n{history}\nActive:\n{active}");    assert_chatwidget_snapshot!(        "unified_exec_unknown_end_with_active_exploring_cell",        snapshot    );}#[tokio::test]async fn unified_exec_end_after_task_complete_is_suppressed() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    let begin = begin_exec_with_source(        &mut chat,        "call-startup",        "echo unified exec startup",        ExecCommandSource::UnifiedExecStartup,    );    drain_insert_history(&mut rx);    chat.on_task_complete(        /*last_agent_message*/ None, /*duration_ms*/ None, /*from_replay*/ false,    );    end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);    let cells = drain_insert_history(&mut rx);    assert!(        cells.is_empty(),        "expected unified exec end after task complete to be suppressed"    );}#[tokio::test]async fn unified_exec_interaction_after_task_complete_is_suppressed() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    chat.on_task_complete(        /*last_agent_message*/ None, /*duration_ms*/ None, /*from_replay*/ false,    );    terminal_interaction(&mut chat, "call-1", "proc-1", "ls\n");    let cells = drain_insert_history(&mut rx);    assert!(        cells.is_empty(),        "expected unified exec interaction after task complete to be suppressed"    );}#[tokio::test]async fn unified_exec_wait_after_final_agent_message_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_turn_started(&mut chat, "turn-1");    begin_unified_exec_startup(&mut chat, "call-wait", "proc-1", "cargo test -p codex-core");    terminal_interaction(&mut chat, "call-wait-stdin", "proc-1", "");    complete_assistant_message(&mut chat, "msg-1", "Final response.", /*phase*/ None);    handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);    let cells = drain_insert_history(&mut rx);    let combined = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_chatwidget_snapshot!("unified_exec_wait_after_final_agent_message", combined);}#[tokio::test]async fn unified_exec_wait_before_streamed_agent_message_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_turn_started(&mut chat, "turn-1");    begin_unified_exec_startup(        &mut chat,        "call-wait-stream",        "proc-1",        "cargo test -p codex-core",    );    terminal_interaction(&mut chat, "call-wait-stream-stdin", "proc-1", "");    handle_agent_message_delta(&mut chat, "Streaming response.");    handle_turn_completed(&mut chat, "turn-wait-1", /*duration_ms*/ None);    let cells = drain_insert_history(&mut rx);    let combined = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_chatwidget_snapshot!("unified_exec_wait_before_streamed_agent_message", combined);}#[tokio::test]async fn final_worked_for_uses_cumulative_turn_duration_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_turn_started(&mut chat, "turn-1");    let exec = begin_exec_with_source(        &mut chat,        "call-1",        "echo preparing",        ExecCommandSource::Agent,    );    end_exec(&mut chat, exec, "preparing\n", "", /*exit_code*/ 0);    complete_assistant_message(        &mut chat,        "msg-final",        "Final response.",        Some(MessagePhase::FinalAnswer),    );    handle_turn_completed(&mut chat, "turn-1", Some(125_000));    let cells = drain_insert_history(&mut rx);    let combined = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(        combined.contains("Worked for 2m 05s"),        "expected final separator to use cumulative turn duration, got:\n{combined}"    );    assert_chatwidget_snapshot!("final_worked_for_uses_cumulative_turn_duration", combined);}#[tokio::test]async fn unified_exec_wait_status_header_updates_on_late_command_display() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    chat.unified_exec_processes.push(UnifiedExecProcessSummary {        key: "proc-1".to_string(),        call_id: "call-1".to_string(),        command_display: "sleep 5".to_string(),        recent_chunks: Vec::new(),    });    terminal_interaction(&mut chat, "call-1", "proc-1", "");    assert!(chat.transcript.active_cell.is_none());    assert_eq!(        chat.status_state.current_status.header,        "Waiting for background terminal"    );    let status = chat        .bottom_pane        .status_widget()        .expect("status indicator should be visible");    assert_eq!(status.header(), "Waiting for background terminal");    assert_eq!(status.details(), Some("sleep 5"));}#[tokio::test]async fn unified_exec_empty_poll_for_finished_process_does_not_show_waiting_status() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    terminal_interaction(&mut chat, "call-finished", "proc-finished", "");    assert_eq!(chat.status_state.current_status.header, "Working");    let status = chat        .bottom_pane        .status_widget()        .expect("task status indicator should remain visible");    assert_eq!(status.header(), "Working");    assert!(chat.unified_exec_wait_streak.is_none());}#[tokio::test]async fn unified_exec_waiting_multiple_empty_snapshots() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    begin_unified_exec_startup(&mut chat, "call-wait-1", "proc-1", "just fix");    terminal_interaction(&mut chat, "call-wait-1a", "proc-1", "");    terminal_interaction(&mut chat, "call-wait-1b", "proc-1", "");    assert_eq!(        chat.status_state.current_status.header,        "Waiting for background terminal"    );    let status = chat        .bottom_pane        .status_widget()        .expect("status indicator should be visible");    assert_eq!(status.header(), "Waiting for background terminal");    assert_eq!(status.details(), Some("just fix"));    handle_turn_completed(&mut chat, "turn-wait-3", /*duration_ms*/ None);    let cells = drain_insert_history(&mut rx);    let combined = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_chatwidget_snapshot!("unified_exec_waiting_multiple_empty_after", combined);}#[tokio::test]async fn unified_exec_wait_status_renders_command_in_single_details_row_snapshot() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    begin_unified_exec_startup(        &mut chat,        "call-wait-ui",        "proc-ui",        "cargo test -p codex-core -- --exact some::very::long::test::name",    );    terminal_interaction(&mut chat, "call-wait-ui-stdin", "proc-ui", "");    let rendered = render_bottom_popup(&chat, /*width*/ 48);    assert_chatwidget_snapshot!(        "unified_exec_wait_status_renders_command_in_single_details_row",        normalize_snapshot_paths(rendered)    );}#[tokio::test]async fn unified_exec_empty_then_non_empty_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    begin_unified_exec_startup(&mut chat, "call-wait-2", "proc-2", "just fix");    terminal_interaction(&mut chat, "call-wait-2a", "proc-2", "");    terminal_interaction(&mut chat, "call-wait-2b", "proc-2", "ls\n");    let cells = drain_insert_history(&mut rx);    let combined = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_chatwidget_snapshot!("unified_exec_empty_then_non_empty_after", combined);}#[tokio::test]async fn unified_exec_non_empty_then_empty_snapshots() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.on_task_started();    begin_unified_exec_startup(&mut chat, "call-wait-3", "proc-3", "just fix");    terminal_interaction(&mut chat, "call-wait-3a", "proc-3", "pwd\n");    terminal_interaction(&mut chat, "call-wait-3b", "proc-3", "");    assert_eq!(        chat.status_state.current_status.header,        "Waiting for background terminal"    );    let status = chat        .bottom_pane        .status_widget()        .expect("status indicator should be visible");    assert_eq!(status.header(), "Waiting for background terminal");    assert_eq!(status.details(), Some("just fix"));    let pre_cells = drain_insert_history(&mut rx);    let active_combined = pre_cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_chatwidget_snapshot!("unified_exec_non_empty_then_empty_active", active_combined);    handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);    let post_cells = drain_insert_history(&mut rx);    let mut combined = pre_cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    let post = post_cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    if !combined.is_empty() && !post.is_empty() {        combined.push('\n');    }    combined.push_str(&post);    assert_chatwidget_snapshot!("unified_exec_non_empty_then_empty_after", combined);}#[tokio::test]async fn view_image_tool_call_adds_history_cell() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let image_path = chat.config.cwd.join("example.png");    handle_view_image_tool_call(&mut chat, "call-image", image_path);    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1, "expected a single history cell");    let combined = lines_to_single_string(&cells[0]);    assert_chatwidget_snapshot!("local_image_attachment_history_snapshot", combined);}#[tokio::test]async fn image_generation_call_adds_history_cell() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_image_generation_end(        &mut chat,        "call-image-generation",        "completed",        Some("A tiny blue square".into()),        Some(test_path_buf("/tmp/ig-1.png").abs()),    );    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1, "expected a single history cell");    let platform_file_url = url::Url::from_file_path(test_path_buf("/tmp/ig-1.png"))        .expect("test path should convert to file URL")        .to_string();    let combined =        lines_to_single_string(&cells[0]).replace(&platform_file_url, "file:///tmp/ig-1.png");    assert_chatwidget_snapshot!("image_generation_call_history_snapshot", combined);    handle_image_generation_end(        &mut chat,        "call-image-generation-failed",        "failed",        Some("A tiny blue square".into()),        /*saved_path*/ None,    );    let cells = drain_insert_history(&mut rx);    assert_eq!(cells.len(), 1, "expected a single failure history cell");    assert_chatwidget_snapshot!(        "failed_image_generation_call_history_snapshot",        lines_to_single_string(&cells[0])    );}#[tokio::test]async fn exec_history_extends_previous_when_consecutive() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // 1) Start "ls -la" (List)    let begin_ls = begin_exec(&mut chat, "call-ls", "ls -la");    assert_chatwidget_snapshot!("exploring_step1_start_ls", active_blob(&chat));    // 2) Finish "ls -la"    end_exec(&mut chat, begin_ls, "", "", /*exit_code*/ 0);    assert_chatwidget_snapshot!("exploring_step2_finish_ls", active_blob(&chat));    // 3) Start "cat foo.txt" (Read)    let begin_cat_foo = begin_exec(&mut chat, "call-cat-foo", "cat foo.txt");    assert_chatwidget_snapshot!("exploring_step3_start_cat_foo", active_blob(&chat));    // 4) Complete "cat foo.txt"    end_exec(        &mut chat,        begin_cat_foo,        "hello from foo",        "",        /*exit_code*/ 0,    );    assert_chatwidget_snapshot!("exploring_step4_finish_cat_foo", active_blob(&chat));    // 5) Start & complete "sed -n 100,200p foo.txt" (treated as Read of foo.txt)    let begin_sed_range = begin_exec(&mut chat, "call-sed-range", "sed -n 100,200p foo.txt");    end_exec(        &mut chat,        begin_sed_range,        "chunk",        "",        /*exit_code*/ 0,    );    assert_chatwidget_snapshot!("exploring_step5_finish_sed_range", active_blob(&chat));    // 6) Start & complete "cat bar.txt"    let begin_cat_bar = begin_exec(&mut chat, "call-cat-bar", "cat bar.txt");    end_exec(        &mut chat,        begin_cat_bar,        "hello from bar",        "",        /*exit_code*/ 0,    );    assert_chatwidget_snapshot!("exploring_step6_finish_cat_bar", active_blob(&chat));}#[tokio::test]async fn user_shell_command_renders_output_not_exploring() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let begin_ls = begin_exec_with_source(        &mut chat,        "user-shell-ls",        "ls",        ExecCommandSource::UserShell,    );    end_exec(        &mut chat,        begin_ls,        "file1\nfile2\n",        "",        /*exit_code*/ 0,    );    let cells = drain_insert_history(&mut rx);    assert_eq!(        cells.len(),        1,        "expected a single history cell for the user command"    );    let blob = lines_to_single_string(cells.first().unwrap());    assert_chatwidget_snapshot!("user_shell_ls_output", blob);}#[tokio::test]async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let thread_id = ThreadId::new();    let rollout_file = NamedTempFile::new().unwrap();    let configured = crate::session_state::ThreadSessionState {        thread_id,        forked_from_id: None,        fork_parent_title: None,        thread_name: None,        model: "test-model".to_string(),        model_provider_id: "test-provider".to_string(),        service_tier: None,        approval_policy: AskForApproval::Never,        approvals_reviewer: ApprovalsReviewer::User,        permission_profile: PermissionProfile::read_only(),        active_permission_profile: None,        cwd: test_path_buf("/home/user/project").abs(),        runtime_workspace_roots: Vec::new(),        instruction_source_paths: Vec::new(),        reasoning_effort: Some(ReasoningEffortConfig::default()),        collaboration_mode: None,        personality: None,        message_history: None,        network_proxy: None,        rollout_path: Some(rollout_file.path().to_path_buf()),    };    chat.handle_thread_session(configured);    drain_insert_history(&mut rx);    while op_rx.try_recv().is_ok() {}    handle_turn_started(&mut chat, "turn-1");    chat.bottom_pane        .set_composer_text("!echo hi".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    match op_rx.try_recv() {        Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"),        other => panic!("expected RunUserShellCommand op, got {other:?}"),    }    assert_matches!(        rx.try_recv(),        Ok(AppEvent::AppendMessageHistoryEntry { text, .. }) if text == "!echo hi"    );    assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));}#[tokio::test]async fn user_message_during_user_shell_command_is_queued_not_steered() {    let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.thread_id = Some(ThreadId::new());    handle_turn_started(&mut chat, "turn-1");    let begin = begin_exec_with_source(        &mut chat,        "user-shell-sleep",        "sleep 10",        ExecCommandSource::UserShell,    );    assert!(chat.only_user_shell_commands_running());    chat.bottom_pane        .set_composer_text("hi".to_string(), Vec::new(), Vec::new());    chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));    assert_eq!(chat.queued_user_message_texts(), vec!["hi".to_string()]);    end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);    complete_assistant_message(        &mut chat,        "msg-done",        "done",        Some(MessagePhase::FinalAnswer),    );    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: "hi".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected queued user message after shell completion, got {other:?}"),    }    assert!(chat.input_queue.queued_user_messages.is_empty());}#[tokio::test]async fn disabled_slash_command_while_task_running_snapshot() {    // Build a chat widget and simulate an active task    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.bottom_pane.set_task_running(/*running*/ true);    // Dispatch a command that is unavailable while a task runs (e.g., /model)    chat.dispatch_command(SlashCommand::Model);    // Drain history and snapshot the rendered error line(s)    let cells = drain_insert_history(&mut rx);    assert!(        !cells.is_empty(),        "expected an error message history cell to be emitted",    );    let blob = lines_to_single_string(cells.last().unwrap());    assert_chatwidget_snapshot!("disabled_slash_command_while_task_running_snapshot", blob);}//// Snapshot test: command approval modal//// Synthesizes an exec approval request to trigger the approval modal// and snapshots the visual output using the ratatui TestBackend.#[tokio::test]async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {    // Build a chat widget with manual channels to avoid spawning the agent.    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Ensure policy allows surfacing approvals explicitly (not strictly required for direct event).    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())?;    // Inject an exec approval request to display the approval modal.    let ev = ExecApprovalRequestEvent {        call_id: "call-approve-cmd".into(),        approval_id: Some("call-approve-cmd".into()),        turn_id: "turn-approve-cmd".into(),        command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: Some(            "this is a test reason such as one that would be produced by the model".into(),        ),        network_approval_context: None,        proposed_execpolicy_amendment: Some(ExecPolicyAmendment {            command: vec!["echo".into(), "hello".into(), "world".into()],        }),        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-approve", ev);    // Render to a fixed-size test terminal and snapshot.    // Call desired_height first and use that exact height for rendering.    let width = 100;    let height = chat.desired_height(width);    let mut terminal =        crate::custom_terminal::Terminal::with_options(VT100Backend::new(width, height))            .expect("create terminal");    let viewport = Rect::new(0, 0, width, height);    terminal.set_viewport_area(viewport);    terminal        .draw(|f| chat.render(f.area(), f.buffer_mut()))        .expect("draw approval modal");    assert!(        terminal            .backend()            .vt100()            .screen()            .contents()            .contains("echo hello world")    );    assert_chatwidget_snapshot!(        "approval_modal_exec",        terminal.backend().vt100().screen().contents()    );    Ok(())}// Snapshot test: command approval modal without a reason// Ensures spacing looks correct when no reason text is provided.#[tokio::test]async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())?;    let ev = ExecApprovalRequestEvent {        call_id: "call-approve-cmd-noreason".into(),        approval_id: Some("call-approve-cmd-noreason".into()),        turn_id: "turn-approve-cmd-noreason".into(),        command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: None,        network_approval_context: None,        proposed_execpolicy_amendment: Some(ExecPolicyAmendment {            command: vec!["echo".into(), "hello".into(), "world".into()],        }),        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-approve-noreason", ev);    let width = 100;    let height = chat.desired_height(width);    let mut terminal =        ratatui::Terminal::new(VT100Backend::new(width, height)).expect("create terminal");    terminal.set_viewport_area(Rect::new(0, 0, width, height));    terminal        .draw(|f| chat.render(f.area(), f.buffer_mut()))        .expect("draw approval modal (no reason)");    assert_chatwidget_snapshot!(        "approval_modal_exec_no_reason",        terminal.backend().vt100().screen().contents()    );    Ok(())}// Snapshot test: approval modal with a proposed execpolicy prefix that is multi-line;// we should not offer adding it to execpolicy.#[tokio::test]async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()-> anyhow::Result<()> {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())?;    let script = "python - <<'PY'\nprint('hello')\nPY".to_string();    let command = vec!["bash".into(), "-lc".into(), script];    let ev = ExecApprovalRequestEvent {        call_id: "call-approve-cmd-multiline-trunc".into(),        approval_id: Some("call-approve-cmd-multiline-trunc".into()),        turn_id: "turn-approve-cmd-multiline-trunc".into(),        command: command.clone(),        cwd: AbsolutePathBuf::current_dir().expect("current dir"),        reason: None,        network_approval_context: None,        proposed_execpolicy_amendment: Some(ExecPolicyAmendment { command }),        proposed_network_policy_amendments: None,        additional_permissions: None,        available_decisions: None,    };    handle_exec_approval_request(&mut chat, "sub-approve-multiline-trunc", ev);    let width = 100;    let height = chat.desired_height(width);    let mut terminal =        ratatui::Terminal::new(VT100Backend::new(width, height)).expect("create terminal");    terminal.set_viewport_area(Rect::new(0, 0, width, height));    terminal        .draw(|f| chat.render(f.area(), f.buffer_mut()))        .expect("draw approval modal (multiline prefix)");    let contents = terminal.backend().vt100().screen().contents();    assert!(!contents.contains("don't ask again"));    assert_chatwidget_snapshot!(        "approval_modal_exec_multiline_prefix_no_execpolicy",        contents    );    Ok(())}// Snapshot test: patch approval modal#[tokio::test]async fn approval_modal_patch_snapshot() -> anyhow::Result<()> {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())?;    // Build a small changeset and a reason/grant_root to exercise the prompt text.    let mut changes = HashMap::new();    changes.insert(        PathBuf::from("README.md"),        FileChange::Add {            content: "hello\nworld\n".into(),        },    );    let ev = ApplyPatchApprovalRequestEvent {        call_id: "call-approve-patch".into(),        turn_id: "turn-approve-patch".into(),        changes,        reason: Some("The model wants to apply changes".into()),        grant_root: Some(PathBuf::from("/tmp")),    };    handle_apply_patch_approval_request(&mut chat, "sub-approve-patch", ev);    // Render at the widget's desired height and snapshot.    let height = chat.desired_height(/*width*/ 80);    let mut terminal =        ratatui::Terminal::new(VT100Backend::new(/*width*/ 80, height)).expect("create terminal");    terminal.set_viewport_area(Rect::new(0, 0, 80, height));    terminal        .draw(|f| chat.render(f.area(), f.buffer_mut()))        .expect("draw patch approval modal");    let contents = terminal.backend().vt100().screen().contents();    assert!(!contents.contains("$ apply_patch"));    assert_chatwidget_snapshot!("approval_modal_patch", contents);    Ok(())}#[tokio::test]async fn interrupt_preserves_unified_exec_processes() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    begin_unified_exec_startup(&mut chat, "call-1", "process-1", "sleep 5");    begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6");    assert_eq!(chat.unified_exec_processes.len(), 2);    handle_turn_interrupted(&mut chat, "turn-1");    assert_eq!(chat.unified_exec_processes.len(), 2);    chat.add_ps_output();    let cells = drain_insert_history(&mut rx);    let combined = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<Vec<_>>()        .join("\n");    assert!(        combined.contains("Background terminals"),        "expected /ps to remain available after interrupt; got {combined:?}"    );    assert!(        combined.contains("sleep 5") && combined.contains("sleep 6"),        "expected /ps to list running unified exec processes; got {combined:?}"    );    let _ = drain_insert_history(&mut rx);}#[tokio::test]async fn interrupt_preserves_unified_exec_wait_streak_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_turn_started(&mut chat, "turn-1");    let begin = begin_unified_exec_startup(&mut chat, "call-1", "process-1", "just fix");    terminal_interaction(&mut chat, "call-1a", "process-1", "");    handle_turn_interrupted(&mut chat, "turn-1");    end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);    let cells = drain_insert_history(&mut rx);    let combined = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<Vec<_>>()        .join("\n");    let snapshot = format!("cells={}\n{combined}", cells.len());    assert_chatwidget_snapshot!("interrupt_preserves_unified_exec_wait_streak", snapshot);}#[tokio::test]async fn turn_complete_keeps_unified_exec_processes() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    begin_unified_exec_startup(&mut chat, "call-1", "process-1", "sleep 5");    begin_unified_exec_startup(&mut chat, "call-2", "process-2", "sleep 6");    assert_eq!(chat.unified_exec_processes.len(), 2);    handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);    assert_eq!(chat.unified_exec_processes.len(), 2);    chat.add_ps_output();    let cells = drain_insert_history(&mut rx);    let combined = cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<Vec<_>>()        .join("\n");    assert!(        combined.contains("Background terminals"),        "expected /ps to remain available after turn complete; got {combined:?}"    );    assert!(        combined.contains("sleep 5") && combined.contains("sleep 6"),        "expected /ps to list running unified exec processes; got {combined:?}"    );    let _ = drain_insert_history(&mut rx);}#[tokio::test]async fn apply_patch_events_emit_history_cells() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // 1) Approval request -> proposed patch summary cell    let mut changes = HashMap::new();    changes.insert(        PathBuf::from("foo.txt"),        FileChange::Add {            content: "hello\n".to_string(),        },    );    let ev = ApplyPatchApprovalRequestEvent {        call_id: "c1".into(),        turn_id: "turn-c1".into(),        changes,        reason: None,        grant_root: None,    };    handle_apply_patch_approval_request(&mut chat, "s1", ev);    assert!(        drain_insert_history(&mut rx).is_empty(),        "expected approval request to surface via modal without emitting history cells"    );    // 2) Begin apply -> per-file apply block cell (no global header)    let mut changes2 = HashMap::new();    changes2.insert(        PathBuf::from("foo.txt"),        FileChange::Add {            content: "hello\n".to_string(),        },    );    handle_patch_apply_begin(&mut chat, "c1", "turn-c1", changes2);    let cells = drain_insert_history(&mut rx);    assert!(!cells.is_empty(), "expected apply block cell to be sent");    let blob = lines_to_single_string(cells.last().unwrap());    assert!(        blob.contains("Added foo.txt") || blob.contains("Edited foo.txt"),        "expected single-file header with filename (Added/Edited): {blob:?}"    );    // 3) End apply success -> success cell    let mut end_changes = HashMap::new();    end_changes.insert(        PathBuf::from("foo.txt"),        FileChange::Add {            content: "hello\n".to_string(),        },    );    handle_patch_apply_end(        &mut chat,        "c1",        "turn-c1",        end_changes,        AppServerPatchApplyStatus::Completed,    );    let cells = drain_insert_history(&mut rx);    assert!(        cells.is_empty(),        "no success cell should be emitted anymore"    );}#[tokio::test]async fn apply_patch_manual_approval_adjusts_header() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let mut proposed_changes = HashMap::new();    proposed_changes.insert(        PathBuf::from("foo.txt"),        FileChange::Add {            content: "hello\n".to_string(),        },    );    handle_apply_patch_approval_request(        &mut chat,        "s1",        ApplyPatchApprovalRequestEvent {            call_id: "c1".into(),            turn_id: "turn-c1".into(),            changes: proposed_changes,            reason: None,            grant_root: None,        },    );    drain_insert_history(&mut rx);    let mut apply_changes = HashMap::new();    apply_changes.insert(        PathBuf::from("foo.txt"),        FileChange::Add {            content: "hello\n".to_string(),        },    );    handle_patch_apply_begin(&mut chat, "c1", "turn-c1", apply_changes);    let cells = drain_insert_history(&mut rx);    assert!(!cells.is_empty(), "expected apply block cell to be sent");    let blob = lines_to_single_string(cells.last().unwrap());    assert!(        blob.contains("Added foo.txt") || blob.contains("Edited foo.txt"),        "expected apply summary header for foo.txt: {blob:?}"    );}#[tokio::test]async fn apply_patch_manual_flow_snapshot() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    let mut proposed_changes = HashMap::new();    proposed_changes.insert(        PathBuf::from("foo.txt"),        FileChange::Add {            content: "hello\n".to_string(),        },    );    handle_apply_patch_approval_request(        &mut chat,        "s1",        ApplyPatchApprovalRequestEvent {            call_id: "c1".into(),            turn_id: "turn-c1".into(),            changes: proposed_changes,            reason: Some("Manual review required".into()),            grant_root: None,        },    );    let history_before_apply = drain_insert_history(&mut rx);    assert!(        history_before_apply.is_empty(),        "expected approval modal to defer history emission"    );    let mut apply_changes = HashMap::new();    apply_changes.insert(        PathBuf::from("foo.txt"),        FileChange::Add {            content: "hello\n".to_string(),        },    );    handle_patch_apply_begin(&mut chat, "c1", "turn-c1", apply_changes);    let approved_lines = drain_insert_history(&mut rx)        .pop()        .expect("approved patch cell");    assert_chatwidget_snapshot!(        "apply_patch_manual_flow_history_approved",        lines_to_single_string(&approved_lines)    );}#[tokio::test]async fn apply_patch_approval_sends_op_with_call_id() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Simulate receiving an approval request with a distinct event id and call id.    let mut changes = HashMap::new();    changes.insert(        PathBuf::from("file.rs"),        FileChange::Add {            content: "fn main(){}\n".into(),        },    );    let ev = ApplyPatchApprovalRequestEvent {        call_id: "call-999".into(),        turn_id: "turn-999".into(),        changes,        reason: None,        grant_root: None,    };    handle_apply_patch_approval_request(&mut chat, "sub-123", ev);    // Approve via key press 'y'    chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    // Expect a thread-scoped PatchApproval op carrying the call id.    let mut found = false;    while let Ok(app_ev) = rx.try_recv() {        if let AppEvent::SubmitThreadOp {            op: Op::PatchApproval { id, decision },            ..        } = app_ev        {            assert_eq!(id, "call-999");            assert_matches!(                decision,                codex_app_server_protocol::FileChangeApprovalDecision::Accept            );            found = true;            break;        }    }    assert!(found, "expected PatchApproval op to be sent");}#[tokio::test]async fn apply_patch_full_flow_integration_like() {    let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // 1) Backend requests approval    let mut changes = HashMap::new();    changes.insert(        PathBuf::from("pkg.rs"),        FileChange::Add { content: "".into() },    );    handle_apply_patch_approval_request(        &mut chat,        "sub-xyz",        ApplyPatchApprovalRequestEvent {            call_id: "call-1".into(),            turn_id: "turn-call-1".into(),            changes,            reason: None,            grant_root: None,        },    );    // 2) User approves via 'y' and App receives a thread-scoped op    chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    let mut maybe_op: Option<Op> = None;    while let Ok(app_ev) = rx.try_recv() {        if let AppEvent::SubmitThreadOp { op, .. } = app_ev {            maybe_op = Some(op);            break;        }    }    let op = maybe_op.expect("expected thread-scoped op after key press");    // 3) App forwards to widget.submit_op, which pushes onto codex_op_tx    chat.submit_op(op);    let forwarded = op_rx        .try_recv()        .expect("expected op forwarded to codex channel");    match forwarded {        Op::PatchApproval { id, decision } => {            assert_eq!(id, "call-1");            assert_matches!(                decision,                codex_app_server_protocol::FileChangeApprovalDecision::Accept            );        }        other => panic!("unexpected op forwarded: {other:?}"),    }    // 4) Simulate patch begin/end events from backend; ensure history cells are emitted    let mut changes2 = HashMap::new();    changes2.insert(        PathBuf::from("pkg.rs"),        FileChange::Add { content: "".into() },    );    handle_patch_apply_begin(&mut chat, "call-1", "turn-call-1", changes2);    let mut end_changes = HashMap::new();    end_changes.insert(        PathBuf::from("pkg.rs"),        FileChange::Add { content: "".into() },    );    handle_patch_apply_end(        &mut chat,        "call-1",        "turn-call-1",        end_changes,        AppServerPatchApplyStatus::Completed,    );}#[tokio::test]async fn apply_patch_untrusted_shows_approval_modal() -> anyhow::Result<()> {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Ensure approval policy is untrusted (OnRequest)    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())?;    // Simulate a patch approval request from backend    let mut changes = HashMap::new();    changes.insert(        PathBuf::from("a.rs"),        FileChange::Add { content: "".into() },    );    handle_apply_patch_approval_request(        &mut chat,        "sub-1",        ApplyPatchApprovalRequestEvent {            call_id: "call-1".into(),            turn_id: "turn-call-1".into(),            changes,            reason: None,            grant_root: None,        },    );    // Render and ensure the approval modal title is present    let area = Rect::new(0, 0, 80, 12);    let mut buf = Buffer::empty(area);    chat.render(area, &mut buf);    let mut contains_title = false;    for y in 0..area.height {        let mut row = String::new();        for x in 0..area.width {            row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));        }        if row.contains("Would you like to make the following edits?") {            contains_title = true;            break;        }    }    assert!(        contains_title,        "expected approval modal to be visible with title 'Would you like to make the following edits?'"    );    Ok(())}#[tokio::test]async fn apply_patch_request_omits_diff_summary_from_modal() -> anyhow::Result<()> {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    // Ensure we are in OnRequest so an approval is surfaced    chat.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())?;    // Simulate backend asking to apply a patch adding two lines to README.md    let mut changes = HashMap::new();    changes.insert(        PathBuf::from("README.md"),        FileChange::Add {            // Two lines (no trailing empty line counted)            content: "line one\nline two\n".into(),        },    );    handle_apply_patch_approval_request(        &mut chat,        "sub-apply",        ApplyPatchApprovalRequestEvent {            call_id: "call-apply".into(),            turn_id: "turn-apply".into(),            changes,            reason: None,            grant_root: None,        },    );    assert!(        drain_insert_history(&mut rx).is_empty(),        "expected approval request to render via modal instead of history"    );    let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));    let mut buf = ratatui::buffer::Buffer::empty(area);    chat.render(area, &mut buf);    let mut contents = String::new();    for y in 0..area.height {        for x in 0..area.width {            contents.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));        }        contents.push('\n');    }    assert!(!contents.contains("README.md (+2 -0)"));    assert!(!contents.contains("+line one"));    assert!(!contents.contains("+line two"));    Ok(())}