use super::super::*;
use super::support::*;
#[test]
fn logout_picker_escape_cancels_without_provider_selection() {
let mut state = MissionControlState::default();
state.open_logout_picker(
vec![crate::tui::state::LoginProviderEntry {
id: "openai-codex".to_string(),
label: "OpenAI Codex".to_string(),
description: "codex".to_string(),
status: "configured".to_string(),
}],
None,
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
let action = handle_key(key(KeyCode::Esc), &mut state);
assert!(matches!(action, InputAction::None));
assert!(state.modals.logout_picker.is_none());
}
#[test]
fn logout_picker_up_down_navigation_scrolls_and_selects_logout_provider() {
let mut state = MissionControlState::default();
let entries = (0..12)
.map(|index| crate::tui::state::LoginProviderEntry {
id: format!("provider-{index}"),
label: format!("Provider {index}"),
description: "test provider".to_string(),
status: "configured".to_string(),
})
.collect::<Vec<_>>();
state.open_logout_picker(entries, None, DEFAULT_PICKER_VISIBLE_ROWS as usize);
for _ in 0..11 {
assert!(matches!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
));
}
let picker = state.modals.logout_picker.as_ref().unwrap();
assert_eq!(picker.selected, 11);
assert!(matches!(
handle_key(key(KeyCode::Up), &mut state),
InputAction::None
));
let picker = state.modals.logout_picker.as_ref().unwrap();
assert_eq!(picker.selected, 10);
let action = handle_key(key(KeyCode::Enter), &mut state);
assert!(
matches!(action, InputAction::SelectLogoutProvider(provider) if provider == "provider-10")
);
assert!(state.modals.logout_picker.is_none());
}
#[test]
fn logout_picker_uses_actual_viewport_to_keep_selection_visible() {
let mut state = MissionControlState::default();
let entries = (0..6)
.map(|index| crate::tui::state::LoginProviderEntry {
id: format!("provider-{index}"),
label: format!("Provider {index}"),
description: "test provider".to_string(),
status: "configured".to_string(),
})
.collect::<Vec<_>>();
state.open_logout_picker(entries, None, 2);
for _ in 0..3 {
assert!(matches!(
handle_key_with_all_viewports(
key(KeyCode::Down),
&mut state,
KeyViewports {
logout_picker: Some(PickerViewport { visible_rows: 2 }),
..KeyViewports::default()
},
&[]
),
InputAction::None
));
}
let picker = state.modals.logout_picker.as_ref().unwrap();
assert_eq!(picker.selected, 3);
let offset = state.scroll_offset(&state.scroll_views.logout_picker);
assert!(picker.selected >= offset);
assert!(picker.selected < offset + 2);
}
#[test]
fn model_picker_keys_take_priority_over_autocomplete_and_prompt() {
let mut state = MissionControlState {
provider: "openai-codex".to_string(),
model: "gpt-a".to_string(),
prompt_editor: crate::tui::state::PromptEditor::new("/m", 0),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
state.autocomplete = Some(crate::tui::state::AutocompleteState {
candidates: vec![AutocompleteCandidate::slash_command("model", "select")],
selected: 0,
token_start: 0,
token_end: 2,
token: "/m".to_string(),
});
state.open_model_picker(
vec![
crate::model_catalog::ModelCatalogEntry::new_codex("gpt-a"),
crate::model_catalog::ModelCatalogEntry::new_codex("gpt-b"),
],
std::collections::BTreeSet::new(),
None,
1,
);
assert!(matches!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
));
assert!(matches!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::SelectModel(model) if model == "openai-codex/gpt-b"
));
assert!(state.modals.model_picker.is_none());
assert_eq!(state.prompt_plain_text(), "/m");
}
#[test]
fn model_picker_uses_actual_viewport_to_keep_selection_visible() {
let mut state = MissionControlState {
provider: "openai-codex".to_string(),
model: "gpt-0".to_string(),
..Default::default()
};
state.open_model_picker(
(0..6)
.map(|index| crate::model_catalog::ModelCatalogEntry::new_codex(format!("gpt-{index}")))
.collect(),
std::collections::BTreeSet::new(),
None,
2,
);
for _ in 0..3 {
assert!(matches!(
handle_key_with_all_viewports(
key(KeyCode::Down),
&mut state,
KeyViewports {
model_picker: Some(PickerViewport { visible_rows: 2 }),
..KeyViewports::default()
},
&[]
),
InputAction::None
));
}
let picker = state.modals.model_picker.as_ref().unwrap();
assert_eq!(picker.selected, 3);
assert_eq!(state.scroll_offset(&state.scroll_views.model_picker), 2);
}
#[test]
fn session_picker_keys_take_priority_over_prompt_and_autocomplete() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("keep", 0),
..Default::default()
};
state.open_session_picker(
vec![
crate::tui::state::SessionPickerRow {
id: "session-a".into(),
label: "session-a".into(),
is_current: false,
preview: None,
},
crate::tui::state::SessionPickerRow {
id: "session-b".into(),
label: "session-b".into(),
is_current: false,
preview: None,
},
],
1,
);
assert!(matches!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
));
assert_eq!(state.prompt_plain_text(), "keep");
assert_eq!(state.selected_session_id().as_deref(), Some("session-b"));
}
#[test]
fn session_picker_tab_focus_and_preview_arrows_do_not_move_list() {
let mut state = MissionControlState::default();
state.open_session_picker(
vec![
crate::tui::state::SessionPickerRow {
id: "session-a".into(),
label: "session-a".into(),
is_current: false,
preview: None,
},
crate::tui::state::SessionPickerRow {
id: "session-b".into(),
label: "session-b".into(),
is_current: false,
preview: None,
},
],
2,
);
state.set_session_preview(
"session-a",
crate::tui::state::SessionPreviewState::Ready(crate::tui::state::SessionPreview {
first: crate::tui::state::PreviewMessage {
role: "user",
text: "one two three four five six seven eight nine ten".to_string(),
},
most_recent: None,
}),
);
let viewports = KeyViewports {
sessions_modal: Some(SessionPickerViewport {
list_rows: 2,
preview_rows: 1,
preview_width: 8,
preview_visible: true,
}),
..KeyViewports::default()
};
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Tab), &mut state, viewports, &[]),
InputAction::None
);
assert_eq!(
state.modals.session_picker.as_ref().unwrap().focus,
crate::tui::state::SessionPickerFocus::Preview
);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]),
InputAction::None
);
let picker = state.modals.session_picker.as_ref().unwrap();
assert_eq!(picker.selected, 0);
assert!(state.scroll_offset(&state.scroll_views.session_preview) > 0);
assert_eq!(
handle_key_with_all_viewports(
modified_key(KeyCode::BackTab, KeyModifiers::SHIFT),
&mut state,
viewports,
&[]
),
InputAction::None
);
assert_eq!(
state.modals.session_picker.as_ref().unwrap().focus,
crate::tui::state::SessionPickerFocus::List
);
}
#[test]
fn session_picker_tab_keeps_list_focus_when_preview_column_hidden() {
let mut state = MissionControlState::default();
state.open_session_picker(
vec![crate::tui::state::SessionPickerRow {
id: "session-a".into(),
label: "session-a".into(),
is_current: false,
preview: None,
}],
1,
);
let viewports = KeyViewports {
sessions_modal: Some(SessionPickerViewport {
list_rows: 1,
preview_rows: 1,
preview_width: 1,
preview_visible: false,
}),
..KeyViewports::default()
};
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Tab), &mut state, viewports, &[]),
InputAction::None
);
assert_eq!(
state.modals.session_picker.as_ref().unwrap().focus,
crate::tui::state::SessionPickerFocus::List
);
}
#[test]
fn session_picker_enter_selects_focused_row_and_empty_enter_noops() {
let mut state = MissionControlState::default();
state.open_session_picker(
vec![crate::tui::state::SessionPickerRow {
id: "session-a".into(),
label: "session-a".into(),
is_current: false,
preview: None,
}],
1,
);
assert!(
matches!(handle_key(key(KeyCode::Enter), &mut state), InputAction::SelectSession(id) if id == "session-a")
);
assert!(!state.session_picker_visible());
state.open_session_picker(Vec::new(), 1);
assert!(matches!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::None
));
assert!(state.session_picker_visible());
}
#[test]
fn session_picker_escape_closes_without_selection() {
let mut state = MissionControlState::default();
state.open_session_picker(
vec![crate::tui::state::SessionPickerRow {
id: "session-a".into(),
label: "session-a".into(),
is_current: false,
preview: None,
}],
1,
);
assert!(matches!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::None
));
assert!(!state.session_picker_visible());
}