Codex Handbook
tui/src/chatwidget/tests/mcp_startup.rs 625 lines
use super::*;use pretty_assertions::assert_eq;fn notify_mcp_status(chat: &mut ChatWidget, name: &str, status: McpServerStartupState) {    chat.handle_server_notification(        ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {            thread_id: Some("thread-1".to_string()),            name: name.to_string(),            status,            error: None,        }),        /*replay_kind*/ None,    );}fn notify_mcp_status_error(chat: &mut ChatWidget, name: &str, error: &str) {    chat.handle_server_notification(        ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {            thread_id: Some("thread-1".to_string()),            name: name.to_string(),            status: McpServerStartupState::Failed,            error: Some(error.to_string()),        }),        /*replay_kind*/ None,    );}#[tokio::test]async fn mcp_startup_ignores_status_for_other_thread() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["sentry".to_string()]);    let parent_thread_id = ThreadId::new();    let child_thread_id = ThreadId::new();    chat.thread_id = Some(parent_thread_id);    chat.on_stream_error(        "Connection interrupted, retrying".to_string(),        /*additional_details*/ None,    );    let status_before = chat.status_state.current_status.clone();    let retry_status_header_before = chat.status_state.retry_status_header.clone();    for status in [        McpServerStartupState::Starting,        McpServerStartupState::Failed,    ] {        chat.handle_server_notification(            ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {                thread_id: Some(child_thread_id.to_string()),                name: "sentry".to_string(),                status,                error: matches!(status, McpServerStartupState::Failed)                    .then(|| "sentry is not logged in".to_string()),            }),            /*replay_kind*/ None,        );    }    assert!(drain_insert_history(&mut rx).is_empty());    assert!(!chat.bottom_pane.is_task_running());    assert!(chat.mcp_startup_status.is_none());    assert_eq!(chat.status_state.current_status, status_before);    assert_eq!(        chat.status_state.retry_status_header,        retry_status_header_before    );}#[tokio::test]async fn mcp_startup_dedupes_same_round_duplicate_failure_warning() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    let failure_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_eq!(        failure_text,        "⚠ MCP client for `alpha` failed to start: handshake failed\n"    );    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_eq!(summary_text, "⚠ MCP startup incomplete (failed: alpha)\n");}#[tokio::test]async fn mcp_startup_header_booting_snapshot() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    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 chat widget");    assert_chatwidget_snapshot!(        "mcp_startup_header_booting",        normalized_backend_snapshot(terminal.backend())    );}#[tokio::test]async fn mcp_startup_complete_does_not_clear_running_task() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    handle_turn_started(&mut chat, "turn-1");    assert!(chat.bottom_pane.is_task_running());    assert!(chat.bottom_pane.status_indicator_visible());    chat.set_mcp_startup_expected_servers(["schaltwerk".to_string()]);    notify_mcp_status(&mut chat, "schaltwerk", McpServerStartupState::Starting);    notify_mcp_status(&mut chat, "schaltwerk", McpServerStartupState::Ready);    assert!(chat.bottom_pane.is_task_running());    assert!(chat.bottom_pane.status_indicator_visible());    assert_eq!(chat.status_state.current_status.header, "Working");}#[tokio::test]async fn turn_start_preserves_active_mcp_startup_header() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.set_mcp_startup_expected_servers(["schaltwerk".to_string()]);    notify_mcp_status(&mut chat, "schaltwerk", McpServerStartupState::Starting);    handle_turn_started(&mut chat, "turn-1");    assert!(chat.bottom_pane.is_task_running());    assert_eq!(        chat.status_state.current_status.header,        "Booting MCP server: schaltwerk"    );    notify_mcp_status(&mut chat, "schaltwerk", McpServerStartupState::Ready);    assert_eq!(chat.status_state.current_status.header, "Working");}#[tokio::test]async fn turn_start_replaces_idle_completed_mcp_startup_header() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.set_mcp_startup_expected_servers(["schaltwerk".to_string()]);    notify_mcp_status(&mut chat, "schaltwerk", McpServerStartupState::Starting);    notify_mcp_status(&mut chat, "schaltwerk", McpServerStartupState::Ready);    assert!(!chat.bottom_pane.is_task_running());    assert_eq!(        chat.status_state.current_status.header,        "Booting MCP server: schaltwerk"    );    handle_turn_started(&mut chat, "turn-1");    assert!(chat.bottom_pane.is_task_running());    assert_eq!(chat.status_state.current_status.header, "Working");}#[tokio::test]async fn app_server_mcp_startup_failure_renders_warning_history() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(chat.bottom_pane.is_task_running());    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    let failure_cells = drain_insert_history(&mut rx);    let failure_text = failure_cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(failure_text.contains("MCP client for `alpha` failed to start: handshake failed"));    assert!(!failure_text.contains("MCP startup incomplete"));    assert!(chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    let summary_cells = drain_insert_history(&mut rx);    let summary_text = summary_cells        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_eq!(summary_text, "⚠ MCP startup incomplete (failed: alpha)\n");    assert!(!chat.bottom_pane.is_task_running());    let width: u16 = 120;    let ui_height: u16 = chat.desired_height(width);    let vt_height: u16 = ui_height.saturating_add(1).max(10);    let viewport = Rect::new(0, vt_height - ui_height - 1, width, ui_height);    let backend = VT100Backend::new(width, vt_height);    let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");    term.set_viewport_area(viewport);    for lines in failure_cells.into_iter().chain(summary_cells) {        crate::insert_history::insert_history_lines(&mut term, lines)            .expect("Failed to insert history lines in test");    }    term.draw(|f| {        chat.render(f.area(), f.buffer_mut());    })    .expect("draw MCP startup warning history");    assert_chatwidget_snapshot!(        "app_server_mcp_startup_failure_renders_warning_history",        normalize_snapshot_paths(term.backend().vt100().screen().contents())    );}#[tokio::test]async fn mcp_startup_failure_restores_running_status_header() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    handle_turn_started(&mut chat, "turn-1");    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    assert!(        chat.status_state            .current_status            .header            .starts_with("Starting MCP servers")    );    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    let _ = drain_insert_history(&mut rx);    assert!(chat.bottom_pane.is_task_running());    assert!(chat.bottom_pane.status_indicator_visible());    assert_eq!(chat.status_state.current_status.header, "Working");}#[tokio::test]async fn mcp_startup_complete_preserves_review_status() {    let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string()]);    handle_turn_started(&mut chat, "turn-1");    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    assert!(        chat.status_state            .current_status            .header            .starts_with("Booting MCP server")    );    chat.on_guardian_assessment(GuardianAssessmentEvent {        id: "guardian-1".to_string(),        target_item_id: Some("guardian-target-1".to_string()),        turn_id: "turn-1".to_string(),        started_at_ms: 0,        completed_at_ms: None,        status: GuardianAssessmentStatus::InProgress,        risk_level: None,        user_authorization: None,        rationale: None,        decision_source: None,        action: GuardianAssessmentAction::Command {            source: GuardianCommandSource::Shell,            command: "rm -rf '/tmp/guardian target'".to_string(),            cwd: test_path_buf("/tmp").abs(),        },    });    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Ready);    assert!(chat.bottom_pane.is_task_running());    assert!(chat.bottom_pane.status_indicator_visible());    assert_eq!(        chat.status_state.current_status.header,        "Reviewing approval request"    );    assert_eq!(        chat.status_state.current_status.details,        Some("rm -rf '/tmp/guardian target'".to_string())    );}#[tokio::test]async fn app_server_mcp_startup_lag_settles_startup_and_ignores_late_updates() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    let _ = drain_insert_history(&mut rx);    assert!(chat.bottom_pane.is_task_running());    chat.finish_mcp_startup_after_lag();    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(summary_text.contains("MCP startup interrupted"));    assert!(summary_text.contains("beta"));    assert!(summary_text.contains("MCP startup incomplete (failed: alpha)"));    assert!(!chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(!chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(!chat.bottom_pane.is_task_running());}#[tokio::test]async fn app_server_mcp_startup_after_lag_can_settle_without_starting_updates() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    chat.finish_mcp_startup_after_lag();    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    let failure_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(failure_text.contains("MCP client for `alpha` failed to start: handshake failed"));    assert!(chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_eq!(summary_text, "⚠ MCP startup incomplete (failed: alpha)\n");    assert!(!chat.bottom_pane.is_task_running());}#[tokio::test]async fn app_server_mcp_startup_after_lag_preserves_partial_terminal_only_round() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    let _ = drain_insert_history(&mut rx);    chat.finish_mcp_startup_after_lag();    let _ = drain_insert_history(&mut rx);    assert!(!chat.bottom_pane.is_task_running());    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    assert!(drain_insert_history(&mut rx).is_empty());    assert!(!chat.bottom_pane.is_task_running());    chat.finish_mcp_startup_after_lag();    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(summary_text.contains("MCP client for `alpha` failed to start: handshake failed"));    assert!(summary_text.contains("MCP startup incomplete (failed: alpha)"));    assert!(!chat.bottom_pane.is_task_running());}#[tokio::test]async fn app_server_mcp_startup_next_round_discards_stale_terminal_updates() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    let _ = drain_insert_history(&mut rx);    chat.finish_mcp_startup_after_lag();    let _ = drain_insert_history(&mut rx);    assert!(!chat.bottom_pane.is_task_running());    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: stale handshake failed",    );    assert!(drain_insert_history(&mut rx).is_empty());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(!chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Ready);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(summary_text.is_empty());    assert!(!chat.bottom_pane.is_task_running());}#[tokio::test]async fn app_server_mcp_startup_next_round_keeps_terminal_statuses_after_starting() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    chat.finish_mcp_startup_after_lag();    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    assert!(drain_insert_history(&mut rx).is_empty());    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    let failure_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(failure_text.contains("MCP client for `alpha` failed to start: handshake failed"));    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert_eq!(summary_text, "⚠ MCP startup incomplete (failed: alpha)\n");    assert!(!chat.bottom_pane.is_task_running());}#[tokio::test]async fn app_server_mcp_startup_next_round_with_empty_expected_servers_reactivates() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(std::iter::empty::<String>());    chat.finish_mcp_startup(Vec::new(), Vec::new());    notify_mcp_status(&mut chat, "runtime", McpServerStartupState::Starting);    assert!(drain_insert_history(&mut rx).is_empty());    assert!(chat.bottom_pane.is_task_running());    notify_mcp_status_error(        &mut chat,        "runtime",        "MCP client for `runtime` failed to start: handshake failed",    );    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(summary_text.contains("MCP client for `runtime` failed to start: handshake failed"));    assert!(summary_text.contains("MCP startup incomplete (failed: runtime)"));    assert!(!chat.bottom_pane.is_task_running());}#[tokio::test]async fn app_server_mcp_startup_after_lag_includes_runtime_servers_with_expected_set() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string()]);    notify_mcp_status_error(        &mut chat,        "runtime",        "MCP client for `runtime` failed to start: handshake failed",    );    let warning_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(warning_text.contains("MCP client for `runtime` failed to start: handshake failed"));    assert!(chat.bottom_pane.is_task_running());    chat.finish_mcp_startup_after_lag();    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(summary_text.contains("MCP startup incomplete (failed: runtime)"));    assert!(!chat.bottom_pane.is_task_running());}#[tokio::test]async fn app_server_mcp_startup_next_round_after_lag_can_settle_without_starting_updates() {    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;    chat.show_welcome_banner = false;    chat.set_mcp_startup_expected_servers(["alpha".to_string(), "beta".to_string()]);    notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting);    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Starting);    let _ = drain_insert_history(&mut rx);    chat.finish_mcp_startup_after_lag();    let _ = drain_insert_history(&mut rx);    assert!(!chat.bottom_pane.is_task_running());    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: stale handshake failed",    );    assert!(drain_insert_history(&mut rx).is_empty());    chat.finish_mcp_startup_after_lag();    notify_mcp_status_error(        &mut chat,        "alpha",        "MCP client for `alpha` failed to start: handshake failed",    );    let failure_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(failure_text.is_empty());    assert!(!chat.bottom_pane.is_task_running());    notify_mcp_status(&mut chat, "beta", McpServerStartupState::Ready);    let summary_text = drain_insert_history(&mut rx)        .iter()        .map(|lines| lines_to_single_string(lines))        .collect::<String>();    assert!(summary_text.contains("MCP client for `alpha` failed to start: handshake failed"));    assert!(summary_text.contains("MCP startup incomplete (failed: alpha)"));    assert!(!chat.bottom_pane.is_task_running());}