Codex Handbook
tui/src/app/tests.rs 6245 lines
//! App-level orchestration tests for the TUI.mod model_catalog;mod session_summary;mod startup;use super::*;use crate::app_backtrack::BacktrackSelection;use crate::app_backtrack::BacktrackState;use crate::app_backtrack::user_count;use crate::chatwidget::ChatWidgetInit;use crate::chatwidget::create_initial_user_message;use crate::chatwidget::tests::helpers::render_bottom_popup;use crate::chatwidget::tests::make_chatwidget_manual_with_sender;use crate::chatwidget::tests::set_chatgpt_auth;use crate::chatwidget::tests::set_fast_mode_test_catalog;use crate::file_search::FileSearchManager;use crate::goal_files;use crate::history_cell::AgentMarkdownCell;use crate::history_cell::AgentMessageCell;use crate::history_cell::HistoryCell;use crate::history_cell::PlainHistoryCell;use crate::history_cell::UserHistoryCell;use crate::history_cell::new_session_info;use crate::multi_agents::AgentPickerThreadEntry;use crate::multi_agents::SubAgentActivityDisplay;use assert_matches::assert_matches;use crate::app_command::AppCommand as Op;use crate::diff_model::FileChange;use crate::legacy_core::config::ConfigBuilder;use crate::legacy_core::config::ConfigOverrides;use crate::legacy_core::config::PermissionProfileSnapshot;use crate::legacy_core::config::TerminalResizeReflowMaxRows;use codex_app_server_client::AppServerPath;use codex_app_server_protocol::AdditionalFileSystemPermissions;use codex_app_server_protocol::AdditionalNetworkPermissions;use codex_app_server_protocol::AdditionalPermissionProfile;use codex_app_server_protocol::AgentMessageDeltaNotification;use codex_app_server_protocol::AskForApproval;use codex_app_server_protocol::CommandExecutionRequestApprovalParams;use codex_app_server_protocol::ConfigWarningNotification;use codex_app_server_protocol::FileChangeRequestApprovalParams;use codex_app_server_protocol::FileUpdateChange;use codex_app_server_protocol::ItemStartedNotification;use codex_app_server_protocol::JSONRPCErrorError;use codex_app_server_protocol::McpServerElicitationRequest;use codex_app_server_protocol::McpServerElicitationRequestParams;use codex_app_server_protocol::McpServerStartupState;use codex_app_server_protocol::McpServerStatusUpdatedNotification;use codex_app_server_protocol::NetworkApprovalContext as AppServerNetworkApprovalContext;use codex_app_server_protocol::NetworkApprovalProtocol as AppServerNetworkApprovalProtocol;use codex_app_server_protocol::NetworkPolicyAmendment as AppServerNetworkPolicyAmendment;use codex_app_server_protocol::NetworkPolicyRuleAction as AppServerNetworkPolicyRuleAction;use codex_app_server_protocol::NonSteerableTurnKind as AppServerNonSteerableTurnKind;use codex_app_server_protocol::PatchChangeKind;use codex_app_server_protocol::PermissionsRequestApprovalParams;use codex_app_server_protocol::RequestId as AppServerRequestId;use codex_app_server_protocol::ServerNotification;use codex_app_server_protocol::ServerRequest;use codex_app_server_protocol::SessionSource;use codex_app_server_protocol::Thread;use codex_app_server_protocol::ThreadClosedNotification;use codex_app_server_protocol::ThreadItem;use codex_app_server_protocol::ThreadSettings;use codex_app_server_protocol::ThreadSettingsUpdatedNotification;use codex_app_server_protocol::ThreadStartedNotification;use codex_app_server_protocol::ThreadTokenUsage;use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification;use codex_app_server_protocol::TokenUsageBreakdown;use codex_app_server_protocol::ToolRequestUserInputParams;use codex_app_server_protocol::Turn;use codex_app_server_protocol::TurnCompletedNotification;use codex_app_server_protocol::TurnError as AppServerTurnError;use codex_app_server_protocol::TurnStartedNotification;use codex_app_server_protocol::TurnStatus;use codex_app_server_protocol::UserInput;use codex_app_server_protocol::UserInput as AppServerUserInput;use codex_app_server_protocol::WarningNotification;use codex_models_manager::test_support::construct_model_info_offline_for_tests;use codex_models_manager::test_support::get_model_offline_for_tests;use codex_otel::SessionTelemetry;use codex_protocol::ThreadId;use codex_protocol::config_types::CollaborationMode;use codex_protocol::config_types::CollaborationModeMask;use codex_protocol::config_types::ModeKind;use codex_protocol::config_types::Personality;use codex_protocol::config_types::SandboxMode;use codex_protocol::config_types::ServiceTier;use codex_protocol::config_types::Settings;use codex_protocol::models::ActivePermissionProfile;use codex_protocol::models::FileSystemPermissions;use codex_protocol::models::NetworkPermissions;use codex_protocol::models::PermissionProfile;use codex_protocol::protocol::MAX_THREAD_GOAL_OBJECTIVE_CHARS;use codex_protocol::request_permissions::RequestPermissionProfile;use codex_protocol::user_input::TextElement;use codex_utils_absolute_path::AbsolutePathBuf;use crossterm::event::KeyModifiers;use insta::assert_snapshot;use pretty_assertions::assert_eq;use ratatui::prelude::Line;use std::path::Path;use std::path::PathBuf;use std::sync::Arc;use std::sync::atomic::AtomicBool;use tempfile::tempdir;use tokio::time;macro_rules! assert_app_snapshot {    ($name:expr, $value:expr $(,)?) => {        insta::with_settings!({snapshot_path => "../snapshots"}, {            assert_snapshot!($name, $value);        });    };}fn test_absolute_path(path: &str) -> AbsolutePathBuf {    AbsolutePathBuf::try_from(PathBuf::from(path)).expect("absolute test path")}async fn next_thread_settings_updated(    app_server: &mut AppServerSession,    thread_id: ThreadId,) -> ThreadSettingsUpdatedNotification {    for _ in 0..20 {        let event = time::timeout(            std::time::Duration::from_secs(/*secs*/ 2),            app_server.next_event(),        )        .await        .expect("app-server should emit an event")        .expect("app-server event stream should remain open");        if let codex_app_server_client::AppServerEvent::ServerNotification(            ServerNotification::ThreadSettingsUpdated(notification),        ) = event            && notification.thread_id == thread_id.to_string()        {            return notification;        }    }    panic!("expected ThreadSettingsUpdated for thread {thread_id}");}#[tokio::test]async fn handle_mcp_inventory_result_respects_origin_thread() {    let mut app = make_test_app().await;    app.transcript_cells        .push(Arc::new(history_cell::new_mcp_inventory_loading(            /*animations_enabled*/ false,        )));    app.handle_mcp_inventory_result(        Ok(vec![McpServerStatus {            name: "docs".to_string(),            server_info: None,            tools: HashMap::new(),            resources: Vec::new(),            resource_templates: Vec::new(),            auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported,        }]),        McpServerStatusDetail::ToolsAndAuthOnly,        /*thread_id*/ None,    );    assert_eq!(app.transcript_cells.len(), 0);    app.active_thread_id = Some(ThreadId::new());    app.transcript_cells        .push(Arc::new(history_cell::new_mcp_inventory_loading(            /*animations_enabled*/ false,        )));    app.handle_mcp_inventory_result(        Ok(Vec::new()),        McpServerStatusDetail::ToolsAndAuthOnly,        Some(ThreadId::new()),    );    assert_eq!(app.transcript_cells.len(), 1);}#[test]fn bypass_hook_trust_startup_warning_snapshot() {    let rendered = lines_to_single_string(        &history_cell::new_warning_event(            "`--dangerously-bypass-hook-trust` is enabled. Enabled hooks may run without review for this invocation."                .to_string(),        )        .display_lines(/*width*/ 80),    );    assert_app_snapshot!("bypass_hook_trust_startup_warning", rendered);}#[tokio::test]async fn enqueue_primary_thread_session_replays_buffered_approval_after_attach() -> Result<()> {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let approval_request =        exec_approval_request(thread_id, "turn-1", "call-1", /*approval_id*/ None);    assert_eq!(        app.pending_app_server_requests            .note_server_request(&approval_request),        None    );    app.enqueue_primary_thread_request(approval_request).await?;    app.enqueue_primary_thread_session(        test_thread_session(thread_id, test_path_buf("/tmp/project")),        Vec::new(),    )    .await?;    let rx = app        .active_thread_rx        .as_mut()        .expect("primary thread receiver should be active");    let event = time::timeout(Duration::from_millis(50), rx.recv())        .await        .expect("timed out waiting for buffered approval event")        .expect("channel closed unexpectedly");    assert!(matches!(        &event,        ThreadBufferedEvent::Request(ServerRequest::CommandExecutionRequestApproval {            params,            ..        }) if params.turn_id == "turn-1"    ));    app.handle_thread_event_now(event);    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    while let Ok(app_event) = app_event_rx.try_recv() {        if let AppEvent::SubmitThreadOp {            thread_id: op_thread_id,            ..        } = app_event        {            assert_eq!(op_thread_id, thread_id);            return Ok(());        }    }    panic!("expected approval action to submit a thread-scoped op");}#[tokio::test]async fn resolved_buffered_approval_does_not_become_actionable_after_drain() -> Result<()> {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let approval_request =        exec_approval_request(thread_id, "turn-1", "call-1", /*approval_id*/ None);    app.enqueue_primary_thread_session(        test_thread_session(thread_id, test_path_buf("/tmp/project")),        Vec::new(),    )    .await?;    while app_event_rx.try_recv().is_ok() {}    assert_eq!(        app.pending_app_server_requests            .note_server_request(&approval_request),        None    );    app.enqueue_thread_request(thread_id, approval_request)        .await?;    let resolved = app        .pending_app_server_requests        .resolve_notification(&AppServerRequestId::Integer(1))        .expect("matching app-server request should resolve");    app.chat_widget.dismiss_app_server_request(&resolved);    while app_event_rx.try_recv().is_ok() {}    let rx = app        .active_thread_rx        .as_mut()        .expect("primary thread receiver should be active");    let event = time::timeout(Duration::from_millis(50), rx.recv())        .await        .expect("timed out waiting for buffered approval event")        .expect("channel closed unexpectedly");    assert!(matches!(        &event,        ThreadBufferedEvent::Request(ServerRequest::CommandExecutionRequestApproval {            params,            ..        }) if params.turn_id == "turn-1"    ));    app.handle_thread_event_now(event);    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));    while let Ok(app_event) = app_event_rx.try_recv() {        assert!(            !matches!(app_event, AppEvent::SubmitThreadOp { .. }),            "resolved buffered approval should not become actionable"        );    }    Ok(())}#[tokio::test]async fn enqueue_primary_thread_session_replays_turns_before_initial_prompt_submit() -> Result<()> {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let initial_prompt = "follow-up after replay".to_string();    let config = app.config.clone();    let model = get_model_offline_for_tests(config.model.as_deref());    app.chat_widget = ChatWidget::new_with_app_event(ChatWidgetInit {        config,        frame_requester: crate::tui::FrameRequester::test_dummy(),        app_event_tx: app.app_event_tx.clone(),        workspace_command_runner: None,        initial_user_message: create_initial_user_message(            Some(initial_prompt.clone()),            Vec::new(),            Vec::new(),        ),        enhanced_keys_supported: false,        has_chatgpt_account: false,        has_codex_backend_auth: false,        model_catalog: app.model_catalog.clone(),        feedback: codex_feedback::CodexFeedback::new(),        is_first_run: false,        status_account_display: None,        runtime_model_provider_base_url: None,        initial_plan_type: None,        model: Some(model),        startup_tooltip_override: None,        status_line_invalid_items_warned: app.status_line_invalid_items_warned.clone(),        terminal_title_invalid_items_warned: app.terminal_title_invalid_items_warned.clone(),        session_telemetry: app.session_telemetry.clone(),    });    app.enqueue_primary_thread_session(        test_thread_session(thread_id, test_path_buf("/tmp/project")),        vec![test_turn(            "turn-1",            TurnStatus::Completed,            vec![ThreadItem::UserMessage {                id: "user-1".to_string(),                client_id: None,                content: vec![AppServerUserInput::Text {                    text: "earlier prompt".to_string(),                    text_elements: Vec::new(),                }],            }],        )],    )    .await?;    let mut saw_replayed_answer = false;    let mut submitted_items = None;    while let Ok(event) = app_event_rx.try_recv() {        match event {            AppEvent::InsertHistoryCell(cell) => {                let transcript = lines_to_single_string(&cell.transcript_lines(/*width*/ 80));                saw_replayed_answer |= transcript.contains("earlier prompt");            }            AppEvent::SubmitThreadOp {                thread_id: op_thread_id,                op: Op::UserTurn { items, .. },            } => {                assert_eq!(op_thread_id, thread_id);                submitted_items = Some(items);            }            AppEvent::CodexOp(Op::UserTurn { items, .. }) => {                submitted_items = Some(items);            }            _ => {}        }    }    assert!(        saw_replayed_answer,        "expected replayed history before initial prompt submit"    );    assert_eq!(        submitted_items,        Some(vec![UserInput::Text {            text: initial_prompt,            text_elements: Vec::new(),        }])    );    Ok(())}#[tokio::test]async fn reset_thread_event_state_aborts_listener_tasks() {    struct NotifyOnDrop(Option<tokio::sync::oneshot::Sender<()>>);    impl Drop for NotifyOnDrop {        fn drop(&mut self) {            if let Some(tx) = self.0.take() {                let _ = tx.send(());            }        }    }    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    let (started_tx, started_rx) = tokio::sync::oneshot::channel();    let (dropped_tx, dropped_rx) = tokio::sync::oneshot::channel();    let handle = tokio::spawn(async move {        let _notify_on_drop = NotifyOnDrop(Some(dropped_tx));        let _ = started_tx.send(());        std::future::pending::<()>().await;    });    app.thread_event_listener_tasks.insert(thread_id, handle);    started_rx        .await        .expect("listener task should report it started");    app.reset_thread_event_state();    assert_eq!(app.thread_event_listener_tasks.is_empty(), true);    time::timeout(Duration::from_millis(50), dropped_rx)        .await        .expect("timed out waiting for listener task abort")        .expect("listener task drop notification should succeed");}#[tokio::test]async fn history_lookup_response_is_routed_to_requesting_thread() -> Result<()> {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    app.lookup_message_history_entry(thread_id, /*offset*/ 0, /*log_id*/ 1)        .await?;    let app_event = tokio::time::timeout(Duration::from_secs(1), app_event_rx.recv())        .await        .expect("history lookup should emit an app event")        .expect("app event channel should stay open");    let AppEvent::ThreadHistoryEntryResponse {        thread_id: routed_thread_id,        event,    } = app_event    else {        panic!("expected thread-routed history response");    };    assert_eq!(routed_thread_id, thread_id);    assert_eq!(event.offset, 0);    assert_eq!(event.log_id, 1);    assert!(event.entry.is_none());    Ok(())}#[tokio::test]async fn enqueue_thread_event_does_not_block_when_channel_full() -> Result<()> {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    app.thread_event_channels        .insert(thread_id, ThreadEventChannel::new(/*capacity*/ 1));    app.set_thread_active(thread_id, /*active*/ true).await;    let event = thread_closed_notification(thread_id);    app.enqueue_thread_notification(thread_id, event.clone())        .await?;    time::timeout(        Duration::from_millis(50),        app.enqueue_thread_notification(thread_id, event),    )    .await    .expect("enqueue_thread_notification blocked on a full channel")?;    let mut rx = app        .thread_event_channels        .get_mut(&thread_id)        .expect("missing thread channel")        .receiver        .take()        .expect("missing receiver");    time::timeout(Duration::from_millis(50), rx.recv())        .await        .expect("timed out waiting for first event")        .expect("channel closed unexpectedly");    time::timeout(Duration::from_millis(50), rx.recv())        .await        .expect("timed out waiting for second event")        .expect("channel closed unexpectedly");    Ok(())}#[tokio::test]async fn replay_thread_snapshot_restores_draft_and_queued_input() {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.thread_event_channels.insert(        thread_id,        ThreadEventChannel::new_with_session(            THREAD_EVENT_CHANNEL_CAPACITY,            session.clone(),            Vec::new(),        ),    );    app.activate_thread_channel(thread_id).await;    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget        .apply_external_edit("draft prompt".to_string());    app.chat_widget.submit_user_message_with_mode(        "queued follow-up".to_string(),        CollaborationModeMask {            name: "Default".to_string(),            mode: None,            model: None,            reasoning_effort: None,            developer_instructions: None,        },    );    let expected_input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected thread input state");    app.store_active_thread_receiver().await;    let snapshot = {        let channel = app            .thread_event_channels            .get(&thread_id)            .expect("thread channel should exist");        let store = channel.store.lock().await;        assert_eq!(store.input_state, Some(expected_input_state));        store.snapshot()    };    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.replay_thread_snapshot(snapshot, /*resume_restored_queue*/ true);    assert_eq!(app.chat_widget.composer_text_with_pending(), "draft prompt");    assert!(app.chat_widget.queued_user_message_texts().is_empty());    while let Ok(op) = new_op_rx.try_recv() {        assert!(            !matches!(op, Op::UserTurn { .. }),            "draft-only replay should not auto-submit queued input"        );    }}#[tokio::test]async fn active_turn_id_for_thread_uses_snapshot_turns() {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.thread_event_channels.insert(        thread_id,        ThreadEventChannel::new_with_session(            THREAD_EVENT_CHANNEL_CAPACITY,            session,            vec![test_turn("turn-1", TurnStatus::InProgress, Vec::new())],        ),    );    assert_eq!(        app.active_turn_id_for_thread(thread_id).await,        Some("turn-1".to_string())    );}#[tokio::test]async fn replayed_turn_complete_submits_restored_queued_follow_up() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget.handle_server_notification(        turn_started_notification(thread_id, "turn-1"),        /*replay_kind*/ None,    );    app.chat_widget.handle_server_notification(        agent_message_delta_notification(thread_id, "turn-1", "agent-1", "streaming"),        /*replay_kind*/ None,    );    app.chat_widget        .apply_external_edit("queued follow-up".to_string());    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));    let input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected queued follow-up state");    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session.clone());    while new_op_rx.try_recv().is_ok() {}    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: Vec::new(),            events: vec![ThreadBufferedEvent::Notification(                turn_completed_notification(thread_id, "turn-1", TurnStatus::Completed),            )],            input_state: Some(input_state),        },        /*resume_restored_queue*/ true,    );    match next_user_turn_op(&mut new_op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "queued follow-up".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected queued follow-up submission, got {other:?}"),    }}#[tokio::test]async fn replay_only_thread_keeps_restored_queue_visible() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget.handle_server_notification(        turn_started_notification(thread_id, "turn-1"),        /*replay_kind*/ None,    );    app.chat_widget.handle_server_notification(        agent_message_delta_notification(thread_id, "turn-1", "agent-1", "streaming"),        /*replay_kind*/ None,    );    app.chat_widget        .apply_external_edit("queued follow-up".to_string());    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));    let input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected queued follow-up state");    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session.clone());    while new_op_rx.try_recv().is_ok() {}    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: Vec::new(),            events: vec![ThreadBufferedEvent::Notification(                turn_completed_notification(thread_id, "turn-1", TurnStatus::Completed),            )],            input_state: Some(input_state),        },        /*resume_restored_queue*/ false,    );    assert_eq!(        app.chat_widget.queued_user_message_texts(),        vec!["queued follow-up".to_string()]    );    assert!(        new_op_rx.try_recv().is_err(),        "replay-only threads should not auto-submit restored queue"    );}#[tokio::test]async fn replay_thread_snapshot_keeps_queue_when_running_state_only_comes_from_snapshot() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget.handle_server_notification(        turn_started_notification(thread_id, "turn-1"),        /*replay_kind*/ None,    );    app.chat_widget.handle_server_notification(        agent_message_delta_notification(thread_id, "turn-1", "agent-1", "streaming"),        /*replay_kind*/ None,    );    app.chat_widget        .apply_external_edit("queued follow-up".to_string());    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));    let input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected queued follow-up state");    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session.clone());    while new_op_rx.try_recv().is_ok() {}    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: Vec::new(),            events: vec![],            input_state: Some(input_state),        },        /*resume_restored_queue*/ true,    );    assert_eq!(        app.chat_widget.queued_user_message_texts(),        vec!["queued follow-up".to_string()]    );    assert!(        new_op_rx.try_recv().is_err(),        "restored queue should stay queued when replay did not prove the turn finished"    );}#[tokio::test]async fn replay_thread_snapshot_in_progress_turn_restores_running_queue_state() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget.handle_server_notification(        turn_started_notification(thread_id, "turn-1"),        /*replay_kind*/ None,    );    app.chat_widget.handle_server_notification(        agent_message_delta_notification(thread_id, "turn-1", "agent-1", "streaming"),        /*replay_kind*/ None,    );    app.chat_widget        .apply_external_edit("queued follow-up".to_string());    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));    let input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected queued follow-up state");    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session.clone());    while new_op_rx.try_recv().is_ok() {}    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: vec![test_turn("turn-1", TurnStatus::InProgress, Vec::new())],            events: Vec::new(),            input_state: Some(input_state),        },        /*resume_restored_queue*/ true,    );    assert_eq!(        app.chat_widget.queued_user_message_texts(),        vec!["queued follow-up".to_string()]    );    assert!(        new_op_rx.try_recv().is_err(),        "restored queue should stay queued while replayed turn is still running"    );}#[tokio::test]async fn replay_thread_snapshot_in_progress_turn_restores_running_state_without_input_state() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    let (chat_widget, _app_event_tx, _rx, _new_op_rx) = make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session);    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: vec![test_turn("turn-1", TurnStatus::InProgress, Vec::new())],            events: Vec::new(),            input_state: None,        },        /*resume_restored_queue*/ false,    );    assert!(app.chat_widget.is_task_running_for_test());}#[tokio::test]async fn replay_thread_snapshot_does_not_submit_queue_before_replay_catches_up() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget.handle_server_notification(        turn_started_notification(thread_id, "turn-1"),        /*replay_kind*/ None,    );    app.chat_widget.handle_server_notification(        agent_message_delta_notification(thread_id, "turn-1", "agent-1", "streaming"),        /*replay_kind*/ None,    );    app.chat_widget        .apply_external_edit("queued follow-up".to_string());    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));    let input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected queued follow-up state");    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session.clone());    while new_op_rx.try_recv().is_ok() {}    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: Vec::new(),            events: vec![                ThreadBufferedEvent::Notification(turn_completed_notification(                    thread_id,                    "turn-0",                    TurnStatus::Completed,                )),                ThreadBufferedEvent::Notification(turn_started_notification(thread_id, "turn-1")),            ],            input_state: Some(input_state),        },        /*resume_restored_queue*/ true,    );    assert!(        new_op_rx.try_recv().is_err(),        "queued follow-up should stay queued until the latest turn completes"    );    assert_eq!(        app.chat_widget.queued_user_message_texts(),        vec!["queued follow-up".to_string()]    );    app.chat_widget.handle_server_notification(        turn_completed_notification(thread_id, "turn-1", TurnStatus::Completed),        /*replay_kind*/ None,    );    match next_user_turn_op(&mut new_op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: "queued follow-up".to_string(),                text_elements: Vec::new(),            }]        ),        other => panic!("expected queued follow-up submission, got {other:?}"),    }}#[tokio::test]async fn replay_thread_snapshot_restores_pending_pastes_for_submit() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.thread_event_channels.insert(        thread_id,        ThreadEventChannel::new_with_session(            THREAD_EVENT_CHANNEL_CAPACITY,            session.clone(),            Vec::new(),        ),    );    app.activate_thread_channel(thread_id).await;    app.chat_widget.handle_thread_session(session);    let large = "x".repeat(1005);    app.chat_widget.handle_paste(large.clone());    let expected_input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected thread input state");    app.store_active_thread_receiver().await;    let snapshot = {        let channel = app            .thread_event_channels            .get(&thread_id)            .expect("thread channel should exist");        let store = channel.store.lock().await;        assert_eq!(store.input_state, Some(expected_input_state));        store.snapshot()    };    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.replay_thread_snapshot(snapshot, /*resume_restored_queue*/ true);    assert_eq!(app.chat_widget.composer_text_with_pending(), large);    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    match next_user_turn_op(&mut new_op_rx) {        Op::UserTurn { items, .. } => assert_eq!(            items,            vec![UserInput::Text {                text: large,                text_elements: Vec::new(),            }]        ),        other => panic!("expected restored paste submission, got {other:?}"),    }}#[tokio::test]async fn replay_thread_snapshot_restores_collaboration_mode_for_draft_submit() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget        .set_reasoning_effort(Some(ReasoningEffortConfig::High));    app.chat_widget        .set_collaboration_mask(CollaborationModeMask {            name: "Plan".to_string(),            mode: Some(ModeKind::Plan),            model: Some("gpt-restored".to_string()),            reasoning_effort: Some(Some(ReasoningEffortConfig::High)),            developer_instructions: None,        });    app.chat_widget        .apply_external_edit("draft prompt".to_string());    let input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected draft input state");    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget        .set_reasoning_effort(Some(ReasoningEffortConfig::Low));    app.chat_widget        .set_collaboration_mask(CollaborationModeMask {            name: "Default".to_string(),            mode: Some(ModeKind::Default),            model: Some("gpt-replacement".to_string()),            reasoning_effort: Some(Some(ReasoningEffortConfig::Low)),            developer_instructions: None,        });    while new_op_rx.try_recv().is_ok() {}    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: Vec::new(),            events: vec![],            input_state: Some(input_state),        },        /*resume_restored_queue*/ true,    );    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    match next_user_turn_op(&mut new_op_rx) {        Op::UserTurn {            items,            model,            effort,            collaboration_mode,            ..        } => {            assert_eq!(                items,                vec![UserInput::Text {                    text: "draft prompt".to_string(),                    text_elements: Vec::new(),                }]            );            assert_eq!(model, "gpt-restored".to_string());            assert_eq!(effort, Some(ReasoningEffortConfig::High));            assert_eq!(                collaboration_mode,                Some(CollaborationMode {                    mode: ModeKind::Plan,                    settings: Settings {                        model: "gpt-restored".to_string(),                        reasoning_effort: Some(ReasoningEffortConfig::High),                        developer_instructions: None,                    },                })            );        }        other => panic!("expected restored draft submission, got {other:?}"),    }}#[tokio::test]async fn replay_thread_snapshot_restores_collaboration_mode_without_input() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget        .set_reasoning_effort(Some(ReasoningEffortConfig::High));    app.chat_widget        .set_collaboration_mask(CollaborationModeMask {            name: "Plan".to_string(),            mode: Some(ModeKind::Plan),            model: Some("gpt-restored".to_string()),            reasoning_effort: Some(Some(ReasoningEffortConfig::High)),            developer_instructions: None,        });    let input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected collaboration-only input state");    let (chat_widget, _app_event_tx, _rx, _new_op_rx) = make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget        .set_reasoning_effort(Some(ReasoningEffortConfig::Low));    app.chat_widget        .set_collaboration_mask(CollaborationModeMask {            name: "Default".to_string(),            mode: Some(ModeKind::Default),            model: Some("gpt-replacement".to_string()),            reasoning_effort: Some(Some(ReasoningEffortConfig::Low)),            developer_instructions: None,        });    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: Vec::new(),            events: vec![],            input_state: Some(input_state),        },        /*resume_restored_queue*/ true,    );    assert_eq!(        app.chat_widget.active_collaboration_mode_kind(),        ModeKind::Plan    );    assert_eq!(app.chat_widget.current_model(), "gpt-restored");    assert_eq!(        app.chat_widget.current_reasoning_effort(),        Some(ReasoningEffortConfig::High)    );}#[tokio::test]async fn replayed_interrupted_turn_restores_queued_input_to_composer() {    let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));    app.chat_widget.handle_thread_session(session.clone());    app.chat_widget.handle_server_notification(        turn_started_notification(thread_id, "turn-1"),        /*replay_kind*/ None,    );    app.chat_widget.handle_server_notification(        agent_message_delta_notification(thread_id, "turn-1", "agent-1", "streaming"),        /*replay_kind*/ None,    );    app.chat_widget        .apply_external_edit("queued follow-up".to_string());    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    let input_state = app        .chat_widget        .capture_thread_input_state()        .expect("expected queued follow-up state");    let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =        make_chatwidget_manual_with_sender().await;    app.chat_widget = chat_widget;    app.chat_widget.handle_thread_session(session.clone());    while new_op_rx.try_recv().is_ok() {}    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: Vec::new(),            events: vec![ThreadBufferedEvent::Notification(                turn_completed_notification(thread_id, "turn-1", TurnStatus::Interrupted),            )],            input_state: Some(input_state),        },        /*resume_restored_queue*/ true,    );    assert_eq!(        app.chat_widget.composer_text_with_pending(),        "queued follow-up"    );    assert!(app.chat_widget.queued_user_message_texts().is_empty());    assert!(        new_op_rx.try_recv().is_err(),        "replayed interrupted turns should restore queued input for editing, not submit it"    );}#[tokio::test]async fn token_usage_update_refreshes_status_line_with_runtime_context_window() {    let mut app = make_test_app().await;    app.chat_widget.setup_status_line(        vec![crate::bottom_pane::StatusLineItem::ContextWindowSize],        /*use_theme_colors*/ true,    );    assert_eq!(app.chat_widget.status_line_text(), None);    app.handle_thread_event_now(ThreadBufferedEvent::Notification(token_usage_notification(        ThreadId::new(),        "turn-1",        Some(950_000),    )));    assert_eq!(        app.chat_widget.status_line_text(),        Some("950K window".into())    );}#[tokio::test]async fn collab_receiver_notification_caches_thread_without_app_server_read() {    let mut app = make_test_app().await;    let receiver_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id");    app.handle_thread_event_now(ThreadBufferedEvent::Notification(        ServerNotification::ItemStarted(ItemStartedNotification {            thread_id: ThreadId::new().to_string(),            turn_id: "turn-1".to_string(),            started_at_ms: 0,            item: ThreadItem::CollabAgentToolCall {                id: "wait-1".to_string(),                tool: codex_app_server_protocol::CollabAgentTool::Wait,                status: codex_app_server_protocol::CollabAgentToolCallStatus::InProgress,                sender_thread_id: ThreadId::new().to_string(),                receiver_thread_ids: vec![receiver_thread_id.to_string()],                prompt: None,                model: None,                reasoning_effort: None,                agents_states: HashMap::new(),            },        }),    ));    assert_eq!(        app.agent_navigation.get(&receiver_thread_id),        Some(&AgentPickerThreadEntry {            agent_nickname: None,            agent_role: None,            agent_path: None,            is_running: false,            is_closed: false,        })    );}#[tokio::test]async fn collab_receiver_notification_does_not_cache_not_found_thread() {    let mut app = make_test_app().await;    let receiver_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000124").expect("valid thread id");    app.handle_thread_event_now(ThreadBufferedEvent::Notification(        ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification {            thread_id: ThreadId::new().to_string(),            turn_id: "turn-1".to_string(),            completed_at_ms: 0,            item: ThreadItem::CollabAgentToolCall {                id: "send-1".to_string(),                tool: codex_app_server_protocol::CollabAgentTool::SendInput,                status: codex_app_server_protocol::CollabAgentToolCallStatus::Failed,                sender_thread_id: ThreadId::new().to_string(),                receiver_thread_ids: vec![receiver_thread_id.to_string()],                prompt: Some("hello".to_string()),                model: None,                reasoning_effort: None,                agents_states: HashMap::from([(                    receiver_thread_id.to_string(),                    codex_app_server_protocol::CollabAgentState {                        status: codex_app_server_protocol::CollabAgentStatus::NotFound,                        message: None,                    },                )]),            },        }),    ));    assert_eq!(app.agent_navigation.get(&receiver_thread_id), None);}#[tokio::test]async fn open_agent_picker_keeps_missing_threads_for_replay() -> Result<()> {    let mut app = Box::pin(make_test_app()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let thread_id = ThreadId::new();    app.thread_event_channels        .insert(thread_id, ThreadEventChannel::new(/*capacity*/ 1));    Box::pin(app.open_agent_picker(&mut app_server)).await;    assert_eq!(app.thread_event_channels.contains_key(&thread_id), true);    assert_eq!(        app.agent_navigation.get(&thread_id),        Some(&AgentPickerThreadEntry {            agent_nickname: None,            agent_role: None,            agent_path: None,            is_running: false,            is_closed: true,        })    );    assert_eq!(app.agent_navigation.ordered_thread_ids(), vec![thread_id]);    Ok(())}#[tokio::test]async fn open_agent_picker_preserves_cached_metadata_for_replay_threads() -> Result<()> {    let mut app = Box::pin(make_test_app()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let thread_id = ThreadId::new();    app.thread_event_channels        .insert(thread_id, ThreadEventChannel::new(/*capacity*/ 1));    app.agent_navigation.upsert(        thread_id,        Some("Robie".to_string()),        Some("explorer".to_string()),        /*is_closed*/ true,    );    Box::pin(app.open_agent_picker(&mut app_server)).await;    assert_eq!(app.thread_event_channels.contains_key(&thread_id), true);    assert_eq!(        app.agent_navigation.get(&thread_id),        Some(&AgentPickerThreadEntry {            agent_nickname: Some("Robie".to_string()),            agent_role: Some("explorer".to_string()),            agent_path: None,            is_running: false,            is_closed: true,        })    );    Ok(())}#[tokio::test]async fn open_agent_picker_clears_completed_path_backed_agent_running_state() -> Result<()> {    let mut app = Box::pin(make_test_app()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let thread_id = ThreadId::new();    let channel = ThreadEventChannel::new(/*capacity*/ 4);    {        let mut store = channel.store.lock().await;        store.push_notification(turn_started_notification(thread_id, "turn-1"));        store.push_notification(turn_completed_notification(            thread_id,            "turn-1",            TurnStatus::Completed,        ));    }    app.thread_event_channels.insert(thread_id, channel);    app.agent_navigation        .record_sub_agent_activity(SubAgentActivityDisplay {            thread_id,            agent_path: "/root/child".to_string(),            is_running_hint: true,        });    Box::pin(app.open_agent_picker(&mut app_server)).await;    assert_eq!(        app.agent_navigation.get(&thread_id),        Some(&AgentPickerThreadEntry {            agent_nickname: None,            agent_role: None,            agent_path: Some("/root/child".to_string()),            is_running: false,            is_closed: false,        })    );    Ok(())}#[tokio::test]async fn open_agent_picker_refreshes_replay_only_path_backed_liveness() -> Result<()> {    let mut app = Box::pin(make_test_app()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let thread_id = ThreadId::new();    let mut channel = ThreadEventChannel::new(/*capacity*/ 4);    channel.mark_replay_only();    {        let mut store = channel.store.lock().await;        store.push_notification(turn_started_notification(thread_id, "turn-1"));    }    app.thread_event_channels.insert(thread_id, channel);    app.agent_navigation        .record_sub_agent_activity(SubAgentActivityDisplay {            thread_id,            agent_path: "/root/child".to_string(),            is_running_hint: true,        });    Box::pin(app.open_agent_picker(&mut app_server)).await;    assert_eq!(        app.agent_navigation.get(&thread_id),        Some(&AgentPickerThreadEntry {            agent_nickname: None,            agent_role: None,            agent_path: Some("/root/child".to_string()),            is_running: false,            is_closed: true,        })    );    Ok(())}#[tokio::test]async fn open_agent_picker_prunes_terminal_metadata_only_threads() -> Result<()> {    let mut app = Box::pin(make_test_app()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let thread_id = ThreadId::new();    app.agent_navigation.upsert(        thread_id,        Some("Ghost".to_string()),        Some("worker".to_string()),        /*is_closed*/ false,    );    Box::pin(app.open_agent_picker(&mut app_server)).await;    assert_eq!(app.agent_navigation.get(&thread_id), None);    assert!(app.agent_navigation.is_empty());    Ok(())}#[tokio::test]async fn open_agent_picker_marks_terminal_read_errors_closed() -> Result<()> {    let mut app = Box::pin(make_test_app()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let thread_id = ThreadId::new();    app.thread_event_channels        .insert(thread_id, ThreadEventChannel::new(/*capacity*/ 1));    app.agent_navigation.upsert(        thread_id,        Some("Robie".to_string()),        Some("explorer".to_string()),        /*is_closed*/ false,    );    Box::pin(app.open_agent_picker(&mut app_server)).await;    assert_eq!(        app.agent_navigation.get(&thread_id),        Some(&AgentPickerThreadEntry {            agent_nickname: Some("Robie".to_string()),            agent_role: Some("explorer".to_string()),            agent_path: None,            is_running: false,            is_closed: true,        })    );    Ok(())}#[test]fn open_agent_picker_marks_loaded_threads_open() -> Result<()> {    const WORKER_THREADS: usize = 1;    const TEST_STACK_SIZE_BYTES: usize = 8 * 1024 * 1024;    let runtime = tokio::runtime::Builder::new_multi_thread()        .worker_threads(WORKER_THREADS)        .thread_stack_size(TEST_STACK_SIZE_BYTES)        .enable_all()        .build()?;    runtime.block_on(async {        let mut app = Box::pin(make_test_app()).await;        let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(            app.chat_widget.config_ref(),        ))        .await        .expect("embedded app server");        let started = app_server            .start_thread(app.chat_widget.config_ref())            .await?;        let thread_id = started.session.thread_id;        app.thread_event_channels            .insert(thread_id, ThreadEventChannel::new(/*capacity*/ 1));        Box::pin(app.open_agent_picker(&mut app_server)).await;        assert_eq!(            app.agent_navigation.get(&thread_id),            Some(&AgentPickerThreadEntry {                agent_nickname: None,                agent_role: None,                agent_path: None,                is_running: false,                is_closed: false,            })        );        Ok(())    })}#[test]fn attach_live_thread_for_selection_rejects_empty_non_ephemeral_fallback_threads() -> Result<()> {    const WORKER_THREADS: usize = 1;    const TEST_STACK_SIZE_BYTES: usize = 8 * 1024 * 1024;    let runtime = tokio::runtime::Builder::new_multi_thread()        .worker_threads(WORKER_THREADS)        .thread_stack_size(TEST_STACK_SIZE_BYTES)        .enable_all()        .build()?;    runtime.block_on(async {        let config = {            let app = make_test_app().await;            app.chat_widget.config_ref().clone()        };        let mut app_server = crate::start_embedded_app_server_for_picker(&config)            .await            .expect("embedded app server");        let started = app_server.start_thread(&config).await?;        let thread_id = started.session.thread_id;        let mut app = make_test_app().await;        app.agent_navigation.upsert(            thread_id,            Some("Scout".to_string()),            Some("worker".to_string()),            /*is_closed*/ false,        );        let err = app            .attach_live_thread_for_selection(&mut app_server, thread_id)            .await            .expect_err("empty fallback should not attach as a blank replay-only thread");        assert_eq!(            err.to_string(),            format!("Agent thread {thread_id} is not yet available for replay or live attach.")        );        assert!(!app.thread_event_channels.contains_key(&thread_id));        Ok(())    })}#[test]fn attach_live_thread_for_selection_rejects_unmaterialized_fallback_threads() -> Result<()> {    const WORKER_THREADS: usize = 1;    const TEST_STACK_SIZE_BYTES: usize = 8 * 1024 * 1024;    let runtime = tokio::runtime::Builder::new_multi_thread()        .worker_threads(WORKER_THREADS)        .thread_stack_size(TEST_STACK_SIZE_BYTES)        .enable_all()        .build()?;    runtime.block_on(async {        let mut app = make_test_app().await;        let mut app_server =            crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref()).await?;        let mut ephemeral_config = app.chat_widget.config_ref().clone();        ephemeral_config.ephemeral = true;        let started = app_server.start_thread(&ephemeral_config).await?;        let thread_id = started.session.thread_id;        app.agent_navigation.upsert(            thread_id,            Some("Scout".to_string()),            Some("worker".to_string()),            /*is_closed*/ false,        );        let err = app            .attach_live_thread_for_selection(&mut app_server, thread_id)            .await            .expect_err("ephemeral fallback should not attach as a blank live thread");        assert_eq!(            err.to_string(),            format!("Agent thread {thread_id} is not yet available for replay or live attach.")        );        assert!(!app.thread_event_channels.contains_key(&thread_id));        Ok(())    })}#[tokio::test]async fn should_attach_live_thread_for_selection_skips_closed_metadata_only_threads() {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    app.agent_navigation.upsert(        thread_id,        Some("Ghost".to_string()),        Some("worker".to_string()),        /*is_closed*/ true,    );    assert!(!app.should_attach_live_thread_for_selection(thread_id));    app.agent_navigation.upsert(        thread_id,        Some("Ghost".to_string()),        Some("worker".to_string()),        /*is_closed*/ false,    );    assert!(app.should_attach_live_thread_for_selection(thread_id));    app.thread_event_channels        .insert(thread_id, ThreadEventChannel::new(/*capacity*/ 1));    assert!(!app.should_attach_live_thread_for_selection(thread_id));}#[tokio::test]async fn refresh_agent_picker_thread_liveness_prunes_closed_metadata_only_threads() -> Result<()> {    let mut app = Box::pin(make_test_app()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let thread_id = ThreadId::new();    app.agent_navigation.upsert(        thread_id,        Some("Ghost".to_string()),        Some("worker".to_string()),        /*is_closed*/ false,    );    let is_available =        Box::pin(app.refresh_agent_picker_thread_liveness(&mut app_server, thread_id)).await;    assert!(!is_available);    assert_eq!(app.agent_navigation.get(&thread_id), None);    assert!(!app.thread_event_channels.contains_key(&thread_id));    Ok(())}#[tokio::test]async fn open_agent_picker_prompts_to_enable_multi_agent_when_disabled() -> Result<()> {    let (mut app, mut app_event_rx, _op_rx) = Box::pin(make_test_app_with_channels()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let _ = app.config.features.disable(Feature::Collab);    Box::pin(app.open_agent_picker(&mut app_server)).await;    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert_matches!(        app_event_rx.try_recv(),        Ok(AppEvent::UpdateFeatureFlags { updates }) if updates == vec![(Feature::Collab, true)]    );    let cell = match app_event_rx.try_recv() {        Ok(AppEvent::InsertHistoryCell(cell)) => cell,        other => panic!("expected InsertHistoryCell event, got {other:?}"),    };    let rendered = cell        .display_lines(/*width*/ 120)        .into_iter()        .map(|line| line.to_string())        .collect::<Vec<_>>()        .join("\n");    assert!(rendered.contains("Subagents will be enabled in the next session."));    Ok(())}#[tokio::test]async fn update_memory_settings_persists_and_updates_widget_config() -> Result<()> {    let (mut app, _app_event_rx, _op_rx) = Box::pin(make_test_app_with_channels()).await;    let codex_home = tempdir()?;    app.config.codex_home = codex_home.path().to_path_buf().abs();    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;    Box::pin(app.update_memory_settings_with_app_server(        &mut app_server,        /*use_memories*/ false,        /*generate_memories*/ false,    ))    .await;    assert!(!app.config.memories.use_memories);    assert!(!app.config.memories.generate_memories);    assert!(!app.chat_widget.config_ref().memories.use_memories);    assert!(!app.chat_widget.config_ref().memories.generate_memories);    let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;    let config_value = toml::from_str::<TomlValue>(&config)?;    let memories = config_value        .as_table()        .and_then(|table| table.get("memories"))        .and_then(TomlValue::as_table)        .expect("memories table should exist");    assert_eq!(        memories.get("use_memories"),        Some(&TomlValue::Boolean(false))    );    assert_eq!(        memories.get("generate_memories"),        Some(&TomlValue::Boolean(false))    );    assert!(        !memories.contains_key("disable_on_external_context")            && !memories.contains_key("no_memories_if_mcp_or_web_search"),        "the TUI menu should not write the external-context memory setting"    );    app_server.shutdown().await?;    Ok(())}#[test]fn update_memory_settings_updates_current_thread_memory_mode() -> Result<()> {    const WORKER_THREADS: usize = 1;    const TEST_STACK_SIZE_BYTES: usize = 8 * 1024 * 1024;    let runtime = tokio::runtime::Builder::new_multi_thread()        .worker_threads(WORKER_THREADS)        .thread_stack_size(TEST_STACK_SIZE_BYTES)        .enable_all()        .build()?;    runtime.block_on(async {        let (mut app, _app_event_rx, _op_rx) = Box::pin(make_test_app_with_channels()).await;        let codex_home = tempdir()?;        app.config.codex_home = codex_home.path().to_path_buf().abs();        app.config.sqlite_home = codex_home.path().to_path_buf();        // Seed the previous setting so this test exercises the thread-mode update path.        app.config.memories.generate_memories = true;        let mut app_server =            Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;        let started = app_server.start_thread(&app.config).await?;        let thread_id = started.session.thread_id;        app.active_thread_id = Some(thread_id);        Box::pin(app.update_memory_settings_with_app_server(            &mut app_server,            /*use_memories*/ true,            /*generate_memories*/ false,        ))        .await;        let state_db = codex_state::StateRuntime::init(            codex_home.path().to_path_buf(),            app.config.model_provider_id.clone(),        )        .await        .expect("state db should initialize");        let memory_mode = state_db            .get_thread_memory_mode(thread_id)            .await            .expect("thread memory mode should be readable");        assert_eq!(memory_mode.as_deref(), Some("disabled"));        app_server.shutdown().await?;        Ok(())    })}#[tokio::test]async fn reset_memories_clears_local_memory_directories() -> Result<()> {    Box::pin(async {        let (mut app, _app_event_rx, _op_rx) = Box::pin(make_test_app_with_channels()).await;        let codex_home = tempdir()?;        app.config.codex_home = codex_home.path().to_path_buf().abs();        app.config.sqlite_home = codex_home.path().to_path_buf();        let memory_root = codex_home.path().join("memories");        let extensions_root = memory_root.join("extensions");        std::fs::create_dir_all(memory_root.join("rollout_summaries"))?;        std::fs::create_dir_all(&extensions_root)?;        std::fs::write(memory_root.join("MEMORY.md"), "stale memory\n")?;        std::fs::write(            memory_root.join("rollout_summaries").join("stale.md"),            "stale summary\n",        )?;        std::fs::write(extensions_root.join("stale.txt"), "stale extension\n")?;        let mut app_server =            Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;        Box::pin(app.reset_memories_with_app_server(&mut app_server)).await;        assert_eq!(std::fs::read_dir(&memory_root)?.count(), 0);        app_server.shutdown().await?;        Ok(())    })    .await}#[tokio::test]async fn apply_permission_profile_selection_preserves_loader_overrides() -> Result<()> {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let codex_home = tempdir()?;    let selected_config = codex_home.path().join("work.config.toml");    std::fs::write(        &selected_config,        r#"default_permissions = "locked-down"[permissions.locked-down.filesystem]":minimal" = "read""#,    )?;    app.config.codex_home = codex_home.path().to_path_buf().abs();    app.loader_overrides.user_config_path = Some(selected_config.abs());    app.harness_overrides.sandbox_mode = Some(SandboxMode::WorkspaceWrite);    app.harness_overrides.permission_profile = Some(PermissionProfile::workspace_write());    assert!(        app.apply_permission_profile_selection(PermissionProfileSelection {            profile_id: "locked-down".to_string(),            approval_policy: None,            approvals_reviewer: None,            display_label: "locked-down".to_string(),        })        .await    );    assert_eq!(        app.config            .permissions            .active_permission_profile()            .as_ref()            .map(|profile| profile.id.as_str()),        Some("locked-down")    );    assert_eq!(        app.chat_widget            .config_ref()            .permissions            .active_permission_profile()            .as_ref()            .map(|profile| profile.id.as_str()),        Some("locked-down")    );    assert_eq!(        app.runtime_permission_profile_override,        Some(RuntimePermissionProfileOverride::from_config(&app.config))    );    let op = match app_event_rx.try_recv() {        Ok(AppEvent::CodexOp(op)) => op,        other => panic!("expected CodexOp event, got {other:?}"),    };    assert_eq!(        op,        Op::OverrideTurnContext {            cwd: None,            approval_policy: None,            approvals_reviewer: None,            permission_profile: Some(app.config.permissions.permission_profile().clone()),            active_permission_profile: app.config.permissions.active_permission_profile(),            windows_sandbox_level: None,            model: None,            effort: None,            summary: None,            service_tier: None,            collaboration_mode: None,            personality: None,        }    );    let cell = match app_event_rx.try_recv() {        Ok(AppEvent::InsertHistoryCell(cell)) => cell,        other => panic!("expected InsertHistoryCell event, got {other:?}"),    };    let rendered = cell        .display_lines(/*width*/ 120)        .into_iter()        .map(|line| line.to_string())        .collect::<Vec<_>>()        .join("\n");    assert!(rendered.contains("Permissions updated to locked-down"));    Ok(())}#[tokio::test]async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<()> {    let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;    let codex_home = tempdir()?;    app.config.codex_home = codex_home.path().to_path_buf().abs();    let auto_review = auto_review_mode();    let mut app_server = start_config_write_test_app_server(&app).await?;    app.update_feature_flags(&mut app_server, vec![(Feature::GuardianApproval, true)])        .await;    assert!(app.config.features.enabled(Feature::GuardianApproval));    assert!(        app.chat_widget            .config_ref()            .features            .enabled(Feature::GuardianApproval)    );    assert_eq!(        app.config.approvals_reviewer,        auto_review.approvals_reviewer    );    assert_eq!(        AskForApproval::from(app.config.permissions.approval_policy.value()),        auto_review.approval_policy    );    assert_eq!(        AskForApproval::from(            app.chat_widget                .config_ref()                .permissions                .approval_policy                .value(),        ),        auto_review.approval_policy    );    assert_eq!(        app.chat_widget            .config_ref()            .permissions            .permission_profile(),        &auto_review.permission_profile()    );    assert_eq!(        app.config.permissions.active_permission_profile(),        Some(auto_review.active_permission_profile.clone())    );    assert_eq!(        app.chat_widget            .config_ref()            .permissions            .active_permission_profile(),        Some(auto_review.active_permission_profile.clone())    );    assert_eq!(        app.chat_widget.config_ref().approvals_reviewer,        auto_review.approvals_reviewer    );    assert_eq!(app.runtime_approval_policy_override, None);    assert_eq!(        app.runtime_permission_profile_override,        Some(RuntimePermissionProfileOverride::from_config(&app.config))    );    assert_eq!(        op_rx.try_recv(),        Ok(Op::OverrideTurnContext {            cwd: None,            approval_policy: Some(auto_review.approval_policy),            approvals_reviewer: Some(auto_review.approvals_reviewer),            permission_profile: Some(auto_review.permission_profile()),            active_permission_profile: Some(auto_review.active_permission_profile.clone()),            windows_sandbox_level: None,            model: None,            effort: None,            summary: None,            service_tier: None,            collaboration_mode: None,            personality: None,        })    );    let cell = match app_event_rx.try_recv() {        Ok(AppEvent::InsertHistoryCell(cell)) => cell,        other => panic!("expected InsertHistoryCell event, got {other:?}"),    };    let rendered = cell        .display_lines(/*width*/ 120)        .into_iter()        .map(|line| line.to_string())        .collect::<Vec<_>>()        .join("\n");    assert!(rendered.contains("Permissions updated to Approve for me"));    let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;    assert!(config.contains("guardian_approval = true"));    assert!(config.contains("approvals_reviewer = \"auto_review\""));    assert!(config.contains("approval_policy = \"on-request\""));    assert!(config.contains("sandbox_mode = \"workspace-write\""));    app_server.shutdown().await?;    Ok(())}#[tokio::test]async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default()-> Result<()> {    let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;    let codex_home = tempdir()?;    app.config.codex_home = codex_home.path().to_path_buf().abs();    let config_toml_path = codex_home.path().join("config.toml").abs();    let config_toml = "approvals_reviewer = \"guardian_subagent\"\napproval_policy = \"on-request\"\nsandbox_mode = \"workspace-write\"\n\n[features]\nguardian_approval = true\n";    std::fs::write(config_toml_path.as_path(), config_toml)?;    let user_config = toml::from_str::<TomlValue>(config_toml)?;    app.config.config_layer_stack = app        .config        .config_layer_stack        .with_user_config(&config_toml_path, user_config);    app.config        .features        .set_enabled(Feature::GuardianApproval, /*enabled*/ true)?;    app.chat_widget        .set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true);    app.config.approvals_reviewer = ApprovalsReviewer::AutoReview;    app.chat_widget        .set_approvals_reviewer(ApprovalsReviewer::AutoReview);    app.config        .permissions        .approval_policy        .set(AskForApproval::OnRequest.to_core())?;    app.config        .permissions        .set_permission_profile(PermissionProfile::workspace_write())?;    app.chat_widget        .set_approval_policy(AskForApproval::OnRequest);    app.chat_widget        .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::legacy(            PermissionProfile::workspace_write(),        ))?;    let mut app_server = start_config_write_test_app_server(&app).await?;    app.update_feature_flags(&mut app_server, vec![(Feature::GuardianApproval, false)])        .await;    assert!(!app.config.features.enabled(Feature::GuardianApproval));    assert!(        !app.chat_widget            .config_ref()            .features            .enabled(Feature::GuardianApproval)    );    assert_eq!(app.config.approvals_reviewer, ApprovalsReviewer::User);    assert_eq!(        AskForApproval::from(app.config.permissions.approval_policy.value()),        AskForApproval::OnRequest    );    assert_eq!(        app.chat_widget.config_ref().approvals_reviewer,        ApprovalsReviewer::User    );    assert_eq!(app.runtime_approval_policy_override, None);    assert_eq!(        op_rx.try_recv(),        Ok(Op::OverrideTurnContext {            cwd: None,            approval_policy: None,            approvals_reviewer: Some(ApprovalsReviewer::User),            permission_profile: None,            active_permission_profile: None,            windows_sandbox_level: None,            model: None,            effort: None,            summary: None,            service_tier: None,            collaboration_mode: None,            personality: None,        })    );    let cell = match app_event_rx.try_recv() {        Ok(AppEvent::InsertHistoryCell(cell)) => cell,        other => panic!("expected InsertHistoryCell event, got {other:?}"),    };    let rendered = cell        .display_lines(/*width*/ 120)        .into_iter()        .map(|line| line.to_string())        .collect::<Vec<_>>()        .join("\n");    assert!(rendered.contains("Permissions updated to Ask for approval"));    let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;    assert!(!config.contains("guardian_approval = true"));    assert!(!config.contains("approvals_reviewer ="));    assert!(config.contains("approval_policy = \"on-request\""));    assert!(config.contains("sandbox_mode = \"workspace-write\""));    app_server.shutdown().await?;    Ok(())}#[tokio::test]async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review_policy()-> Result<()> {    let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;    let codex_home = tempdir()?;    app.config.codex_home = codex_home.path().to_path_buf().abs();    let auto_review = auto_review_mode();    let config_toml_path = codex_home.path().join("config.toml").abs();    let config_toml = "approvals_reviewer = \"user\"\n";    std::fs::write(config_toml_path.as_path(), config_toml)?;    let user_config = toml::from_str::<TomlValue>(config_toml)?;    app.config.config_layer_stack = app        .config        .config_layer_stack        .with_user_config(&config_toml_path, user_config);    app.config.approvals_reviewer = ApprovalsReviewer::User;    app.chat_widget        .set_approvals_reviewer(ApprovalsReviewer::User);    let mut app_server = start_config_write_test_app_server(&app).await?;    app.update_feature_flags(&mut app_server, vec![(Feature::GuardianApproval, true)])        .await;    assert!(app.config.features.enabled(Feature::GuardianApproval));    assert_eq!(        app.config.approvals_reviewer,        auto_review.approvals_reviewer    );    assert_eq!(        app.chat_widget.config_ref().approvals_reviewer,        auto_review.approvals_reviewer    );    assert_eq!(        AskForApproval::from(app.config.permissions.approval_policy.value()),        auto_review.approval_policy    );    assert_eq!(        app.chat_widget            .config_ref()            .permissions            .permission_profile(),        &auto_review.permission_profile()    );    assert_eq!(        op_rx.try_recv(),        Ok(Op::OverrideTurnContext {            cwd: None,            approval_policy: Some(auto_review.approval_policy),            approvals_reviewer: Some(auto_review.approvals_reviewer),            permission_profile: Some(auto_review.permission_profile()),            active_permission_profile: Some(auto_review.active_permission_profile.clone()),            windows_sandbox_level: None,            model: None,            effort: None,            summary: None,            service_tier: None,            collaboration_mode: None,            personality: None,        })    );    let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;    assert!(config.contains("approvals_reviewer = \"auto_review\""));    assert!(config.contains("guardian_approval = true"));    assert!(config.contains("approval_policy = \"on-request\""));    assert!(config.contains("sandbox_mode = \"workspace-write\""));    app_server.shutdown().await?;    Ok(())}#[tokio::test]async fn update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history()-> Result<()> {    let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;    let codex_home = tempdir()?;    app.config.codex_home = codex_home.path().to_path_buf().abs();    let config_toml_path = codex_home.path().join("config.toml").abs();    let config_toml = "approvals_reviewer = \"user\"\napproval_policy = \"on-request\"\nsandbox_mode = \"workspace-write\"\n\n[features]\nguardian_approval = true\n";    std::fs::write(config_toml_path.as_path(), config_toml)?;    let user_config = toml::from_str::<TomlValue>(config_toml)?;    app.config.config_layer_stack = app        .config        .config_layer_stack        .with_user_config(&config_toml_path, user_config);    app.config        .features        .set_enabled(Feature::GuardianApproval, /*enabled*/ true)?;    app.chat_widget        .set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true);    app.config.approvals_reviewer = ApprovalsReviewer::User;    app.chat_widget        .set_approvals_reviewer(ApprovalsReviewer::User);    let mut app_server = start_config_write_test_app_server(&app).await?;    app.update_feature_flags(&mut app_server, vec![(Feature::GuardianApproval, false)])        .await;    assert!(!app.config.features.enabled(Feature::GuardianApproval));    assert_eq!(app.config.approvals_reviewer, ApprovalsReviewer::User);    assert_eq!(        app.chat_widget.config_ref().approvals_reviewer,        ApprovalsReviewer::User    );    assert_eq!(        op_rx.try_recv(),        Ok(Op::OverrideTurnContext {            cwd: None,            approval_policy: None,            approvals_reviewer: Some(ApprovalsReviewer::User),            permission_profile: None,            active_permission_profile: None,            windows_sandbox_level: None,            model: None,            effort: None,            summary: None,            service_tier: None,            collaboration_mode: None,            personality: None,        })    );    assert!(        app_event_rx.try_recv().is_err(),        "manual review should not emit a permissions history update when the effective state stays default"    );    let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;    assert!(!config.contains("guardian_approval = true"));    assert!(!config.contains("approvals_reviewer ="));    app_server.shutdown().await?;    Ok(())}#[tokio::test]async fn open_agent_picker_allows_existing_agent_threads_when_feature_is_disabled() -> Result<()> {    let (mut app, mut app_event_rx, _op_rx) = Box::pin(make_test_app_with_channels()).await;    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let thread_id = ThreadId::new();    app.thread_event_channels        .insert(thread_id, ThreadEventChannel::new(/*capacity*/ 1));    Box::pin(app.open_agent_picker(&mut app_server)).await;    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    assert_matches!(        app_event_rx.try_recv(),        Ok(AppEvent::SelectAgentThread(selected_thread_id)) if selected_thread_id == thread_id    );    Ok(())}#[tokio::test]async fn refresh_pending_thread_approvals_only_lists_inactive_threads() {    let mut app = make_test_app().await;    let main_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000001").expect("valid thread");    let agent_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000002").expect("valid thread");    app.primary_thread_id = Some(main_thread_id);    app.active_thread_id = Some(main_thread_id);    app.thread_event_channels        .insert(main_thread_id, ThreadEventChannel::new(/*capacity*/ 1));    let agent_channel = ThreadEventChannel::new(/*capacity*/ 1);    {        let mut store = agent_channel.store.lock().await;        store.push_request(exec_approval_request(            agent_thread_id,            "turn-1",            "call-1",            /*approval_id*/ None,        ));    }    app.thread_event_channels        .insert(agent_thread_id, agent_channel);    app.agent_navigation.upsert(        agent_thread_id,        Some("Robie".to_string()),        Some("explorer".to_string()),        /*is_closed*/ false,    );    app.refresh_pending_thread_approvals().await;    assert_eq!(        app.chat_widget.pending_thread_approvals(),        &["Robie [explorer]".to_string()]    );    app.active_thread_id = Some(agent_thread_id);    app.refresh_pending_thread_approvals().await;    assert!(app.chat_widget.pending_thread_approvals().is_empty());}#[tokio::test]async fn inactive_thread_approval_bubbles_into_active_view() -> Result<()> {    let mut app = make_test_app().await;    let main_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000011").expect("valid thread");    let agent_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000022").expect("valid thread");    app.primary_thread_id = Some(main_thread_id);    app.active_thread_id = Some(main_thread_id);    app.thread_event_channels        .insert(main_thread_id, ThreadEventChannel::new(/*capacity*/ 1));    app.thread_event_channels.insert(        agent_thread_id,        ThreadEventChannel::new_with_session(            /*capacity*/ 1,            ThreadSessionState {                approval_policy: AskForApproval::OnRequest,                permission_profile: PermissionProfile::workspace_write(),                rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")),                ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent"))            },            Vec::new(),        ),    );    app.agent_navigation.upsert(        agent_thread_id,        Some("Robie".to_string()),        Some("explorer".to_string()),        /*is_closed*/ false,    );    app.enqueue_thread_request(        agent_thread_id,        exec_approval_request(            agent_thread_id,            "turn-approval",            "call-approval",            /*approval_id*/ None,        ),    )    .await?;    assert_eq!(app.chat_widget.has_active_view(), true);    assert_eq!(        app.chat_widget.pending_thread_approvals(),        &["Robie [explorer]".to_string()]    );    Ok(())}#[tokio::test]async fn side_defers_parent_approval_overlay_until_parent_replay() -> Result<()> {    let mut app = make_test_app().await;    let parent_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000011").expect("valid thread");    let side_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000022").expect("valid thread");    app.primary_thread_id = Some(parent_thread_id);    app.active_thread_id = Some(side_thread_id);    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    app.thread_event_channels.insert(        parent_thread_id,        ThreadEventChannel::new_with_session(            /*capacity*/ 4,            test_thread_session(parent_thread_id, test_path_buf("/tmp/main")),            Vec::new(),        ),    );    app.enqueue_thread_request(        parent_thread_id,        exec_approval_request(            parent_thread_id,            "turn-approval",            "call-approval",            /*approval_id*/ None,        ),    )    .await?;    assert_eq!(app.chat_widget.has_active_view(), false);    assert!(app.chat_widget.pending_thread_approvals().is_empty());    assert_eq!(        app.side_threads            .get(&side_thread_id)            .and_then(|state| state.parent_status),        Some(SideParentStatus::NeedsApproval)    );    let snapshot = {        let channel = app            .thread_event_channels            .get(&parent_thread_id)            .expect("parent thread channel");        let store = channel.store.lock().await;        store.snapshot()    };    app.side_threads.remove(&side_thread_id);    app.active_thread_id = Some(parent_thread_id);    app.replay_thread_snapshot(snapshot, /*resume_restored_queue*/ false);    assert_eq!(app.chat_widget.has_active_view(), true);    Ok(())}#[tokio::test]async fn replay_snapshot_with_pending_request_suppresses_replay_notices() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000011").expect("valid thread");    let stale_warning = "stale startup warning that should not cover the approval";    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: Some(test_thread_session(thread_id, test_path_buf("/tmp/main"))),            turns: Vec::new(),            events: vec![                ThreadBufferedEvent::Notification(ServerNotification::Warning(                    WarningNotification {                        thread_id: Some(thread_id.to_string()),                        message: stale_warning.to_string(),                    },                )),                ThreadBufferedEvent::Request(exec_approval_request(                    thread_id,                    "turn-approval",                    "call-approval",                    /*approval_id*/ None,                )),            ],            input_state: None,        },        /*resume_restored_queue*/ false,    );    assert_eq!(app.chat_widget.has_active_view(), true);    let mut replayed_history = String::new();    while let Ok(event) = app_event_rx.try_recv() {        if let AppEvent::InsertHistoryCell(cell) = event {            replayed_history.push_str(&lines_to_single_string(                &cell.transcript_lines(/*width*/ 80),            ));        }    }    assert!(        replayed_history.is_empty(),        "expected pending approval replay to suppress session notices, got {replayed_history:?}"    );}#[tokio::test]async fn side_defers_subagent_approval_overlay_until_side_exits() -> Result<()> {    let mut app = make_test_app().await;    let main_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000011").expect("valid thread");    let side_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000022").expect("valid thread");    let agent_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000033").expect("valid thread");    app.primary_thread_id = Some(main_thread_id);    app.active_thread_id = Some(side_thread_id);    app.side_threads        .insert(side_thread_id, SideThreadState::new(main_thread_id));    app.thread_event_channels.insert(        agent_thread_id,        ThreadEventChannel::new_with_session(            /*capacity*/ 4,            ThreadSessionState {                approval_policy: AskForApproval::OnRequest,                permission_profile: PermissionProfile::workspace_write(),                rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")),                ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent"))            },            Vec::new(),        ),    );    app.agent_navigation.upsert(        agent_thread_id,        Some("Robie".to_string()),        Some("explorer".to_string()),        /*is_closed*/ false,    );    app.enqueue_thread_request(        agent_thread_id,        exec_approval_request(            agent_thread_id,            "turn-approval",            "call-approval",            /*approval_id*/ None,        ),    )    .await?;    assert_eq!(app.chat_widget.has_active_view(), false);    assert_eq!(        app.chat_widget.pending_thread_approvals(),        &["Robie [explorer]".to_string()]    );    app.side_threads.remove(&side_thread_id);    app.active_thread_id = Some(main_thread_id);    app.surface_pending_inactive_thread_interactive_requests()        .await?;    assert_eq!(app.chat_widget.has_active_view(), true);    Ok(())}#[tokio::test]async fn inactive_thread_exec_approval_preserves_context() {    let app = make_test_app().await;    let thread_id = ThreadId::new();    let mut request = exec_approval_request(        thread_id,        "turn-approval",        "call-approval",        /*approval_id*/ None,    );    let ServerRequest::CommandExecutionRequestApproval { params, .. } = &mut request else {        panic!("expected exec approval request");    };    params.network_approval_context = Some(AppServerNetworkApprovalContext {        host: "example.com".to_string(),        protocol: AppServerNetworkApprovalProtocol::Socks5Tcp,    });    params.additional_permissions = Some(AdditionalPermissionProfile {        network: Some(AdditionalNetworkPermissions {            enabled: Some(true),        }),        file_system: Some(AdditionalFileSystemPermissions {            read: Some(vec![test_absolute_path("/tmp/read-only").into()]),            write: Some(vec![test_absolute_path("/tmp/write").into()]),            glob_scan_max_depth: None,            entries: None,        }),    });    params.proposed_network_policy_amendments = Some(vec![AppServerNetworkPolicyAmendment {        host: "example.com".to_string(),        action: AppServerNetworkPolicyRuleAction::Allow,    }]);    let Some(ThreadInteractiveRequest::Approval(ApprovalRequest::Exec {        available_decisions,        network_approval_context,        additional_permissions,        ..    })) = app        .interactive_request_for_thread_request(thread_id, &request)        .await        .expect("valid localized paths")    else {        panic!("expected exec approval request");    };    assert_eq!(        network_approval_context,        Some(AppServerNetworkApprovalContext {            host: "example.com".to_string(),            protocol: AppServerNetworkApprovalProtocol::Socks5Tcp,        })    );    assert_eq!(        additional_permissions,        Some(AdditionalPermissionProfile {            network: Some(AdditionalNetworkPermissions {                enabled: Some(true),            }),            file_system: Some(AdditionalFileSystemPermissions {                read: Some(vec![test_absolute_path("/tmp/read-only").into()]),                write: Some(vec![test_absolute_path("/tmp/write").into()]),                glob_scan_max_depth: None,                entries: None,            }),        })    );    assert_eq!(        available_decisions,        vec![            codex_app_server_protocol::CommandExecutionApprovalDecision::Accept,            codex_app_server_protocol::CommandExecutionApprovalDecision::AcceptForSession,            codex_app_server_protocol::CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment {                network_policy_amendment: AppServerNetworkPolicyAmendment {                    host: "example.com".to_string(),                    action: AppServerNetworkPolicyRuleAction::Allow,                },            },            codex_app_server_protocol::CommandExecutionApprovalDecision::Cancel,        ]    );}#[tokio::test]async fn inactive_thread_exec_approval_splits_shell_wrapped_command() {    let app = make_test_app().await;    let thread_id = ThreadId::new();    let script = r#"python3 -c 'print("Hello, world!")'"#;    let mut request = exec_approval_request(        thread_id,        "turn-approval",        "call-approval",        /*approval_id*/ None,    );    let ServerRequest::CommandExecutionRequestApproval { params, .. } = &mut request else {        panic!("expected exec approval request");    };    params.command =        Some(shlex::try_join(["/bin/zsh", "-lc", script]).expect("round-trippable shell wrapper"));    let Some(ThreadInteractiveRequest::Approval(ApprovalRequest::Exec { command, .. })) = app        .interactive_request_for_thread_request(thread_id, &request)        .await        .expect("valid localized paths")    else {        panic!("expected exec approval request");    };    assert_eq!(        command,        vec![            "/bin/zsh".to_string(),            "-lc".to_string(),            script.to_string(),        ]    );}#[tokio::test]async fn inactive_thread_file_change_approval_recovers_buffered_changes() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    app.enqueue_thread_notification(        thread_id,        ServerNotification::ItemStarted(ItemStartedNotification {            thread_id: thread_id.to_string(),            turn_id: "turn-approval".to_string(),            started_at_ms: 0,            item: ThreadItem::FileChange {                id: "patch-approval".to_string(),                changes: vec![FileUpdateChange {                    path: "README.md".to_string(),                    kind: PatchChangeKind::Add,                    diff: "hello\n".to_string(),                }],                status: codex_app_server_protocol::PatchApplyStatus::InProgress,            },        }),    )    .await    .expect("enqueue file change item");    let request = ServerRequest::FileChangeRequestApproval {        request_id: AppServerRequestId::Integer(9),        params: FileChangeRequestApprovalParams {            thread_id: thread_id.to_string(),            turn_id: "turn-approval".to_string(),            item_id: "patch-approval".to_string(),            started_at_ms: 0,            reason: Some("command failed; retry without sandbox?".to_string()),            grant_root: None,        },    };    let request = app        .interactive_request_for_thread_request(thread_id, &request)        .await        .expect("valid localized paths")        .expect("expected file change approval request");    let ThreadInteractiveRequest::Approval(ApprovalRequest::ApplyPatch {        changes, reason, ..    }) = &request    else {        panic!("expected apply-patch approval request");    };    assert_eq!(        changes,        &HashMap::from([(            PathBuf::from("README.md"),            FileChange::Add {                content: "hello\n".to_string(),            },        )])    );    assert_eq!(        reason,        &Some("command failed; retry without sandbox?".to_string())    );    app.push_thread_interactive_request(request);    let cell = match app_event_rx.try_recv() {        Ok(AppEvent::InsertHistoryCell(cell)) => cell,        other => panic!("expected patch preview history cell, saw {other:?}"),    };    let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));    assert!(rendered.contains("• Added README.md (+1 -0)"));    assert!(rendered.contains("1 +hello"));}#[tokio::test]async fn inactive_thread_permissions_approval_preserves_file_system_permissions() {    let app = make_test_app().await;    let thread_id = ThreadId::new();    let request = ServerRequest::PermissionsRequestApproval {        request_id: AppServerRequestId::Integer(7),        params: PermissionsRequestApprovalParams {            thread_id: thread_id.to_string(),            turn_id: "turn-approval".to_string(),            item_id: "call-approval".to_string(),            environment_id: Some("remote".to_string()),            started_at_ms: 0,            cwd: test_absolute_path("/tmp"),            reason: Some("Need access to .git".to_string()),            permissions: codex_app_server_protocol::RequestPermissionProfile {                network: Some(AdditionalNetworkPermissions {                    enabled: Some(true),                }),                file_system: Some(AdditionalFileSystemPermissions {                    read: Some(vec![test_absolute_path("/tmp/read-only").into()]),                    write: Some(vec![test_absolute_path("/tmp/write").into()]),                    glob_scan_max_depth: None,                    entries: None,                }),            },        },    };    let Some(ThreadInteractiveRequest::Approval(ApprovalRequest::Permissions {        environment_id,        permissions,        ..    })) = app        .interactive_request_for_thread_request(thread_id, &request)        .await        .expect("valid localized paths")    else {        panic!("expected permissions approval request");    };    assert_eq!(environment_id.as_deref(), Some("remote"));    assert_eq!(        permissions,        RequestPermissionProfile {            network: Some(NetworkPermissions {                enabled: Some(true),            }),            file_system: Some(FileSystemPermissions::from_read_write_roots(                Some(vec![test_absolute_path("/tmp/read-only")]),                Some(vec![test_absolute_path("/tmp/write")]),            )),        }    );}#[tokio::test]async fn inactive_thread_url_elicitation_routes_to_app_link() {    let app = make_test_app().await;    let thread_id = ThreadId::new();    let request = ServerRequest::McpServerElicitationRequest {        request_id: AppServerRequestId::Integer(9),        params: McpServerElicitationRequestParams {            thread_id: thread_id.to_string(),            turn_id: Some("turn-auth".to_string()),            server_name: "payments".to_string(),            request: McpServerElicitationRequest::Url {                meta: None,                message: "Review the payment details to continue.".to_string(),                url: "https://payments.example/checkout/123".to_string(),                elicitation_id: "payment-123".to_string(),            },        },    };    let Some(ThreadInteractiveRequest::AppLink(params)) = app        .interactive_request_for_thread_request(thread_id, &request)        .await        .expect("valid localized paths")    else {        panic!("expected app link request");    };    assert_eq!(params.title, "Action required");    assert_eq!(params.description, Some("Server: payments".to_string()));    assert_eq!(params.url, "https://payments.example/checkout/123");    assert_eq!(        params.elicitation_target,        Some(crate::bottom_pane::AppLinkElicitationTarget {            thread_id,            server_name: "payments".to_string(),            request_id: AppServerRequestId::Integer(9),        })    );}#[tokio::test]async fn inactive_thread_invalid_url_elicitation_is_declined() {    let (app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    let request = ServerRequest::McpServerElicitationRequest {        request_id: AppServerRequestId::Integer(10),        params: McpServerElicitationRequestParams {            thread_id: thread_id.to_string(),            turn_id: Some("turn-auth".to_string()),            server_name: "payments".to_string(),            request: McpServerElicitationRequest::Url {                meta: None,                message: "Review the payment details to continue.".to_string(),                url: "http://payments.example/checkout/123".to_string(),                elicitation_id: "payment-123".to_string(),            },        },    };    assert!(        app.interactive_request_for_thread_request(thread_id, &request)            .await            .expect("valid localized paths")            .is_none()    );    assert_matches!(        app_event_rx.try_recv(),        Ok(AppEvent::SubmitThreadOp {            thread_id: op_thread_id,            op: Op::ResolveElicitation {                server_name,                request_id: AppServerRequestId::Integer(10),                decision: codex_app_server_protocol::McpServerElicitationAction::Decline,                content: None,                meta: None,            },        }) if op_thread_id == thread_id && server_name == "payments"    );}#[tokio::test]async fn inactive_thread_approval_badge_clears_after_turn_completion_notification() -> Result<()> {    let mut app = make_test_app().await;    let main_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000101").expect("valid thread");    let agent_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000202").expect("valid thread");    app.primary_thread_id = Some(main_thread_id);    app.active_thread_id = Some(main_thread_id);    app.thread_event_channels        .insert(main_thread_id, ThreadEventChannel::new(/*capacity*/ 1));    app.thread_event_channels.insert(        agent_thread_id,        ThreadEventChannel::new_with_session(            /*capacity*/ 4,            ThreadSessionState {                approval_policy: AskForApproval::OnRequest,                permission_profile: PermissionProfile::workspace_write(),                rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")),                ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent"))            },            Vec::new(),        ),    );    app.agent_navigation.upsert(        agent_thread_id,        Some("Robie".to_string()),        Some("explorer".to_string()),        /*is_closed*/ false,    );    app.enqueue_thread_request(        agent_thread_id,        exec_approval_request(            agent_thread_id,            "turn-approval",            "call-approval",            /*approval_id*/ None,        ),    )    .await?;    assert_eq!(        app.chat_widget.pending_thread_approvals(),        &["Robie [explorer]".to_string()]    );    app.enqueue_thread_notification(        agent_thread_id,        turn_completed_notification(agent_thread_id, "turn-approval", TurnStatus::Completed),    )    .await?;    assert!(        app.chat_widget.pending_thread_approvals().is_empty(),        "turn completion should clear inactive-thread approval badge immediately"    );    Ok(())}#[tokio::test]async fn inactive_thread_started_notification_initializes_replay_session() -> Result<()> {    let mut app = make_test_app().await;    let temp_dir = tempdir()?;    let main_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000101").expect("valid thread");    let agent_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000202").expect("valid thread");    let primary_cwd = test_path_buf("/tmp/main").abs();    let shared_root = test_path_buf("/tmp/shared").abs();    let primary_session = ThreadSessionState {        approval_policy: AskForApproval::OnRequest,        permission_profile: PermissionProfile::workspace_write(),        runtime_workspace_roots: vec![primary_cwd.clone(), shared_root.clone()],        ..test_thread_session(main_thread_id, primary_cwd.to_path_buf())    };    app.primary_thread_id = Some(main_thread_id);    app.active_thread_id = Some(main_thread_id);    app.primary_session_configured = Some(primary_session.clone());    app.thread_event_channels.insert(        main_thread_id,        ThreadEventChannel::new_with_session(            /*capacity*/ 4,            primary_session.clone(),            Vec::new(),        ),    );    let rollout_path = temp_dir.path().join("agent-rollout.jsonl");    let rollout = serde_json::json!({        "timestamp": "t0",        "type": "turn_context",        "payload": {            "cwd": test_path_buf("/tmp/agent"),            "model": "gpt-agent",        },    });    std::fs::write(        &rollout_path,        format!("{}\n", serde_json::to_string(&rollout)?),    )?;    app.enqueue_thread_notification(        agent_thread_id,        ServerNotification::ThreadStarted(ThreadStartedNotification {            thread: Thread {                id: agent_thread_id.to_string(),                session_id: agent_thread_id.to_string(),                forked_from_id: None,                parent_thread_id: None,                preview: "agent thread".to_string(),                ephemeral: false,                model_provider: "agent-provider".to_string(),                created_at: 1,                updated_at: 2,                status: codex_app_server_protocol::ThreadStatus::Idle,                path: Some(rollout_path.clone()),                cwd: test_path_buf("/tmp/agent").abs(),                cli_version: "0.0.0".to_string(),                source: codex_app_server_protocol::SessionSource::Unknown,                thread_source: None,                agent_nickname: Some("Robie".to_string()),                agent_role: Some("explorer".to_string()),                git_info: None,                name: Some("agent thread".to_string()),                turns: Vec::new(),            },        }),    )    .await?;    let store = app        .thread_event_channels        .get(&agent_thread_id)        .expect("agent thread channel")        .store        .lock()        .await;    let session = store.session.clone().expect("inferred session");    drop(store);    assert_eq!(session.thread_id, agent_thread_id);    assert_eq!(session.thread_name, Some("agent thread".to_string()));    assert_eq!(session.model, "gpt-agent");    assert_eq!(session.model_provider_id, "agent-provider");    assert_eq!(session.approval_policy, primary_session.approval_policy);    assert_eq!(session.cwd.as_path(), test_path_buf("/tmp/agent").as_path());    assert_eq!(        session.runtime_workspace_roots,        vec![test_path_buf("/tmp/agent").abs(), shared_root]    );    assert_eq!(session.rollout_path, Some(rollout_path));    assert_eq!(        app.agent_navigation.get(&agent_thread_id),        Some(&AgentPickerThreadEntry {            agent_nickname: Some("Robie".to_string()),            agent_role: Some("explorer".to_string()),            agent_path: None,            is_running: false,            is_closed: false,        })    );    Ok(())}#[tokio::test]async fn inactive_thread_started_notification_preserves_primary_model_when_path_missing()-> Result<()> {    let mut app = make_test_app().await;    let main_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000301").expect("valid thread");    let agent_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000302").expect("valid thread");    let primary_cwd = test_path_buf("/tmp/main").abs();    let primary_session = ThreadSessionState {        approval_policy: AskForApproval::OnRequest,        permission_profile: PermissionProfile::workspace_write(),        runtime_workspace_roots: vec![primary_cwd.clone()],        ..test_thread_session(main_thread_id, primary_cwd.to_path_buf())    };    app.primary_thread_id = Some(main_thread_id);    app.active_thread_id = Some(main_thread_id);    app.primary_session_configured = Some(primary_session.clone());    app.thread_event_channels.insert(        main_thread_id,        ThreadEventChannel::new_with_session(            /*capacity*/ 4,            primary_session.clone(),            Vec::new(),        ),    );    app.enqueue_thread_notification(        agent_thread_id,        ServerNotification::ThreadStarted(ThreadStartedNotification {            thread: Thread {                id: agent_thread_id.to_string(),                session_id: agent_thread_id.to_string(),                forked_from_id: None,                parent_thread_id: None,                preview: "agent thread".to_string(),                ephemeral: false,                model_provider: "agent-provider".to_string(),                created_at: 1,                updated_at: 2,                status: codex_app_server_protocol::ThreadStatus::Idle,                path: None,                cwd: test_path_buf("/tmp/agent").abs(),                cli_version: "0.0.0".to_string(),                source: codex_app_server_protocol::SessionSource::Unknown,                thread_source: None,                agent_nickname: Some("Robie".to_string()),                agent_role: Some("explorer".to_string()),                git_info: None,                name: Some("agent thread".to_string()),                turns: Vec::new(),            },        }),    )    .await?;    let store = app        .thread_event_channels        .get(&agent_thread_id)        .expect("agent thread channel")        .store        .lock()        .await;    let session = store.session.clone().expect("inferred session");    assert_eq!(session.model, primary_session.model);    Ok(())}/// `thread/read` is metadata/replay hydration and does not return a fresh/// server-authored `PermissionProfile`, so it must not reuse the cached primary/// session profile after swapping in the read thread's cwd.#[tokio::test]async fn thread_read_session_state_does_not_reuse_primary_permission_profile() {    let mut app = make_test_app().await;    let main_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000401").expect("valid thread");    let read_thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000402").expect("valid thread");    let primary_cwd = test_path_buf("/tmp/main").abs();    let primary_session = ThreadSessionState {        approval_policy: AskForApproval::OnRequest,        permission_profile: PermissionProfile::workspace_write(),        runtime_workspace_roots: vec![primary_cwd.clone()],        ..test_thread_session(main_thread_id, primary_cwd.to_path_buf())    };    app.primary_session_configured = Some(primary_session);    let thread = Thread {        id: read_thread_id.to_string(),        session_id: read_thread_id.to_string(),        forked_from_id: None,        parent_thread_id: None,        preview: "read thread".to_string(),        ephemeral: false,        model_provider: "read-provider".to_string(),        created_at: 1,        updated_at: 2,        status: codex_app_server_protocol::ThreadStatus::Idle,        path: None,        cwd: test_path_buf("/tmp/read").abs(),        cli_version: "0.0.0".to_string(),        source: codex_app_server_protocol::SessionSource::Unknown,        thread_source: None,        agent_nickname: None,        agent_role: None,        git_info: None,        name: Some("read thread".to_string()),        turns: Vec::new(),    };    let session = app        .session_state_for_thread_read(read_thread_id, &thread)        .await;    assert_eq!(session.thread_id, read_thread_id);    assert_eq!(session.cwd.as_path(), test_path_buf("/tmp/read").as_path());    assert_eq!(        session.runtime_workspace_roots,        vec![test_path_buf("/tmp/read").abs()]    );    let expected_permission_profile = app        .chat_widget        .config_ref()        .permissions        .permission_profile()        .clone();    assert_eq!(        session.permission_profile, expected_permission_profile,        "thread/read does not return fresh server permissions; the fallback profile must use the \         active widget permissions rather than reusing the cached primary session profile"    );}#[test]fn agent_picker_item_name_snapshot() {    let thread_id =        ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id");    let snapshot = [        format!(            "{} | {}",            format_agent_picker_item_name(                Some("Robie"),                Some("explorer"),                /*is_primary*/ true            ),            thread_id        ),        format!(            "{} | {}",            format_agent_picker_item_name(                Some("Robie"),                Some("explorer"),                /*is_primary*/ false            ),            thread_id        ),        format!(            "{} | {}",            format_agent_picker_item_name(                Some("Robie"),                /*agent_role*/ None,                /*is_primary*/ false            ),            thread_id        ),        format!(            "{} | {}",            format_agent_picker_item_name(                /*agent_nickname*/ None,                Some("explorer"),                /*is_primary*/ false            ),            thread_id        ),        format!(            "{} | {}",            format_agent_picker_item_name(                /*agent_nickname*/ None, /*agent_role*/ None, /*is_primary*/ false            ),            thread_id        ),    ]    .join("\n");    assert_app_snapshot!("agent_picker_item_name", snapshot);}#[tokio::test]async fn side_fork_config_is_ephemeral_and_appends_developer_guardrails() {    let app = make_test_app().await;    let original_approval_policy = app.config.permissions.approval_policy.value();    let original_sandbox_policy = app.config.legacy_sandbox_policy();    let fork_config = app.side_fork_config();    assert!(fork_config.ephemeral);    assert_eq!(        fork_config.permissions.approval_policy.value(),        original_approval_policy    );    assert_eq!(fork_config.legacy_sandbox_policy(), original_sandbox_policy);    let developer_instructions = fork_config        .developer_instructions        .as_deref()        .expect("side developer instructions");    assert!(        developer_instructions.contains("You are in a side conversation, not the main thread.")    );    assert!(        developer_instructions            .contains("inherited fork history is provided only as reference context")    );    assert!(        developer_instructions.contains(            "Only instructions submitted after the side-conversation boundary are active"        )    );    assert!(developer_instructions.contains("Do not continue, execute, or complete any task"));    assert!(        developer_instructions            .contains("External tools may be available according to this thread's current")    );    assert!(        developer_instructions            .contains("Any MCP or external tool calls or outputs visible in the inherited")    );    assert!(developer_instructions.contains("non-mutating inspection"));    assert!(developer_instructions.contains("Do not modify files"));    assert!(developer_instructions.contains("Do not request escalated permissions"));    assert!(app.transcript_cells.is_empty());}#[tokio::test]async fn side_fork_config_inherits_parent_thread_runtime_settings() {    let mut app = make_test_app().await;    app.config.model = Some("persisted-default-model".to_string());    app.config.model_reasoning_effort = Some(ReasoningEffortConfig::Low);    let parent_service_tier = ServiceTier::Fast.request_value();    let parent_permission_profile = PermissionProfile::workspace_write();    app.chat_widget.set_model("parent-thread-model");    app.chat_widget        .set_reasoning_effort(Some(ReasoningEffortConfig::High));    app.chat_widget        .set_service_tier(Some(parent_service_tier.to_string()));    app.chat_widget        .set_approval_policy(AskForApproval::OnRequest);    app.chat_widget        .set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::legacy(            parent_permission_profile.clone(),        ))        .expect("test permission profile should be accepted");    app.chat_widget        .set_approvals_reviewer(ApprovalsReviewer::AutoReview);    let fork_config = app.side_fork_config();    assert_eq!(        (            fork_config.model.as_deref(),            fork_config.model_reasoning_effort,            fork_config.service_tier.as_deref(),            fork_config.permissions.approval_policy.value(),            fork_config.permissions.permission_profile(),            fork_config.approvals_reviewer,        ),        (            Some("parent-thread-model"),            Some(ReasoningEffortConfig::High),            Some(parent_service_tier),            AskForApproval::OnRequest.to_core(),            &parent_permission_profile,            ApprovalsReviewer::AutoReview,        )    );}#[tokio::test]async fn side_start_block_message_tracks_open_side_conversation() {    let mut app = make_test_app().await;    assert_eq!(        app.side_start_block_message(),        Some("'/side' is unavailable until the main thread is ready.")    );    app.primary_thread_id = Some(ThreadId::new());    assert_eq!(app.side_start_block_message(), None);    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    assert_eq!(        app.side_start_block_message(),        Some(            "A side conversation is already open. Press Ctrl+C to return before starting another."        )    );    app.side_threads.remove(&side_thread_id);    assert_eq!(app.side_start_block_message(), None);}#[tokio::test]async fn side_parent_status_tracks_parent_turn_lifecycle() -> Result<()> {    let mut app = make_test_app().await;    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    app.primary_thread_id = Some(parent_thread_id);    app.active_thread_id = Some(side_thread_id);    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    app.enqueue_thread_notification(        parent_thread_id,        turn_completed_notification(parent_thread_id, "turn-1", TurnStatus::Completed),    )    .await?;    assert_eq!(        app.side_threads            .get(&side_thread_id)            .and_then(|state| state.parent_status),        Some(SideParentStatus::Finished)    );    app.enqueue_thread_notification(        parent_thread_id,        turn_started_notification(parent_thread_id, "turn-2"),    )    .await?;    assert_eq!(        app.side_threads            .get(&side_thread_id)            .and_then(|state| state.parent_status),        None    );    app.enqueue_thread_notification(        parent_thread_id,        turn_completed_notification(parent_thread_id, "turn-2", TurnStatus::Failed),    )    .await?;    assert_eq!(        app.side_threads            .get(&side_thread_id)            .and_then(|state| state.parent_status),        Some(SideParentStatus::Failed)    );    Ok(())}#[tokio::test]async fn side_parent_status_prioritizes_input_over_approval() -> Result<()> {    let mut app = make_test_app().await;    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    app.primary_thread_id = Some(parent_thread_id);    app.active_thread_id = Some(side_thread_id);    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    app.enqueue_thread_request(        parent_thread_id,        exec_approval_request(            parent_thread_id,            "turn-approval",            "call-approval",            /*approval_id*/ None,        ),    )    .await?;    assert_eq!(        app.side_threads            .get(&side_thread_id)            .and_then(|state| state.parent_status),        Some(SideParentStatus::NeedsApproval)    );    app.enqueue_thread_request(        parent_thread_id,        request_user_input_request(parent_thread_id, "turn-input", "call-input"),    )    .await?;    assert_eq!(        app.side_threads            .get(&side_thread_id)            .and_then(|state| state.parent_status),        Some(SideParentStatus::NeedsInput)    );    app.enqueue_thread_notification(        parent_thread_id,        ServerNotification::ServerRequestResolved(            codex_app_server_protocol::ServerRequestResolvedNotification {                thread_id: parent_thread_id.to_string(),                request_id: AppServerRequestId::Integer(2),            },        ),    )    .await?;    assert_eq!(        app.side_threads            .get(&side_thread_id)            .and_then(|state| state.parent_status),        Some(SideParentStatus::NeedsApproval)    );    app.enqueue_thread_notification(        parent_thread_id,        ServerNotification::ServerRequestResolved(            codex_app_server_protocol::ServerRequestResolvedNotification {                thread_id: parent_thread_id.to_string(),                request_id: AppServerRequestId::Integer(1),            },        ),    )    .await?;    assert_eq!(        app.side_threads            .get(&side_thread_id)            .and_then(|state| state.parent_status),        None    );    Ok(())}#[tokio::test]async fn side_thread_snapshot_hides_forked_parent_transcript() {    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    let mut store = ThreadEventStore::new(/*capacity*/ 4);    let session = ThreadSessionState {        forked_from_id: Some(parent_thread_id),        fork_parent_title: None,        ..test_thread_session(side_thread_id, test_path_buf("/tmp/side"))    };    let parent_turn = test_turn(        "parent-turn",        TurnStatus::Completed,        vec![ThreadItem::UserMessage {            id: "parent-user".to_string(),            client_id: None,            content: vec![AppServerUserInput::Text {                text: "parent prompt should stay hidden".to_string(),                text_elements: Vec::new(),            }],        }],    );    App::install_side_thread_snapshot(&mut store, session, vec![parent_turn]);    let stored_session = store.session.as_ref().expect("side session");    assert_eq!(stored_session.thread_id, side_thread_id);    assert_eq!(stored_session.forked_from_id, None);    assert_eq!(store.turns, Vec::<Turn>::new());    assert_eq!(store.active_turn_id(), None);}#[tokio::test]async fn side_thread_snapshot_does_not_refresh_from_fork_history() {    let mut app = make_test_app().await;    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    let snapshot = ThreadEventSnapshot {        session: Some(ThreadSessionState {            rollout_path: None,            ..test_thread_session(side_thread_id, test_path_buf("/tmp/side"))        }),        turns: Vec::new(),        events: Vec::new(),        input_state: None,    };    assert!(!app.should_refresh_snapshot_session(        side_thread_id,        /*is_replay_only*/ false,        &snapshot    ));}#[tokio::test]async fn side_thread_snapshot_skips_session_header_preamble() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    while app_event_rx.try_recv().is_ok() {}    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    app.primary_thread_id = Some(parent_thread_id);    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    let snapshot = ThreadEventSnapshot {        session: Some(ThreadSessionState {            forked_from_id: Some(parent_thread_id),            fork_parent_title: None,            ..test_thread_session(side_thread_id, test_path_buf("/tmp/side"))        }),        turns: Vec::new(),        events: Vec::new(),        input_state: None,    };    app.replay_thread_snapshot(snapshot, /*resume_restored_queue*/ false);    let mut rendered_cells = Vec::new();    while let Ok(event) = app_event_rx.try_recv() {        if let AppEvent::InsertHistoryCell(cell) = event {            rendered_cells.push(lines_to_single_string(&cell.display_lines(/*width*/ 120)));        }    }    assert_eq!(app.chat_widget.thread_id(), Some(side_thread_id));    assert_eq!(rendered_cells, Vec::<String>::new());    assert_eq!(        app.chat_widget.active_cell_transcript_lines(/*width*/ 120),        None    );}#[tokio::test]async fn primary_thread_ignores_child_mcp_startup_notifications() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    while app_event_rx.try_recv().is_ok() {}    let sentry_config = toml::from_str::<toml::Value>("command = 'true'")        .expect("test MCP config should parse")        .try_into()        .expect("test MCP config should deserialize");    app.config        .mcp_servers        .set(std::collections::HashMap::from([(            "sentry".to_string(),            sentry_config,        )]))        .expect("test MCP servers should accept any configuration");    let app_server = crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref())        .await        .expect("embedded app server");    let parent_thread_id = ThreadId::new();    let child_thread_id = ThreadId::new();    app.primary_thread_id = Some(parent_thread_id);    app.active_thread_id = Some(parent_thread_id);    app.handle_app_server_event(        &app_server,        codex_app_server_client::AppServerEvent::ServerNotification(            ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {                thread_id: Some(child_thread_id.to_string()),                name: "sentry".to_string(),                status: McpServerStartupState::Failed,                error: Some("sentry is not logged in".to_string()),            }),        ),    )    .await;    assert!(app_event_rx.try_recv().is_err());    let mut child_snapshot = app        .thread_event_channels        .get(&child_thread_id)        .expect("child thread channel should be created")        .store        .lock()        .await        .snapshot();    assert!(        matches!(            child_snapshot.events.as_slice(),            [ThreadBufferedEvent::Notification(                ServerNotification::McpServerStatusUpdated(_)            )]        ),        "child MCP startup notification should be buffered for the child thread"    );    app.apply_refreshed_snapshot_thread(        child_thread_id,        AppServerStartedThread {            session: test_thread_session(child_thread_id, test_path_buf("/tmp/child")),            turns: Vec::new(),        },        &mut child_snapshot,    )    .await;    app.replay_thread_snapshot(child_snapshot, /*resume_restored_queue*/ false);    let mut rendered_cells = Vec::new();    while let Ok(event) = app_event_rx.try_recv() {        if let AppEvent::InsertHistoryCell(cell) = event {            rendered_cells.push(lines_to_single_string(&cell.display_lines(/*width*/ 120)));        }    }    let rendered = rendered_cells.join("\n");    assert_eq!(app.chat_widget.thread_id(), Some(child_thread_id));    assert_eq!(rendered.matches("sentry is not logged in").count(), 1);    assert_eq!(        rendered            .matches("MCP startup incomplete (failed: sentry)")            .count(),        1    );}#[tokio::test]async fn app_scoped_mcp_startup_notifications_do_not_render_in_active_thread() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    while app_event_rx.try_recv().is_ok() {}    let app_server = crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref())        .await        .expect("embedded app server");    let thread_id = ThreadId::new();    app.primary_thread_id = Some(thread_id);    app.active_thread_id = Some(thread_id);    app.handle_app_server_event(        &app_server,        codex_app_server_client::AppServerEvent::ServerNotification(            ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {                thread_id: None,                name: "sentry".to_string(),                status: McpServerStartupState::Failed,                error: Some("sentry is not logged in".to_string()),            }),        ),    )    .await;    assert!(app_event_rx.try_recv().is_err());    assert_eq!(        app.chat_widget.active_cell_transcript_lines(/*width*/ 120),        None    );}#[tokio::test]async fn active_side_thread_renders_live_mcp_startup_notifications() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    while app_event_rx.try_recv().is_ok() {}    let sentry_config = toml::from_str::<toml::Value>("command = 'true'")        .expect("test MCP config should parse")        .try_into()        .expect("test MCP config should deserialize");    app.config        .mcp_servers        .set(std::collections::HashMap::from([(            "sentry".to_string(),            sentry_config,        )]))        .expect("test MCP servers should accept any configuration");    let app_server = crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref())        .await        .expect("embedded app server");    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    app.primary_thread_id = Some(parent_thread_id);    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    app.ensure_thread_channel(side_thread_id);    app.activate_thread_channel(side_thread_id).await;    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: Some(test_thread_session(                side_thread_id,                test_path_buf("/tmp/side"),            )),            turns: Vec::new(),            events: Vec::new(),            input_state: None,        },        /*resume_restored_queue*/ false,    );    app.sync_side_thread_ui();    for status in [        McpServerStartupState::Starting,        McpServerStartupState::Failed,    ] {        app.handle_app_server_event(            &app_server,            codex_app_server_client::AppServerEvent::ServerNotification(                ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {                    thread_id: Some(side_thread_id.to_string()),                    name: "sentry".to_string(),                    status,                    error: matches!(status, McpServerStartupState::Failed)                        .then(|| "sentry is not logged in".to_string()),                }),            ),        )        .await;    }    let mut active_thread_events = Vec::new();    let active_thread_rx = app        .active_thread_rx        .as_mut()        .expect("side thread receiver should be active");    while let Ok(event) = active_thread_rx.try_recv() {        active_thread_events.push(event);    }    for event in active_thread_events {        app.handle_thread_event_now(event);    }    let mut rendered_cells = Vec::new();    while let Ok(event) = app_event_rx.try_recv() {        if let AppEvent::InsertHistoryCell(cell) = event {            rendered_cells.push(lines_to_single_string(&cell.display_lines(/*width*/ 120)));        }    }    let rendered = rendered_cells.join("\n");    assert!(app.chat_widget.side_conversation_active());    assert_eq!(rendered.matches("sentry is not logged in").count(), 1);    assert_eq!(        rendered            .matches("MCP startup incomplete (failed: sentry)")            .count(),        1    );}#[tokio::test]async fn side_restore_user_message_puts_inline_question_back_in_composer() {    let mut app = make_test_app().await;    let user_message = crate::chatwidget::UserMessage::from("side question");    app.restore_side_user_message(Some(user_message));    assert_eq!(        app.chat_widget.composer_text_with_pending(),        "side question"    );}#[tokio::test]async fn side_discard_selection_keeps_current_side_thread() {    let mut app = make_test_app().await;    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    app.active_thread_id = Some(side_thread_id);    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    assert_eq!(        app.side_thread_to_discard_after_switch(side_thread_id),        None    );    assert_eq!(        app.side_thread_to_discard_after_switch(parent_thread_id),        Some(side_thread_id)    );}#[tokio::test]async fn discard_side_thread_removes_agent_navigation_entry() -> Result<()> {    Box::pin(async {        let mut app = make_test_app().await;        let mut app_server =            crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref()).await?;        let mut side_config = app.chat_widget.config_ref().clone();        side_config.ephemeral = true;        let started = app_server.start_thread(&side_config).await?;        let side_thread_id = started.session.thread_id;        app.side_threads            .insert(side_thread_id, SideThreadState::new(ThreadId::new()));        app.agent_navigation.upsert(            side_thread_id,            Some("Side".to_string()),            Some("side".to_string()),            /*is_closed*/ false,        );        assert!(            app.discard_side_thread(&mut app_server, side_thread_id)                .await        );        assert_eq!(app.agent_navigation.get(&side_thread_id), None);        assert!(!app.side_threads.contains_key(&side_thread_id));        Ok(())    })    .await}#[tokio::test]async fn discard_side_thread_keeps_local_state_when_server_close_fails() -> Result<()> {    Box::pin(async {        let mut app = make_test_app().await;        let mut app_server =            crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref()).await?;        let parent_thread_id = ThreadId::new();        let side_thread_id = ThreadId::new();        app.active_thread_id = Some(side_thread_id);        app.side_threads            .insert(side_thread_id, SideThreadState::new(parent_thread_id));        app.agent_navigation.upsert(            side_thread_id,            Some("Side".to_string()),            Some("side".to_string()),            /*is_closed*/ false,        );        assert!(            !app.discard_side_thread(&mut app_server, side_thread_id)                .await        );        assert_eq!(app.active_thread_id, Some(side_thread_id));        assert_eq!(            app.side_threads                .get(&side_thread_id)                .map(|state| state.parent_thread_id),            Some(parent_thread_id)        );        assert!(app.agent_navigation.get(&side_thread_id).is_some());        Ok(())    })    .await}#[tokio::test]async fn discard_closed_side_thread_removes_local_state_without_server_rpc() {    let mut app = make_test_app().await;    let parent_thread_id = ThreadId::new();    let side_thread_id = ThreadId::new();    app.active_thread_id = Some(side_thread_id);    app.side_threads        .insert(side_thread_id, SideThreadState::new(parent_thread_id));    app.thread_event_channels        .insert(side_thread_id, ThreadEventChannel::new(/*capacity*/ 4));    app.agent_navigation.upsert(        side_thread_id,        Some("Side".to_string()),        Some("side".to_string()),        /*is_closed*/ false,    );    app.discard_closed_side_thread(side_thread_id).await;    assert_eq!(app.active_thread_id, None);    assert!(!app.side_threads.contains_key(&side_thread_id));    assert!(!app.thread_event_channels.contains_key(&side_thread_id));    assert_eq!(app.agent_navigation.get(&side_thread_id), None);}#[tokio::test]async fn active_non_primary_shutdown_target_returns_none_for_non_shutdown_event() -> Result<()> {    let mut app = make_test_app().await;    app.active_thread_id = Some(ThreadId::new());    app.primary_thread_id = Some(ThreadId::new());    assert_eq!(        app.active_non_primary_shutdown_target(&ServerNotification::SkillsChanged(            codex_app_server_protocol::SkillsChangedNotification {},        )),        None    );    Ok(())}#[tokio::test]async fn active_non_primary_shutdown_target_returns_none_for_primary_thread_shutdown() -> Result<()>{    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    app.active_thread_id = Some(thread_id);    app.primary_thread_id = Some(thread_id);    assert_eq!(        app.active_non_primary_shutdown_target(&thread_closed_notification(thread_id)),        None    );    Ok(())}#[tokio::test]async fn active_non_primary_shutdown_target_returns_ids_for_non_primary_shutdown() -> Result<()> {    let mut app = make_test_app().await;    let active_thread_id = ThreadId::new();    let primary_thread_id = ThreadId::new();    app.active_thread_id = Some(active_thread_id);    app.primary_thread_id = Some(primary_thread_id);    assert_eq!(        app.active_non_primary_shutdown_target(&thread_closed_notification(active_thread_id)),        Some((active_thread_id, primary_thread_id))    );    Ok(())}#[tokio::test]async fn active_non_primary_shutdown_target_returns_none_when_shutdown_exit_is_pending()-> Result<()> {    let mut app = make_test_app().await;    let active_thread_id = ThreadId::new();    let primary_thread_id = ThreadId::new();    app.active_thread_id = Some(active_thread_id);    app.primary_thread_id = Some(primary_thread_id);    app.pending_shutdown_exit_thread_id = Some(active_thread_id);    assert_eq!(        app.active_non_primary_shutdown_target(&thread_closed_notification(active_thread_id)),        None    );    Ok(())}#[tokio::test]async fn active_non_primary_shutdown_target_still_switches_for_other_pending_exit_thread()-> Result<()> {    let mut app = make_test_app().await;    let active_thread_id = ThreadId::new();    let primary_thread_id = ThreadId::new();    app.active_thread_id = Some(active_thread_id);    app.primary_thread_id = Some(primary_thread_id);    app.pending_shutdown_exit_thread_id = Some(ThreadId::new());    assert_eq!(        app.active_non_primary_shutdown_target(&thread_closed_notification(active_thread_id)),        Some((active_thread_id, primary_thread_id))    );    Ok(())}async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {    let mut app = make_test_app().await;    app.config.cwd = test_path_buf("/tmp/project").abs();    app.chat_widget.set_model("gpt-test");    app.chat_widget        .set_reasoning_effort(Some(ReasoningEffortConfig::High));    let story_part_one = "In the cliffside town of Bracken Ferry, the lighthouse had been dark for \            nineteen years, and the children were told it was because the sea no longer wanted a \            guide. Mara, who repaired clocks for a living, found that hard to believe. Every dawn she \            heard the gulls circling the empty tower, and every dusk she watched ships hesitate at the \            mouth of the bay as if listening for a signal that never came. When an old brass key fell \            out of a cracked parcel in her workshop, tagged only with the words 'for the lamp room,' \            she decided to climb the hill and see what the town had forgotten.";    let story_part_two = "Inside the lighthouse she found gears wrapped in oilcloth, logbooks filled \            with weather notes, and a lens shrouded beneath salt-stiff canvas. The mechanism was not \            broken, only unfinished. Someone had removed the governor spring and hidden it in a false \            drawer, along with a letter from the last keeper admitting he had darkened the light on \            purpose after smugglers threatened his family. Mara spent the night rebuilding the clockwork \            from spare watch parts, her fingers blackened with soot and grease, while a storm gathered \            over the water and the harbor bells began to ring.";    let story_part_three = "At midnight the first squall hit, and the fishing boats returned early, \            blind in sheets of rain. Mara wound the mechanism, set the teeth by hand, and watched the \            great lens begin to turn in slow, certain arcs. The beam swept across the bay, caught the \            whitecaps, and reached the boats just as they were drifting toward the rocks below the \            eastern cliffs. In the morning the town square was crowded with wet sailors, angry elders, \            and wide-eyed children, but when the oldest captain placed the keeper's log on the fountain \            and thanked Mara for relighting the coast, nobody argued. By sunset, Bracken Ferry had a \            lighthouse again, and Mara had more clocks to mend than ever because everyone wanted \            something in town to keep better time.";    let user_cell = |text: &str| -> Arc<dyn HistoryCell> {        Arc::new(UserHistoryCell {            message: text.to_string(),            text_elements: Vec::new(),            local_image_paths: Vec::new(),            remote_image_urls: Vec::new(),        }) as Arc<dyn HistoryCell>    };    let agent_cell = |text: &str| -> Arc<dyn HistoryCell> {        Arc::new(AgentMessageCell::new(            vec![Line::from(text.to_string())],            /*is_first_line*/ true,        )) as Arc<dyn HistoryCell>    };    let make_header = |is_first| -> Arc<dyn HistoryCell> {        let session = ThreadSessionState {            thread_id: ThreadId::new(),            forked_from_id: None,            fork_parent_title: None,            thread_name: None,            model: "gpt-test".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("/tmp/project").abs(),            runtime_workspace_roots: Vec::new(),            instruction_source_paths: Vec::new(),            reasoning_effort: Some(ReasoningEffortConfig::High),            collaboration_mode: None,            personality: None,            message_history: None,            network_proxy: None,            rollout_path: Some(PathBuf::new()),        };        Arc::new(new_session_info(            app.chat_widget.config_ref(),            app.chat_widget.current_model(),            &session,            is_first,            /*tooltip_override*/ None,            /*auth_plan*/ None,            /*show_fast_status*/ false,        )) as Arc<dyn HistoryCell>    };    app.transcript_cells = vec![        make_header(true),        Arc::new(crate::history_cell::new_info_event(            "startup tip that used to replay".to_string(),            /*hint*/ None,        )) as Arc<dyn HistoryCell>,        user_cell("Tell me a long story about a town with a dark lighthouse."),        agent_cell(story_part_one),        user_cell("Continue the story and reveal why the light went out."),        agent_cell(story_part_two),        user_cell("Finish the story with a storm and a resolution."),        agent_cell(story_part_three),    ];    app.has_emitted_history_lines = true;    let rendered = app        .clear_ui_header_lines_with_version(/*width*/ 80, "<VERSION>")        .iter()        .map(|line| {            line.spans                .iter()                .map(|span| span.content.as_ref())                .collect::<String>()        })        .collect::<Vec<_>>()        .join("\n");    assert!(        !rendered.contains("startup tip that used to replay"),        "clear header should not replay startup notices"    );    assert!(        !rendered.contains("Bracken Ferry"),        "clear header should not replay prior conversation turns"    );    rendered}#[tokio::test]#[cfg_attr(    target_os = "windows",    ignore = "snapshot path rendering differs on Windows")]async fn clear_ui_after_long_transcript_snapshots_fresh_header_only() {    let rendered = render_clear_ui_header_after_long_transcript_for_snapshot().await;    assert_app_snapshot!("clear_ui_after_long_transcript_fresh_header_only", rendered);}#[tokio::test]#[cfg_attr(    target_os = "windows",    ignore = "snapshot path rendering differs on Windows")]async fn ctrl_l_clear_ui_after_long_transcript_reuses_clear_header_snapshot() {    let rendered = render_clear_ui_header_after_long_transcript_for_snapshot().await;    assert_app_snapshot!("clear_ui_after_long_transcript_fresh_header_only", rendered);}#[tokio::test]#[cfg_attr(    target_os = "windows",    ignore = "snapshot path rendering differs on Windows")]async fn clear_ui_header_shows_fast_status_for_fast_capable_models() {    let mut app = make_test_app().await;    app.config.cwd = test_path_buf("/tmp/project").abs();    app.chat_widget.set_model("gpt-5.4");    set_fast_mode_test_catalog(&mut app.chat_widget);    app.chat_widget        .set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));    app.chat_widget.set_service_tier(Some(        codex_protocol::config_types::ServiceTier::Fast            .request_value()            .to_string(),    ));    set_chatgpt_auth(&mut app.chat_widget);    set_fast_mode_test_catalog(&mut app.chat_widget);    let rendered = app        .clear_ui_header_lines_with_version(/*width*/ 80, "<VERSION>")        .iter()        .map(|line| {            line.spans                .iter()                .map(|span| span.content.as_ref())                .collect::<String>()        })        .collect::<Vec<_>>()        .join("\n");    assert_app_snapshot!("clear_ui_header_fast_status_fast_capable_models", rendered);}async fn make_test_app() -> App {    let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await;    let config = chat_widget.config_ref().clone();    let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone());    let model = get_model_offline_for_tests(config.model.as_deref());    let session_telemetry = test_session_telemetry(&config, model.as_str());    App {        model_catalog: chat_widget.model_catalog(),        session_telemetry,        app_event_tx,        chat_widget,        workspace_command_runner: None,        config,        state_db: None,        cli_kv_overrides: Vec::new(),        harness_overrides: ConfigOverrides::default(),        loader_overrides: LoaderOverrides::without_managed_config_for_tests(),        cloud_config_bundle: CloudConfigBundleLoader::default(),        runtime_approval_policy_override: None,        runtime_permission_profile_override: None,        file_search,        transcript_cells: Vec::new(),        overlay: None,        deferred_history_lines: Vec::new(),        has_emitted_history_lines: false,        transcript_reflow: TranscriptReflowState::default(),        initial_history_replay_buffer: None,        enhanced_keys_supported: false,        keymap: crate::keymap::RuntimeKeymap::defaults(),        commit_anim_running: Arc::new(AtomicBool::new(false)),        status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),        terminal_title_invalid_items_warned: Arc::new(AtomicBool::new(false)),        skill_load_warnings: SkillLoadWarningState::default(),        backtrack: BacktrackState::default(),        backtrack_render_pending: false,        feedback: codex_feedback::CodexFeedback::new(),        feedback_audience: FeedbackAudience::External,        environment_manager: Arc::new(EnvironmentManager::default_for_tests()),        app_server_target: crate::AppServerTarget::Embedded,        pending_update_action: None,        pending_shutdown_exit_thread_id: None,        windows_sandbox: WindowsSandboxState::default(),        thread_event_channels: HashMap::new(),        thread_event_listener_tasks: HashMap::new(),        agent_navigation: AgentNavigationState::default(),        side_threads: HashMap::new(),        active_thread_id: None,        active_thread_rx: None,        primary_thread_id: None,        last_subagent_backfill_attempt: None,        primary_session_configured: None,        pending_primary_events: VecDeque::new(),        pending_app_server_requests: PendingAppServerRequests::default(),        pending_startup_thread_start: false,        pending_plugin_enabled_writes: HashMap::new(),        pending_hook_enabled_writes: HashMap::new(),    }}async fn make_test_app_with_channels() -> (    App,    tokio::sync::mpsc::UnboundedReceiver<AppEvent>,    tokio::sync::mpsc::UnboundedReceiver<Op>,) {    let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await;    let config = chat_widget.config_ref().clone();    let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone());    let model = get_model_offline_for_tests(config.model.as_deref());    let session_telemetry = test_session_telemetry(&config, model.as_str());    (        App {            model_catalog: chat_widget.model_catalog(),            session_telemetry,            app_event_tx,            chat_widget,            workspace_command_runner: None,            config,            state_db: None,            cli_kv_overrides: Vec::new(),            harness_overrides: ConfigOverrides::default(),            loader_overrides: LoaderOverrides::without_managed_config_for_tests(),            cloud_config_bundle: CloudConfigBundleLoader::default(),            runtime_approval_policy_override: None,            runtime_permission_profile_override: None,            file_search,            transcript_cells: Vec::new(),            overlay: None,            deferred_history_lines: Vec::new(),            has_emitted_history_lines: false,            transcript_reflow: TranscriptReflowState::default(),            initial_history_replay_buffer: None,            enhanced_keys_supported: false,            keymap: crate::keymap::RuntimeKeymap::defaults(),            commit_anim_running: Arc::new(AtomicBool::new(false)),            status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),            terminal_title_invalid_items_warned: Arc::new(AtomicBool::new(false)),            skill_load_warnings: SkillLoadWarningState::default(),            backtrack: BacktrackState::default(),            backtrack_render_pending: false,            feedback: codex_feedback::CodexFeedback::new(),            feedback_audience: FeedbackAudience::External,            environment_manager: Arc::new(EnvironmentManager::default_for_tests()),            app_server_target: crate::AppServerTarget::Embedded,            pending_update_action: None,            pending_shutdown_exit_thread_id: None,            windows_sandbox: WindowsSandboxState::default(),            thread_event_channels: HashMap::new(),            thread_event_listener_tasks: HashMap::new(),            agent_navigation: AgentNavigationState::default(),            side_threads: HashMap::new(),            active_thread_id: None,            active_thread_rx: None,            primary_thread_id: None,            last_subagent_backfill_attempt: None,            primary_session_configured: None,            pending_primary_events: VecDeque::new(),            pending_app_server_requests: PendingAppServerRequests::default(),            pending_startup_thread_start: false,            pending_plugin_enabled_writes: HashMap::new(),            pending_hook_enabled_writes: HashMap::new(),        },        rx,        op_rx,    )}#[tokio::test]async fn set_thread_goal_draft_materializes_long_objective_and_confirms_before_paste() -> Result<()>{    let mut app = make_test_app().await;    let mut app_server =        crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref()).await?;    let started = app_server        .start_thread(app.chat_widget.config_ref())        .await?;    let thread_id = started.session.thread_id;    app.enqueue_primary_thread_session(started.session, started.turns)        .await?;    let objective = "x".repeat(MAX_THREAD_GOAL_OBJECTIVE_CHARS + 1);    app.set_thread_goal_draft(        &mut app_server,        thread_id,        crate::goal_files::GoalDraft {            objective: objective.clone(),            ..Default::default()        },        crate::app_event::ThreadGoalSetMode::ConfirmIfExists,    )    .await;    let response = app_server.thread_goal_get(thread_id).await?;    let goal = response.goal.expect("goal should be set");    let saved_objective = goal.objective.clone();    let codex_home = app_server        .codex_home_path(&app.chat_widget.config_ref().codex_home)        .expect("codex home");    assert!(goal_files::objective_file_path(&goal.objective, Some(&codex_home)).is_some());    assert_eq!(        goal_files::objective_text_for_edit(&mut app_server, Some(&codex_home), &goal.objective)            .await            .expect("managed goal file should be readable"),        objective    );    let is_managed = |home: &AppServerPath, path: &str| {        let reference = goal_files::objective_file_reference(&AppServerPath::from_app_server(path))            .expect("goal objective reference");        goal_files::objective_file_path(&reference, Some(home)).is_some()    };    let suffix = "attachments/00000000-0000-4000-8000-000000000000/goal-objective.md";    for path in [        format!("/tmp/{suffix}"),        format!("{codex_home}/../other/{suffix}"),        format!("{codex_home}/other/{suffix}"),    ] {        assert!(!is_managed(&codex_home, &path));    }    assert!(!is_managed(        &AppServerPath::from_app_server("/tmp/codex\\home"),        &format!("/tmp/codex/home/{suffix}")    ));    let unix_path = AppServerPath::from_app_server("/tmp/codex\\").join("a");    assert_eq!(unix_path.as_str(), "/tmp/codex\\/a");    let attachments_dir = app.chat_widget.config_ref().codex_home.join("attachments");    let attachment_count = std::fs::read_dir(&attachments_dir)?.count();    let placeholder = "[Pasted Content 5 chars]";    let paste_draft = crate::goal_files::GoalDraft {        objective: format!("Use {placeholder}"),        text_elements: vec![TextElement::new(            (4..4 + placeholder.len()).into(),            Some(placeholder.to_string()),        )],        pending_pastes: vec![(placeholder.to_string(), "hello".to_string())],        ..Default::default()    };    app.set_thread_goal_draft(        &mut app_server,        thread_id,        paste_draft.clone(),        crate::app_event::ThreadGoalSetMode::ConfirmIfExists,    )    .await;    assert_eq!(        std::fs::read_dir(&attachments_dir)?.count(),        attachment_count    );    assert_eq!(        app_server            .thread_goal_get(thread_id)            .await?            .goal            .expect("goal should still be set")            .objective,        saved_objective    );    app.set_thread_goal_draft(        &mut app_server,        thread_id,        paste_draft,        crate::app_event::ThreadGoalSetMode::ReplaceExisting,    )    .await;    let goal = app_server        .thread_goal_get(thread_id)        .await?        .goal        .expect("replacement goal should be set");    let paste_path = goal        .objective        .strip_prefix("Use pasted text file: ")        .and_then(|text| text.strip_suffix(". Read this file before continuing."))        .expect("paste file reference");    assert_eq!(std::fs::read_to_string(paste_path)?, "hello");    let attachment_count = std::fs::read_dir(&attachments_dir)?.count();    let stale_paste = (placeholder.to_string(), "hello".to_string());    app.set_thread_goal_draft(        &mut app_server,        thread_id,        crate::goal_files::GoalDraft {            objective: "small goal".to_string(),            pending_pastes: vec![stale_paste],            ..Default::default()        },        crate::app_event::ThreadGoalSetMode::ReplaceExisting,    )    .await;    assert_eq!(        std::fs::read_dir(&attachments_dir)?.count(),        attachment_count    );    let whitespace_placeholder = "[Pasted Content 3 chars]";    app.set_thread_goal_draft(        &mut app_server,        thread_id,        crate::goal_files::GoalDraft {            objective: whitespace_placeholder.to_string(),            text_elements: vec![TextElement::new(                (0..whitespace_placeholder.len()).into(),                Some(whitespace_placeholder.to_string()),            )],            pending_pastes: vec![(whitespace_placeholder.to_string(), " \n\t".to_string())],            ..Default::default()        },        crate::app_event::ThreadGoalSetMode::ReplaceExisting,    )    .await;    assert_eq!(        std::fs::read_dir(&attachments_dir)?.count(),        attachment_count    );    assert_eq!(        app_server            .thread_goal_get(thread_id)            .await?            .goal            .expect("small goal should remain set")            .objective,        "small goal"    );    let image_dir = tempfile::tempdir()?;    let image_path = image_dir.path().join("local-image.png");    std::fs::write(&image_path, b"png bytes")?;    let image_placeholder = "[Image #3]";    app.set_thread_goal_draft(        &mut app_server,        thread_id,        crate::goal_files::GoalDraft {            objective: format!("Describe {image_placeholder}"),            text_elements: vec![TextElement::new(                (9..9 + image_placeholder.len()).into(),                Some(image_placeholder.to_string()),            )],            local_images: vec![crate::bottom_pane::LocalImageAttachment {                placeholder: image_placeholder.to_string(),                path: image_path,            }],            remote_image_urls: vec![                "https://example.com/first.png".to_string(),                "https://example.com/second.png".to_string(),            ],            ..Default::default()        },        crate::app_event::ThreadGoalSetMode::ReplaceExisting,    )    .await;    let objective = app_server        .thread_goal_get(thread_id)        .await?        .goal        .expect("image goal should be set")        .objective;    let copied_image = objective        .strip_prefix("Describe image file: ")        .and_then(|text| text.split_once("\n\n"))        .map(|(path, _)| path)        .expect("copied image path");    assert_eq!(std::fs::read(copied_image)?, b"png bytes");    assert!(objective.contains(        "Referenced image URLs:\n- [Image #1]: https://example.com/first.png\n- [Image #2]: https://example.com/second.png"    ));    app_server.shutdown().await?;    Ok(())}#[tokio::test]async fn replace_goal_confirmation_snapshot() {    let mut app = make_test_app().await;    app.show_replace_thread_goal_confirmation(        ThreadId::new(),        goal_files::GoalDraft {            objective: "New goal".to_string(),            ..Default::default()        },    );    assert_app_snapshot!(        "replace_goal_confirmation",        render_bottom_popup(&app.chat_widget, /*width*/ 80)    );}fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState {    ThreadSessionState {        thread_id,        forked_from_id: None,        fork_parent_title: None,        thread_name: None,        model: "gpt-test".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: cwd.abs(),        runtime_workspace_roots: Vec::new(),        instruction_source_paths: Vec::new(),        reasoning_effort: None,        collaboration_mode: None,        personality: None,        message_history: None,        network_proxy: None,        rollout_path: Some(PathBuf::new()),    }}fn plain_line_cell(text: impl Into<String>) -> Arc<dyn HistoryCell> {    Arc::new(PlainHistoryCell::new(vec![Line::from(text.into())])) as Arc<dyn HistoryCell>}fn rendered_line_text(line: &crate::terminal_hyperlinks::HyperlinkLine) -> String {    line.line        .spans        .iter()        .map(|span| span.content.as_ref())        .collect()}#[tokio::test]async fn capped_resize_reflow_renders_recent_suffix_only() {    let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;    app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(5);    app.transcript_cells = (0..20)        .map(|i| plain_line_cell(format!("cell {i}")))        .collect();    let rendered = app.render_transcript_lines_for_reflow(/*width*/ 80);    assert_eq!(rendered.lines.len(), 5);    assert_eq!(        rendered            .lines            .iter()            .map(rendered_line_text)            .collect::<Vec<_>>(),        vec![            "cell 17".to_string(),            String::new(),            "cell 18".to_string(),            String::new(),            "cell 19".to_string(),        ]    );}#[tokio::test]async fn uncapped_resize_reflow_renders_all_cells_when_row_cap_absent() {    let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;    app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Disabled;    app.transcript_cells = (0..20)        .map(|i| plain_line_cell(format!("cell {i}")))        .collect();    let rendered = app.render_transcript_lines_for_reflow(/*width*/ 80);    assert_eq!(rendered.lines.len(), 39);    assert_eq!(rendered_line_text(&rendered.lines[0]), "cell 0");    assert_eq!(rendered_line_text(&rendered.lines[38]), "cell 19");}#[tokio::test]async fn resize_reflow_wraps_transcript_early_when_pet_is_enabled() {    let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;    app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Disabled;    app.transcript_cells = vec![Arc::new(AgentMarkdownCell::new(        "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda".to_string(),        Path::new("/tmp"),    ))];    let without_pet = app.render_transcript_lines_for_reflow(/*width*/ 40);    app.chat_widget        .set_pet_image_support_for_tests(crate::pets::PetImageSupport::Supported(            crate::pets::ImageProtocol::Kitty,        ));    app.chat_widget        .install_test_ambient_pet_for_tests(/*animations_enabled*/ false);    let width = app.chat_widget.history_wrap_width(/*width*/ 40);    assert!(width < 40);    let with_pet = app.render_transcript_lines_for_reflow(width);    assert!(        with_pet.lines.len() > without_pet.lines.len(),        "expected pet-enabled transcript reflow to wrap earlier"    );}#[tokio::test]async fn uncapped_resize_reflow_renders_all_cells_under_row_limit() {    let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;    app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(100);    app.transcript_cells = (0..3)        .map(|i| plain_line_cell(format!("cell {i}")))        .collect();    let rendered = app.render_transcript_lines_for_reflow(/*width*/ 80);    assert_eq!(        rendered            .lines            .iter()            .map(rendered_line_text)            .collect::<Vec<_>>(),        vec![            "cell 0".to_string(),            String::new(),            "cell 1".to_string(),            String::new(),            "cell 2".to_string(),        ]    );}#[tokio::test]async fn initial_replay_buffer_keeps_recent_rows_when_row_cap_present() {    let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;    app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(3);    app.begin_initial_history_replay_buffer();    for index in 0..5 {        App::buffer_initial_history_replay_display_lines(            app.initial_history_replay_buffer                .as_mut()                .expect("initial replay buffer active"),            vec![Line::from(format!("line {index}")).into()],            /*max_rows*/ 3,        );    }    let buffer = app        .initial_history_replay_buffer        .as_ref()        .expect("initial replay buffer should remain active");    assert_eq!(        buffer            .retained_lines            .iter()            .map(rendered_line_text)            .collect::<Vec<_>>(),        vec![            "line 2".to_string(),            "line 3".to_string(),            "line 4".to_string(),        ]    );}#[tokio::test]async fn thread_switch_replay_buffer_uses_transcript_tail_mode_when_row_cap_present() {    let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;    app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(3);    app.begin_thread_switch_history_replay_buffer();    let buffer = app        .initial_history_replay_buffer        .as_ref()        .expect("thread switch replay buffer should be active");    assert!(buffer.render_from_transcript_tail);    assert!(buffer.retained_lines.is_empty());}#[tokio::test]async fn thread_switch_replay_buffer_is_disabled_without_row_cap() {    let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;    app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Disabled;    app.begin_thread_switch_history_replay_buffer();    assert!(app.initial_history_replay_buffer.is_none());}#[tokio::test]async fn height_shrink_schedules_resize_reflow() {    let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;    let frame_requester = crate::tui::FrameRequester::test_dummy();    assert!(!app.handle_draw_size_change(        ratatui::layout::Size::new(/*width*/ 118, /*height*/ 35),        ratatui::layout::Size::new(/*width*/ 118, /*height*/ 35),        &frame_requester,    ));    assert!(app.handle_draw_size_change(        ratatui::layout::Size::new(/*width*/ 118, /*height*/ 24),        ratatui::layout::Size::new(/*width*/ 118, /*height*/ 35),        &frame_requester,    ));    assert!(app.transcript_reflow.has_pending_reflow());}fn test_turn(turn_id: &str, status: TurnStatus, items: Vec<ThreadItem>) -> Turn {    Turn {        id: turn_id.to_string(),        items_view: codex_app_server_protocol::TurnItemsView::Full,        items,        status,        error: None,        started_at: None,        completed_at: None,        duration_ms: None,    }}fn turn_started_notification(thread_id: ThreadId, turn_id: &str) -> ServerNotification {    ServerNotification::TurnStarted(TurnStartedNotification {        thread_id: thread_id.to_string(),        turn: Turn {            started_at: Some(0),            ..test_turn(turn_id, TurnStatus::InProgress, Vec::new())        },    })}fn turn_completed_notification(    thread_id: ThreadId,    turn_id: &str,    status: TurnStatus,) -> ServerNotification {    ServerNotification::TurnCompleted(TurnCompletedNotification {        thread_id: thread_id.to_string(),        turn: Turn {            completed_at: Some(0),            duration_ms: Some(1),            ..test_turn(turn_id, status, Vec::new())        },    })}fn thread_closed_notification(thread_id: ThreadId) -> ServerNotification {    ServerNotification::ThreadClosed(ThreadClosedNotification {        thread_id: thread_id.to_string(),    })}fn token_usage_notification(    thread_id: ThreadId,    turn_id: &str,    model_context_window: Option<i64>,) -> ServerNotification {    ServerNotification::ThreadTokenUsageUpdated(ThreadTokenUsageUpdatedNotification {        thread_id: thread_id.to_string(),        turn_id: turn_id.to_string(),        token_usage: ThreadTokenUsage {            total: TokenUsageBreakdown {                total_tokens: 10,                input_tokens: 4,                cached_input_tokens: 1,                output_tokens: 5,                reasoning_output_tokens: 0,            },            last: TokenUsageBreakdown {                total_tokens: 10,                input_tokens: 4,                cached_input_tokens: 1,                output_tokens: 5,                reasoning_output_tokens: 0,            },            model_context_window,        },    })}fn agent_message_delta_notification(    thread_id: ThreadId,    turn_id: &str,    item_id: &str,    delta: &str,) -> ServerNotification {    ServerNotification::AgentMessageDelta(AgentMessageDeltaNotification {        thread_id: thread_id.to_string(),        turn_id: turn_id.to_string(),        item_id: item_id.to_string(),        delta: delta.to_string(),    })}fn exec_approval_request(    thread_id: ThreadId,    turn_id: &str,    item_id: &str,    approval_id: Option<&str>,) -> ServerRequest {    ServerRequest::CommandExecutionRequestApproval {        request_id: AppServerRequestId::Integer(1),        params: CommandExecutionRequestApprovalParams {            thread_id: thread_id.to_string(),            turn_id: turn_id.to_string(),            item_id: item_id.to_string(),            started_at_ms: 0,            approval_id: approval_id.map(str::to_string),            reason: Some("needs approval".to_string()),            network_approval_context: None,            command: Some("echo hello".to_string()),            cwd: Some(test_path_buf("/tmp/project").abs()),            command_actions: None,            additional_permissions: None,            proposed_execpolicy_amendment: None,            proposed_network_policy_amendments: None,            available_decisions: None,        },    }}fn request_user_input_request(thread_id: ThreadId, turn_id: &str, item_id: &str) -> ServerRequest {    ServerRequest::ToolRequestUserInput {        request_id: AppServerRequestId::Integer(2),        params: ToolRequestUserInputParams {            thread_id: thread_id.to_string(),            turn_id: turn_id.to_string(),            item_id: item_id.to_string(),            questions: Vec::new(),            auto_resolution_ms: None,        },    }}#[tokio::test]async fn feedback_submission_without_thread_emits_error_history_cell() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    app.handle_feedback_submitted(        /*origin_thread_id*/ None,        FeedbackCategory::Bug,        /*include_logs*/ true,        Err("boom".to_string()),    )    .await;    let cell = match app_event_rx.try_recv() {        Ok(AppEvent::InsertHistoryCell(cell)) => cell,        other => panic!("expected feedback error history cell, saw {other:?}"),    };    assert_eq!(        lines_to_single_string(&cell.display_lines(/*width*/ 120)),        "■ Failed to upload feedback: boom"    );}#[tokio::test]async fn feedback_submission_for_inactive_thread_replays_into_origin_thread() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let origin_thread_id = ThreadId::new();    let active_thread_id = ThreadId::new();    let origin_session = test_thread_session(origin_thread_id, test_path_buf("/tmp/origin"));    let active_session = test_thread_session(active_thread_id, test_path_buf("/tmp/active"));    app.thread_event_channels.insert(        origin_thread_id,        ThreadEventChannel::new_with_session(            THREAD_EVENT_CHANNEL_CAPACITY,            origin_session.clone(),            Vec::new(),        ),    );    app.thread_event_channels.insert(        active_thread_id,        ThreadEventChannel::new_with_session(            THREAD_EVENT_CHANNEL_CAPACITY,            active_session.clone(),            Vec::new(),        ),    );    app.activate_thread_channel(active_thread_id).await;    app.chat_widget.handle_thread_session(active_session);    while app_event_rx.try_recv().is_ok() {}    app.handle_feedback_submitted(        Some(origin_thread_id),        FeedbackCategory::Bug,        /*include_logs*/ true,        Ok("uploaded-thread".to_string()),    )    .await;    assert_matches!(        app_event_rx.try_recv(),        Err(tokio::sync::mpsc::error::TryRecvError::Empty)    );    let snapshot = {        let channel = app            .thread_event_channels            .get(&origin_thread_id)            .expect("origin thread channel should exist");        let store = channel.store.lock().await;        assert!(matches!(            store.buffer.back(),            Some(ThreadBufferedEvent::FeedbackSubmission(_))        ));        store.snapshot()    };    app.replay_thread_snapshot(snapshot, /*resume_restored_queue*/ false);    let mut rendered_cells = Vec::new();    while let Ok(event) = app_event_rx.try_recv() {        if let AppEvent::InsertHistoryCell(cell) = event {            rendered_cells.push(lines_to_single_string(&cell.display_lines(/*width*/ 120)));        }    }    assert!(rendered_cells.iter().any(|cell| {        cell.contains("• Feedback uploaded. Please open an issue using the following URL:")            && cell.contains("uploaded-thread")    }));}fn next_user_turn_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) -> Op {    let mut seen = Vec::new();    while let Ok(op) = op_rx.try_recv() {        if matches!(op, Op::UserTurn { .. }) {            return op;        }        seen.push(format!("{op:?}"));    }    panic!("expected UserTurn op, saw: {seen:?}");}fn lines_to_single_string(lines: &[Line<'_>]) -> String {    lines        .iter()        .map(|line| {            line.spans                .iter()                .map(|span| span.content.as_ref())                .collect::<String>()        })        .collect::<Vec<_>>()        .join("\n")}fn test_session_telemetry(config: &Config, model: &str) -> SessionTelemetry {    let model_info =        construct_model_info_offline_for_tests(model, &config.to_models_manager_config());    SessionTelemetry::new(        ThreadId::new(),        model,        model_info.slug.as_str(),        /*account_id*/ None,        /*account_email*/ None,        /*auth_mode*/ None,        "test_originator".to_string(),        /*log_user_prompts*/ false,        "test".to_string(),        crate::test_support::session_source_cli(),    )}#[test]fn active_turn_not_steerable_turn_error_extracts_structured_server_error() {    let turn_error = AppServerTurnError {        message: "cannot steer a review turn".to_string(),        codex_error_info: Some(AppServerCodexErrorInfo::ActiveTurnNotSteerable {            turn_kind: AppServerNonSteerableTurnKind::Review,        }),        additional_details: None,    };    let error = TypedRequestError::Server {        method: "turn/steer".to_string(),        source: JSONRPCErrorError {            code: -32602,            message: turn_error.message.clone(),            data: Some(serde_json::to_value(&turn_error).expect("turn error should serialize")),        },    };    assert_eq!(        active_turn_not_steerable_turn_error(&error),        Some(turn_error)    );}#[test]fn session_start_error_surfaces_archived_guidance_without_rollout_path() {    let thread_id =        ThreadId::from_string("019e72f4-e09a-70f2-b2c2-a153a57b8cc0").expect("thread id");    let target_session = SessionTarget {        path: Some(std::path::PathBuf::from(            "/Users/me/.codex/archived_sessions/rollout.jsonl",        )),        thread_id,    };    let expected = format!(        "session {thread_id} is archived. Run `codex unarchive {thread_id}` to unarchive it first."    );    for action in ["resume", "fork"] {        let err = color_eyre::eyre::eyre!(            "thread/{action} failed during TUI bootstrap: thread/{action} failed: {expected} (code -32600)"        );        assert_eq!(            session_start_error(action, &target_session, err).to_string(),            expected        );    }}#[test]fn active_turn_steer_race_detects_missing_active_turn() {    let error = TypedRequestError::Server {        method: "turn/steer".to_string(),        source: JSONRPCErrorError {            code: -32602,            message: "no active turn to steer".to_string(),            data: None,        },    };    assert_eq!(        active_turn_steer_race(&error),        Some(ActiveTurnSteerRace::Missing)    );    assert_eq!(active_turn_not_steerable_turn_error(&error), None);}#[test]fn active_turn_steer_race_extracts_actual_turn_id_from_mismatch() {    let error = TypedRequestError::Server {        method: "turn/steer".to_string(),        source: JSONRPCErrorError {            code: -32602,            message: "expected active turn id `turn-expected` but found `turn-actual`".to_string(),            data: None,        },    };    assert_eq!(        active_turn_steer_race(&error),        Some(ActiveTurnSteerRace::ExpectedTurnMismatch {            actual_turn_id: "turn-actual".to_string(),        })    );}#[test]fn active_turn_interrupt_race_extracts_actual_turn_id_from_mismatch() {    let error = TypedRequestError::Server {        method: "turn/interrupt".to_string(),        source: JSONRPCErrorError {            code: -32602,            message: "expected active turn id turn-expected but found turn-actual".to_string(),            data: None,        },    };    assert_eq!(        active_turn_interrupt_race(&error),        Some("turn-actual".to_string())    );}#[tokio::test]async fn fresh_session_config_uses_current_service_tier() {    let mut app = make_test_app().await;    app.chat_widget.set_service_tier(Some(        codex_protocol::config_types::ServiceTier::Fast            .request_value()            .to_string(),    ));    let config = app.fresh_session_config();    assert_eq!(        config.service_tier,        Some(            codex_protocol::config_types::ServiceTier::Fast                .request_value()                .to_string()        )    );}#[tokio::test]async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {    let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;    let user_cell = |text: &str,                     text_elements: Vec<TextElement>,                     local_image_paths: Vec<PathBuf>,                     remote_image_urls: Vec<String>|     -> Arc<dyn HistoryCell> {        Arc::new(UserHistoryCell {            message: text.to_string(),            text_elements,            local_image_paths,            remote_image_urls,        }) as Arc<dyn HistoryCell>    };    let agent_cell = |text: &str| -> Arc<dyn HistoryCell> {        Arc::new(AgentMessageCell::new(            vec![Line::from(text.to_string())],            /*is_first_line*/ true,        )) as Arc<dyn HistoryCell>    };    let make_header = |is_first| {        let session = ThreadSessionState {            thread_id: ThreadId::new(),            forked_from_id: None,            fork_parent_title: None,            thread_name: None,            model: "gpt-test".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: None,            collaboration_mode: None,            personality: None,            message_history: None,            network_proxy: None,            rollout_path: Some(PathBuf::new()),        };        Arc::new(new_session_info(            app.chat_widget.config_ref(),            app.chat_widget.current_model(),            &session,            is_first,            /*tooltip_override*/ None,            /*auth_plan*/ None,            /*show_fast_status*/ false,        )) as Arc<dyn HistoryCell>    };    let placeholder = "[Image #1]";    let edited_text = format!("follow-up (edited) {placeholder}");    let edited_range = edited_text.len().saturating_sub(placeholder.len())..edited_text.len();    let edited_text_elements = vec![TextElement::new(        edited_range.into(),        /*placeholder*/ None,    )];    let edited_local_image_paths = vec![PathBuf::from("/tmp/fake-image.png")];    // Simulate a transcript with duplicated history (e.g., from prior backtracks)    // and an edited turn appended after a session header boundary.    app.transcript_cells = vec![        make_header(true),        user_cell("first question", Vec::new(), Vec::new(), Vec::new()),        agent_cell("answer first"),        user_cell("follow-up", Vec::new(), Vec::new(), Vec::new()),        agent_cell("answer follow-up"),        make_header(false),        user_cell("first question", Vec::new(), Vec::new(), Vec::new()),        agent_cell("answer first"),        user_cell(            &edited_text,            edited_text_elements.clone(),            edited_local_image_paths.clone(),            vec!["https://example.com/backtrack.png".to_string()],        ),        agent_cell("answer edited"),    ];    assert_eq!(user_count(&app.transcript_cells), 2);    let base_id = ThreadId::new();    app.chat_widget        .handle_thread_session(crate::session_state::ThreadSessionState {            thread_id: base_id,            forked_from_id: None,            fork_parent_title: None,            thread_name: None,            model: "gpt-test".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: None,            collaboration_mode: None,            personality: None,            message_history: None,            network_proxy: None,            rollout_path: Some(PathBuf::new()),        });    app.backtrack.base_id = Some(base_id);    app.backtrack.primed = true;    app.backtrack.nth_user_message = user_count(&app.transcript_cells).saturating_sub(1);    let selection = app        .confirm_backtrack_from_main()        .expect("backtrack selection");    assert_eq!(selection.nth_user_message, 1);    assert_eq!(selection.prefill, edited_text);    assert_eq!(selection.text_elements, edited_text_elements);    assert_eq!(selection.local_image_paths, edited_local_image_paths);    assert_eq!(        selection.remote_image_urls,        vec!["https://example.com/backtrack.png".to_string()]    );    app.apply_backtrack_rollback(selection);    assert_eq!(        app.chat_widget.remote_image_urls(),        vec!["https://example.com/backtrack.png".to_string()]    );    let mut rollback_turns = None;    while let Ok(op) = op_rx.try_recv() {        if let Op::ThreadRollback { num_turns } = op {            rollback_turns = Some(num_turns);        }    }    assert_eq!(rollback_turns, Some(1));}#[tokio::test]async fn backtrack_remote_image_only_selection_clears_existing_composer_draft() {    let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;    app.transcript_cells = vec![Arc::new(UserHistoryCell {        message: "original".to_string(),        text_elements: Vec::new(),        local_image_paths: Vec::new(),        remote_image_urls: Vec::new(),    }) as Arc<dyn HistoryCell>];    app.chat_widget        .set_composer_text("stale draft".to_string(), Vec::new(), Vec::new());    let remote_image_url = "https://example.com/remote-only.png".to_string();    app.apply_backtrack_rollback(BacktrackSelection {        nth_user_message: 0,        prefill: String::new(),        text_elements: Vec::new(),        local_image_paths: Vec::new(),        remote_image_urls: vec![remote_image_url.clone()],    });    assert_eq!(app.chat_widget.composer_text_with_pending(), "");    assert_eq!(app.chat_widget.remote_image_urls(), vec![remote_image_url]);    let mut rollback_turns = None;    while let Ok(op) = op_rx.try_recv() {        if let Op::ThreadRollback { num_turns } = op {            rollback_turns = Some(num_turns);        }    }    assert_eq!(rollback_turns, Some(1));}#[tokio::test]async fn cancelled_turn_edit_restores_prompt_and_rolls_back_latest_turn() {    let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;    app.transcript_cells = vec![Arc::new(UserHistoryCell {        message: "original".to_string(),        text_elements: Vec::new(),        local_image_paths: Vec::new(),        remote_image_urls: Vec::new(),    }) as Arc<dyn HistoryCell>];    let prompt = crate::chatwidget::UserMessage {        text: "edit me".to_string(),        local_images: Vec::new(),        remote_image_urls: vec!["https://example.com/edit.png".to_string()],        text_elements: Vec::new(),        mention_bindings: Vec::new(),    };    app.apply_cancelled_turn_edit(prompt);    assert_eq!(app.chat_widget.composer_text_with_pending(), "edit me");    assert_snapshot!(        "cancelled_turn_edit_restores_composer",        app.chat_widget.composer_text_with_pending()    );    assert_eq!(        app.chat_widget.remote_image_urls(),        vec!["https://example.com/edit.png".to_string()]    );    assert_matches!(op_rx.try_recv(), Ok(Op::ThreadRollback { num_turns: 1 }));}#[tokio::test]async fn first_cancelled_turn_edit_restores_prompt_without_local_history() {    let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;    let prompt = crate::chatwidget::UserMessage {        text: "edit first prompt".to_string(),        local_images: Vec::new(),        remote_image_urls: vec!["https://example.com/edit.png".to_string()],        text_elements: Vec::new(),        mention_bindings: Vec::new(),    };    app.apply_cancelled_turn_edit(prompt);    assert_eq!(        app.chat_widget.composer_text_with_pending(),        "edit first prompt"    );    assert_eq!(        app.chat_widget.remote_image_urls(),        vec!["https://example.com/edit.png".to_string()]    );    assert_matches!(op_rx.try_recv(), Ok(Op::ThreadRollback { num_turns: 1 }));}#[tokio::test]async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {    let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    app.chat_widget        .handle_thread_session(crate::session_state::ThreadSessionState {            thread_id,            forked_from_id: None,            fork_parent_title: None,            thread_name: None,            model: "gpt-test".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: None,            collaboration_mode: None,            personality: None,            message_history: None,            network_proxy: None,            rollout_path: Some(PathBuf::new()),        });    let data_image_url = "data:image/png;base64,abc123".to_string();    app.transcript_cells = vec![Arc::new(UserHistoryCell {        message: "please inspect this".to_string(),        text_elements: Vec::new(),        local_image_paths: Vec::new(),        remote_image_urls: vec![data_image_url.clone()],    }) as Arc<dyn HistoryCell>];    app.apply_backtrack_rollback(BacktrackSelection {        nth_user_message: 0,        prefill: "please inspect this".to_string(),        text_elements: Vec::new(),        local_image_paths: Vec::new(),        remote_image_urls: vec![data_image_url.clone()],    });    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    let mut saw_rollback = false;    let mut submitted_items: Option<Vec<UserInput>> = None;    while let Ok(op) = op_rx.try_recv() {        match op {            Op::ThreadRollback { .. } => saw_rollback = true,            Op::UserTurn { items, .. } => submitted_items = Some(items),            _ => {}        }    }    assert!(saw_rollback);    let items = submitted_items.expect("expected user turn after backtrack resubmit");    assert!(items.iter().any(|item| {        matches!(            item,            UserInput::Image { url, .. } if url == &data_image_url        )    }));}#[tokio::test]async fn replay_thread_snapshot_replays_turn_history_in_order() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let thread_id = ThreadId::new();    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: Some(test_thread_session(                thread_id,                test_path_buf("/home/user/project"),            )),            turns: vec![                Turn {                    id: "turn-1".to_string(),                    items_view: codex_app_server_protocol::TurnItemsView::Full,                    items: vec![ThreadItem::UserMessage {                        id: "user-1".to_string(),                        client_id: None,                        content: vec![AppServerUserInput::Text {                            text: "first prompt".to_string(),                            text_elements: Vec::new(),                        }],                    }],                    status: TurnStatus::Completed,                    error: None,                    started_at: None,                    completed_at: None,                    duration_ms: None,                },                Turn {                    id: "turn-2".to_string(),                    items_view: codex_app_server_protocol::TurnItemsView::Full,                    items: vec![                        ThreadItem::UserMessage {                            id: "user-2".to_string(),                            client_id: None,                            content: vec![AppServerUserInput::Text {                                text: "third prompt".to_string(),                                text_elements: Vec::new(),                            }],                        },                        ThreadItem::AgentMessage {                            id: "assistant-2".to_string(),                            text: "done".to_string(),                            phase: None,                            memory_citation: None,                        },                    ],                    status: TurnStatus::Completed,                    error: None,                    started_at: None,                    completed_at: None,                    duration_ms: None,                },            ],            events: Vec::new(),            input_state: None,        },        /*resume_restored_queue*/ false,    );    while let Ok(event) = app_event_rx.try_recv() {        if let AppEvent::InsertHistoryCell(cell) = event {            let cell: Arc<dyn HistoryCell> = cell.into();            app.transcript_cells.push(cell);        }    }    let user_messages: Vec<String> = app        .transcript_cells        .iter()        .filter_map(|cell| {            cell.as_any()                .downcast_ref::<UserHistoryCell>()                .map(|cell| cell.message.clone())        })        .collect();    assert_eq!(        user_messages,        vec!["first prompt".to_string(), "third prompt".to_string()]    );}#[tokio::test]async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    let receiver_thread_id =        ThreadId::from_string("019cff70-2599-75e2-af72-b958ce5dc1cc").expect("valid thread");    app.agent_navigation.upsert(        receiver_thread_id,        Some("Robie".to_string()),        Some("explorer".to_string()),        /*is_closed*/ false,    );    let replacement = ChatWidget::new_with_app_event(ChatWidgetInit {        config: app.config.clone(),        frame_requester: crate::tui::FrameRequester::test_dummy(),        app_event_tx: app.app_event_tx.clone(),        workspace_command_runner: None,        initial_user_message: None,        enhanced_keys_supported: app.enhanced_keys_supported,        has_chatgpt_account: app.chat_widget.has_chatgpt_account(),        has_codex_backend_auth: app.chat_widget.has_codex_backend_auth(),        model_catalog: app.model_catalog.clone(),        feedback: app.feedback.clone(),        is_first_run: false,        status_account_display: app.chat_widget.status_account_display().cloned(),        runtime_model_provider_base_url: app            .chat_widget            .runtime_model_provider_base_url()            .map(str::to_string),        initial_plan_type: app.chat_widget.current_plan_type(),        model: Some(app.chat_widget.current_model().to_string()),        startup_tooltip_override: None,        status_line_invalid_items_warned: app.status_line_invalid_items_warned.clone(),        terminal_title_invalid_items_warned: app.terminal_title_invalid_items_warned.clone(),        session_telemetry: app.session_telemetry.clone(),    });    app.replace_chat_widget(replacement);    app.replay_thread_snapshot(        ThreadEventSnapshot {            session: None,            turns: Vec::new(),            events: vec![ThreadBufferedEvent::Notification(                ServerNotification::ItemStarted(                    codex_app_server_protocol::ItemStartedNotification {                        thread_id: "thread-1".to_string(),                        turn_id: "turn-1".to_string(),                        started_at_ms: 0,                        item: ThreadItem::CollabAgentToolCall {                            id: "wait-1".to_string(),                            tool: codex_app_server_protocol::CollabAgentTool::Wait,                            status:                                codex_app_server_protocol::CollabAgentToolCallStatus::InProgress,                            sender_thread_id: ThreadId::new().to_string(),                            receiver_thread_ids: vec![receiver_thread_id.to_string()],                            prompt: None,                            model: None,                            reasoning_effort: None,                            agents_states: HashMap::new(),                        },                    },                ),            )],            input_state: None,        },        /*resume_restored_queue*/ false,    );    let mut saw_named_wait = false;    while let Ok(event) = app_event_rx.try_recv() {        if let AppEvent::InsertHistoryCell(cell) = event {            let transcript = lines_to_single_string(&cell.transcript_lines(/*width*/ 80));            saw_named_wait |= transcript.contains("Robie [explorer]");        }    }    assert!(        saw_named_wait,        "expected replayed wait item to keep agent name"    );}#[tokio::test]async fn refreshed_snapshot_session_persists_resumed_turns() {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    let initial_session = test_thread_session(thread_id, test_path_buf("/tmp/original"));    app.thread_event_channels.insert(        thread_id,        ThreadEventChannel::new_with_session(            /*capacity*/ 4,            initial_session.clone(),            Vec::new(),        ),    );    let resumed_turns = vec![test_turn(        "turn-1",        TurnStatus::Completed,        vec![ThreadItem::UserMessage {            id: "user-1".to_string(),            client_id: None,            content: vec![AppServerUserInput::Text {                text: "restored prompt".to_string(),                text_elements: Vec::new(),            }],        }],    )];    let resumed_session = ThreadSessionState {        cwd: test_path_buf("/tmp/refreshed").abs(),        runtime_workspace_roots: Vec::new(),        instruction_source_paths: Vec::new(),        ..initial_session.clone()    };    let mut snapshot = ThreadEventSnapshot {        session: Some(initial_session),        turns: Vec::new(),        events: Vec::new(),        input_state: None,    };    app.apply_refreshed_snapshot_thread(        thread_id,        AppServerStartedThread {            session: resumed_session.clone(),            turns: resumed_turns.clone(),        },        &mut snapshot,    )    .await;    assert_eq!(snapshot.session, Some(resumed_session.clone()));    assert_eq!(snapshot.turns, resumed_turns);    let store = app        .thread_event_channels        .get(&thread_id)        .expect("thread channel")        .store        .lock()        .await;    let store_snapshot = store.snapshot();    assert_eq!(store_snapshot.session, Some(resumed_session));    assert_eq!(store_snapshot.turns, snapshot.turns);}#[tokio::test]async fn queued_rollback_syncs_overlay_and_clears_deferred_history() {    let mut app = make_test_app().await;    app.transcript_cells = vec![        Arc::new(UserHistoryCell {            message: "first".to_string(),            text_elements: Vec::new(),            local_image_paths: Vec::new(),            remote_image_urls: Vec::new(),        }) as Arc<dyn HistoryCell>,        Arc::new(AgentMessageCell::new(            vec![Line::from("after first")],            /*is_first_line*/ false,        )) as Arc<dyn HistoryCell>,        Arc::new(UserHistoryCell {            message: "second".to_string(),            text_elements: Vec::new(),            local_image_paths: Vec::new(),            remote_image_urls: Vec::new(),        }) as Arc<dyn HistoryCell>,        Arc::new(AgentMessageCell::new(            vec![Line::from("after second")],            /*is_first_line*/ false,        )) as Arc<dyn HistoryCell>,    ];    app.overlay = Some(Overlay::new_transcript(        app.transcript_cells.clone(),        app.keymap.pager.clone(),    ));    app.deferred_history_lines = vec![Line::from("stale buffered line").into()];    app.backtrack.overlay_preview_active = true;    app.backtrack.nth_user_message = 1;    app.chat_widget.update_account_state(        /*status_account_display*/ None, /*plan_type*/ None,        /*has_chatgpt_account*/ false, /*has_codex_backend_auth*/ true,    );    app.chat_widget        .set_composer_text("/usage daily".to_string(), Vec::new(), Vec::new());    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    app.chat_widget        .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));    let pending_usage = app        .chat_widget        .active_cell_transcript_lines(/*width*/ 80)        .expect("pending usage transcript");    assert!(lines_to_single_string(&pending_usage).contains("Token activity\n   Loading..."));    let changed = app.apply_non_pending_thread_rollback(/*num_turns*/ 1);    assert!(changed);    assert!(app.backtrack_render_pending);    assert!(app.deferred_history_lines.is_empty());    assert!(        app.chat_widget            .active_cell_transcript_lines(/*width*/ 80)            .is_none_or(|lines| !lines_to_single_string(&lines).contains("Token activity"))    );    assert_eq!(app.backtrack.nth_user_message, 0);    let user_messages: Vec<String> = app        .transcript_cells        .iter()        .filter_map(|cell| {            cell.as_any()                .downcast_ref::<UserHistoryCell>()                .map(|cell| cell.message.clone())        })        .collect();    assert_eq!(user_messages, vec!["first".to_string()]);    let overlay_cell_count = match app.overlay.as_ref() {        Some(Overlay::Transcript(t)) => t.committed_cell_count(),        _ => panic!("expected transcript overlay"),    };    assert_eq!(overlay_cell_count, app.transcript_cells.len());}#[tokio::test]async fn late_usage_result_can_follow_finalized_plan() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    app.chat_widget        .add_token_activity_output(crate::chatwidget::TokenActivityView::Daily);    let request_id = match app_event_rx.try_recv() {        Ok(AppEvent::RefreshTokenActivity { request_id }) => request_id,        other => panic!("expected token activity refresh request, got {other:?}"),    };    app.chat_widget.note_stream_consolidation_queued();    app.transcript_cells        .push(Arc::new(history_cell::new_proposed_plan_stream(            vec![Line::from("finalized plan")],            /*is_stream_continuation*/ false,        )));    app.chat_widget.note_stream_consolidation_completed();    assert!(        app.chat_widget.finish_token_activity_refresh(            request_id,            Err("token activity unavailable".to_string()),        )    );    assert!(!app.pending_usage_output_insertion_blocked());    assert!(        app.chat_widget            .take_completed_token_activity_output()            .is_some()    );}#[tokio::test]async fn thread_rollback_response_discards_queued_active_thread_events() {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    let (tx, rx) = mpsc::channel(8);    app.active_thread_id = Some(thread_id);    app.active_thread_rx = Some(rx);    tx.send(ThreadBufferedEvent::Notification(        ServerNotification::ConfigWarning(ConfigWarningNotification {            summary: "stale warning".to_string(),            details: None,            path: None,            range: None,        }),    ))    .await    .expect("event should queue");    app.handle_thread_rollback_response(        thread_id,        /*num_turns*/ 1,        &ThreadRollbackResponse {            thread: Thread {                id: thread_id.to_string(),                session_id: thread_id.to_string(),                forked_from_id: None,                parent_thread_id: None,                preview: String::new(),                ephemeral: false,                model_provider: "openai".to_string(),                created_at: 0,                updated_at: 0,                status: codex_app_server_protocol::ThreadStatus::Idle,                path: None,                cwd: test_path_buf("/tmp/project").abs(),                cli_version: "0.0.0".to_string(),                source: SessionSource::Cli,                thread_source: None,                agent_nickname: None,                agent_role: None,                git_info: None,                name: None,                turns: Vec::new(),            },        },    )    .await;    let rx = app        .active_thread_rx        .as_mut()        .expect("active receiver should remain attached");    assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));}#[tokio::test]async fn new_session_requests_shutdown_for_previous_conversation() {    Box::pin(async {        let (mut app, mut app_event_rx, mut op_rx) = Box::pin(make_test_app_with_channels()).await;        let thread_id = ThreadId::new();        let event = crate::session_state::ThreadSessionState {            thread_id,            forked_from_id: None,            fork_parent_title: None,            thread_name: None,            model: "gpt-test".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: None,            collaboration_mode: None,            personality: None,            message_history: None,            network_proxy: None,            rollout_path: Some(PathBuf::new()),        };        app.chat_widget.handle_thread_session(event);        while app_event_rx.try_recv().is_ok() {}        while op_rx.try_recv().is_ok() {}        let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(            app.chat_widget.config_ref(),        ))        .await        .expect("embedded app server");        Box::pin(app.shutdown_current_thread(&mut app_server)).await;        assert!(            op_rx.try_recv().is_err(),            "shutdown should not submit Op::Shutdown"        );    })    .await;}#[tokio::test]async fn shutdown_first_exit_returns_immediate_exit_when_shutdown_submit_fails() {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    app.active_thread_id = Some(thread_id);    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let control = Box::pin(app.handle_exit_mode(&mut app_server, ExitMode::ShutdownFirst)).await;    assert_eq!(app.pending_shutdown_exit_thread_id, None);    assert!(matches!(        control,        AppRunControl::Exit(ExitReason::UserRequested)    ));}#[tokio::test]async fn shutdown_first_exit_uses_app_server_shutdown_without_submitting_op() {    let (mut app, _app_event_rx, mut op_rx) = Box::pin(make_test_app_with_channels()).await;    let thread_id = ThreadId::new();    app.active_thread_id = Some(thread_id);    let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(        app.chat_widget.config_ref(),    ))    .await    .expect("embedded app server");    let control = Box::pin(app.handle_exit_mode(&mut app_server, ExitMode::ShutdownFirst)).await;    assert_eq!(app.pending_shutdown_exit_thread_id, None);    assert!(matches!(        control,        AppRunControl::Exit(ExitReason::UserRequested)    ));    assert!(        op_rx.try_recv().is_err(),        "shutdown should not submit Op::Shutdown"    );}#[tokio::test]async fn interrupt_without_active_turn_is_treated_as_handled() {    Box::pin(async {        let mut app = make_test_app().await;        let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(            app.chat_widget.config_ref(),        ))        .await        .expect("embedded app server");        let started = app_server            .start_thread(app.chat_widget.config_ref())            .await            .expect("thread/start should succeed");        let thread_id = started.session.thread_id;        app.enqueue_primary_thread_session(started.session, started.turns)            .await            .expect("primary thread should be registered");        let op = AppCommand::interrupt();        let handled = Box::pin(app.try_submit_active_thread_op_via_app_server(            &mut app_server,            thread_id,            &op,        ))        .await        .expect("interrupt submission should not fail");        assert_eq!(handled, true);    })    .await;}#[tokio::test]async fn override_turn_context_sends_thread_settings_update() {    Box::pin(async {        let mut app = make_test_app().await;        let mut app_server =            crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref())                .await                .expect("embedded app server");        let started = app_server            .start_thread(app.chat_widget.config_ref())            .await            .expect("thread/start should succeed");        let thread_id = started.session.thread_id;        let initial_model = started.session.model.clone();        let initial_effort = started.session.reasoning_effort.clone();        app.enqueue_primary_thread_session(started.session, started.turns)            .await            .expect("primary thread should be registered");        let service_tier = ServiceTier::Fast.request_value().to_string();        let collaboration_mode = CollaborationMode {            mode: ModeKind::Plan,            settings: Settings {                model: "gpt-5.4".to_string(),                reasoning_effort: Some(ReasoningEffortConfig::High),                developer_instructions: None,            },        };        let op = AppCommand::override_turn_context(            /*cwd*/ None,            Some(AskForApproval::OnRequest),            Some(ApprovalsReviewer::AutoReview),            /*permission_profile*/ None,            Some(ActivePermissionProfile::new(                codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE,            )),            /*windows_sandbox_level*/ None,            Some("gpt-5.4".to_string()),            Some(Some(ReasoningEffortConfig::High)),            /*summary*/ None,            Some(Some(service_tier.clone())),            Some(collaboration_mode.clone()),            Some(Personality::Pragmatic),        );        let handled = app            .try_submit_active_thread_op_via_app_server(&mut app_server, thread_id, &op)            .await            .expect("settings update submission should not fail");        assert_eq!(handled, true);        assert_eq!(            app.primary_session_configured                .as_ref()                .expect("primary session")                .model,            initial_model,            "thread/settings/update response is only an ack; cached state changes on notification"        );        let notification = next_thread_settings_updated(&mut app_server, thread_id).await;        assert_eq!(notification.thread_settings.model, "gpt-5.4");        assert_eq!(            notification.thread_settings.effort,            Some(ReasoningEffortConfig::High)        );        assert_eq!(            notification.thread_settings.service_tier,            Some(service_tier.clone())        );        assert_eq!(            notification.thread_settings.approval_policy,            AskForApproval::OnRequest        );        assert_eq!(            notification.thread_settings.approvals_reviewer.to_core(),            ApprovalsReviewer::AutoReview        );        let notified_mode = &notification.thread_settings.collaboration_mode;        assert_eq!(notified_mode.mode, collaboration_mode.mode);        assert_eq!(            notified_mode.settings.model,            collaboration_mode.settings.model        );        assert_eq!(            notified_mode.settings.reasoning_effort,            collaboration_mode.settings.reasoning_effort        );        assert_eq!(            notification.thread_settings.personality,            Some(Personality::Pragmatic)        );        app.handle_app_server_event(            &app_server,            codex_app_server_client::AppServerEvent::ServerNotification(                ServerNotification::ThreadSettingsUpdated(notification),            ),        )        .await;        let updated_session = app            .primary_session_configured            .as_ref()            .expect("primary session should be updated from notification");        assert_eq!(updated_session.model, initial_model);        assert_eq!(updated_session.reasoning_effort, initial_effort);        let updated_mode = updated_session            .collaboration_mode            .as_deref()            .expect("collaboration mode should be cached");        assert_eq!(updated_mode.mode, collaboration_mode.mode);        assert_eq!(            updated_mode.settings.model,            collaboration_mode.settings.model        );        assert_eq!(            updated_mode.settings.reasoning_effort,            collaboration_mode.settings.reasoning_effort        );        assert_eq!(updated_session.personality, Some(Personality::Pragmatic));        assert_eq!(updated_session.service_tier, Some(service_tier));        assert_eq!(updated_session.approval_policy, AskForApproval::OnRequest);        assert_eq!(            updated_session.approvals_reviewer,            ApprovalsReviewer::AutoReview        );        assert_eq!(            updated_session                .active_permission_profile                .as_ref()                .expect("active profile")                .id,            codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE        );    })    .await;}#[tokio::test]async fn thread_setting_update_params_sync_model_and_default_reasoning() {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    app.active_thread_id = Some(thread_id);    app.chat_widget.set_model("gpt-5.4");    let params = app        .active_thread_model_setting_update_params("gpt-5.4".to_string())        .expect("active thread should produce update params");    assert_eq!(params.thread_id, thread_id.to_string());    assert_eq!(params.model, Some("gpt-5.4".to_string()));    assert_eq!(        params            .collaboration_mode            .as_ref()            .expect("collaboration mode should sync with model")            .settings            .model,        "gpt-5.4"    );    app.chat_widget        .set_reasoning_effort(Some(ReasoningEffortConfig::Low));    app.chat_widget        .set_collaboration_mask(CollaborationModeMask {            name: "Plan".to_string(),            mode: Some(ModeKind::Plan),            model: Some("gpt-plan".to_string()),            reasoning_effort: Some(Some(ReasoningEffortConfig::Medium)),            developer_instructions: None,        });    app.on_update_reasoning_effort(Some(ReasoningEffortConfig::High));    let params = app        .active_thread_reasoning_setting_update_params(Some(ReasoningEffortConfig::High))        .expect("active thread should produce update params");    assert_eq!(params.thread_id, thread_id.to_string());    assert_eq!(params.effort, Some(ReasoningEffortConfig::High));    let collaboration_mode = params        .collaboration_mode        .expect("collaboration mode should sync with reasoning");    assert_eq!(collaboration_mode.mode, ModeKind::Default);    assert_eq!(        collaboration_mode.settings.reasoning_effort,        Some(ReasoningEffortConfig::High)    );}#[tokio::test]async fn inactive_thread_settings_notification_updates_cached_collaboration_mode() {    let mut app = make_test_app().await;    let primary_thread_id = ThreadId::new();    let inactive_thread_id = ThreadId::new();    let primary_session = test_thread_session(primary_thread_id, test_path_buf("/tmp/main"));    let inactive_session = test_thread_session(inactive_thread_id, test_path_buf("/tmp/inactive"));    let collaboration_mode = CollaborationMode {        mode: ModeKind::Plan,        settings: Settings {            model: "gpt-plan".to_string(),            reasoning_effort: Some(ReasoningEffortConfig::High),            developer_instructions: Some("draft a plan first".to_string()),        },    };    app.primary_thread_id = Some(primary_thread_id);    app.active_thread_id = Some(primary_thread_id);    app.primary_session_configured = Some(primary_session.clone());    app.thread_event_channels.insert(        primary_thread_id,        ThreadEventChannel::new_with_session(            THREAD_EVENT_CHANNEL_CAPACITY,            primary_session,            Vec::new(),        ),    );    app.thread_event_channels.insert(        inactive_thread_id,        ThreadEventChannel::new_with_session(            THREAD_EVENT_CHANNEL_CAPACITY,            inactive_session,            Vec::new(),        ),    );    let notification = ThreadSettingsUpdatedNotification {        thread_id: inactive_thread_id.to_string(),        thread_settings: ThreadSettings {            cwd: test_absolute_path("/tmp/thread-settings"),            approval_policy: AskForApproval::OnRequest,            approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::AutoReview,            sandbox_policy: codex_app_server_protocol::SandboxPolicy::ReadOnly {                network_access: false,            },            active_permission_profile: Some(                codex_app_server_protocol::ActivePermissionProfile::read_only(),            ),            model: "gpt-plan".to_string(),            model_provider: "openai".to_string(),            service_tier: None,            effort: collaboration_mode.settings.reasoning_effort.clone(),            summary: None,            collaboration_mode: collaboration_mode.clone(),            personality: Some(Personality::Pragmatic),        },    };    app.enqueue_thread_notification(        inactive_thread_id,        ServerNotification::ThreadSettingsUpdated(notification),    )    .await    .expect("settings notification should be cached");    let cached_session = app        .thread_event_channels        .get(&inactive_thread_id)        .expect("inactive thread channel")        .store        .lock()        .await        .session        .clone()        .expect("inactive session should remain cached");    assert_eq!(cached_session.model, "gpt-test");    assert_eq!(cached_session.personality, Some(Personality::Pragmatic));    assert_eq!(        cached_session.collaboration_mode.as_deref(),        Some(&collaboration_mode)    );    app.chat_widget.handle_thread_session(cached_session);    assert_eq!(        app.chat_widget.active_collaboration_mode_kind(),        ModeKind::Plan    );    assert_eq!(app.chat_widget.current_model(), "gpt-plan");    assert_eq!(        app.chat_widget.current_collaboration_mode().model(),        "gpt-test"    );    assert_eq!(        app.chat_widget.current_reasoning_effort(),        Some(ReasoningEffortConfig::High)    );    assert_eq!(        app.chat_widget.config_ref().personality,        Some(Personality::Pragmatic)    );}#[tokio::test]async fn clear_only_ui_reset_preserves_chat_session_state() {    let mut app = make_test_app().await;    let thread_id = ThreadId::new();    app.chat_widget        .handle_thread_session(crate::session_state::ThreadSessionState {            thread_id,            forked_from_id: None,            fork_parent_title: None,            thread_name: Some("keep me".to_string()),            model: "gpt-test".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("/tmp/project").abs(),            runtime_workspace_roots: Vec::new(),            instruction_source_paths: Vec::new(),            reasoning_effort: None,            collaboration_mode: None,            personality: None,            message_history: None,            network_proxy: None,            rollout_path: Some(PathBuf::new()),        });    app.chat_widget        .apply_external_edit("draft prompt".to_string());    app.transcript_cells = vec![Arc::new(UserHistoryCell {        message: "old message".to_string(),        text_elements: Vec::new(),        local_image_paths: Vec::new(),        remote_image_urls: Vec::new(),    }) as Arc<dyn HistoryCell>];    app.overlay = Some(Overlay::new_transcript(        app.transcript_cells.clone(),        crate::keymap::RuntimeKeymap::defaults().pager,    ));    app.deferred_history_lines = vec![Line::from("stale buffered line").into()];    app.has_emitted_history_lines = true;    app.backtrack.primed = true;    app.backtrack.overlay_preview_active = true;    app.backtrack.nth_user_message = 0;    app.backtrack_render_pending = true;    app.reset_app_ui_state_after_clear();    assert!(app.overlay.is_none());    assert!(app.transcript_cells.is_empty());    assert!(app.deferred_history_lines.is_empty());    assert!(!app.has_emitted_history_lines);    assert!(!app.backtrack.primed);    assert!(!app.backtrack.overlay_preview_active);    assert!(app.backtrack.pending_rollback.is_none());    assert!(!app.backtrack_render_pending);    assert_eq!(app.chat_widget.thread_id(), Some(thread_id));    assert_eq!(app.chat_widget.composer_text_with_pending(), "draft prompt");}#[tokio::test]async fn clear_only_ui_reset_allows_active_skill_warning_to_render_again() {    let mut app = make_test_app().await;    let error = SkillErrorInfo {        path: test_path_buf("/tmp/project/.codex/skills/abc/SKILL.md"),        message: "invalid description".to_string(),    };    assert_eq!(        app.skill_load_warnings            .newly_active_errors(std::slice::from_ref(&error)),        vec![error.clone()]    );    assert_eq!(        app.skill_load_warnings            .newly_active_errors(std::slice::from_ref(&error)),        Vec::<SkillErrorInfo>::new()    );    app.reset_app_ui_state_after_clear();    assert_eq!(        app.skill_load_warnings            .newly_active_errors(std::slice::from_ref(&error)),        vec![error]    );}#[tokio::test]async fn backtrack_esc_does_not_steal_empty_vim_insert_escape() {    let mut app = make_test_app().await;    let esc = crossterm::event::KeyEvent::new(crossterm::event::KeyCode::Esc, KeyModifiers::NONE);    assert!(app.chat_widget.composer_is_empty());    assert!(app.should_handle_backtrack_esc(esc));    app.chat_widget.toggle_vim_mode_and_notify();    assert!(app.should_handle_backtrack_esc(esc));    app.chat_widget        .handle_key_event(crossterm::event::KeyEvent::new(            crossterm::event::KeyCode::Char('i'),            KeyModifiers::NONE,        ));    assert!(app.chat_widget.should_handle_vim_insert_escape(esc));    assert!(!app.should_handle_backtrack_esc(esc));    app.chat_widget.handle_key_event(esc);    assert!(!app.backtrack.primed);    assert!(!app.chat_widget.should_handle_vim_insert_escape(esc));    assert!(app.should_handle_backtrack_esc(esc));}#[tokio::test]async fn side_conversations_reject_backtrack_esc_without_stealing_vim_insert_escape() {    let mut app = make_test_app().await;    let esc = crossterm::event::KeyEvent::new(crossterm::event::KeyCode::Esc, KeyModifiers::NONE);    app.chat_widget        .set_side_conversation_active(/*active*/ true);    assert!(app.chat_widget.composer_is_empty());    assert!(!app.should_handle_backtrack_esc(esc));    assert!(app.should_reject_side_backtrack_esc(esc));    app.chat_widget.toggle_vim_mode_and_notify();    app.chat_widget        .handle_key_event(crossterm::event::KeyEvent::new(            crossterm::event::KeyCode::Char('i'),            KeyModifiers::NONE,        ));    assert!(app.chat_widget.should_handle_vim_insert_escape(esc));    assert!(!app.should_handle_backtrack_esc(esc));    assert!(!app.should_reject_side_backtrack_esc(esc));}#[tokio::test]async fn side_backtrack_rejection_reports_unavailable_message_snapshot() {    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;    app.backtrack.primed = true;    app.reject_side_backtrack_esc();    assert!(!app.backtrack.primed);    let cell = match app_event_rx.try_recv() {        Ok(AppEvent::InsertHistoryCell(cell)) => cell,        other => panic!("expected InsertHistoryCell event, got {other:?}"),    };    let rendered = cell        .display_lines(/*width*/ 80)        .into_iter()        .map(|line| line.to_string())        .collect::<Vec<_>>()        .join("\n");    assert_app_snapshot!(        "side_backtrack_rejection_reports_unavailable_message",        rendered    );}async fn start_config_write_test_app_server(app: &App) -> Result<AppServerSession> {    Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await}