use super::super::*;
use super::support::*;
use crate::tui::state::ConnectProviderStage;
#[test]
fn connect_provider_exposes_oauth_copy_and_open_shortcuts_without_staging_fallback() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_oauth("openai-codex".to_string());
state.set_connect_provider_instructions(
"https://example.test/auth".to_string(),
"open".to_string(),
);
assert!(matches!(
handle_key(
modified_key(KeyCode::Char('y'), KeyModifiers::CONTROL),
&mut state
),
InputAction::CopyOAuthUrl
));
assert!(matches!(
handle_key(
modified_key(KeyCode::Char('o'), KeyModifiers::ALT),
&mut state
),
InputAction::OpenOAuthUrl
));
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.fallback_input
.value(),
""
);
}
#[test]
fn connect_provider_oauth_plain_characters_emit_manual_fallback_actions() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_oauth("openai-codex".to_string());
assert_eq!(
handle_key(key(KeyCode::Char('c')), &mut state),
InputAction::InsertConnectProviderChar('c')
);
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.fallback_input
.value(),
""
);
}
#[test]
fn shifted_printable_modal_text_entry_emits_shifted_actions() {
let mut custom = MissionControlState::default();
custom.open_connect_provider(Vec::new(), None);
custom.start_connect_custom_configuration();
assert_eq!(
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::SHIFT),
&mut custom,
),
InputAction::InsertConnectProviderChar('A')
);
assert_eq!(
handle_key(
modified_key(KeyCode::Char('1'), KeyModifiers::SHIFT),
&mut custom,
),
InputAction::InsertConnectProviderChar('!')
);
assert!(
custom
.modals
.connect_provider
.as_ref()
.unwrap()
.label
.is_empty()
);
assert_eq!(custom.prompt_plain_text(), "");
let mut oauth = MissionControlState::default();
oauth.open_connect_provider(Vec::new(), None);
oauth.start_connect_oauth("openai-codex".to_string());
assert_eq!(
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::SHIFT),
&mut oauth,
),
InputAction::InsertConnectProviderChar('A')
);
assert_eq!(
handle_key(
modified_key(KeyCode::Char('1'), KeyModifiers::SHIFT),
&mut oauth,
),
InputAction::InsertConnectProviderChar('!')
);
assert!(
oauth
.modals
.connect_provider
.as_ref()
.unwrap()
.fallback_input
.is_empty()
);
}
#[test]
fn shift_alt_and_shift_control_modal_text_keys_do_not_insert_text() {
let mut custom = MissionControlState::default();
custom.open_connect_provider(Vec::new(), None);
custom.start_connect_custom_configuration();
assert_eq!(
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::SHIFT | KeyModifiers::ALT),
&mut custom,
),
InputAction::None
);
assert_eq!(
handle_key(
modified_key(
KeyCode::Char('a'),
KeyModifiers::SHIFT | KeyModifiers::CONTROL,
),
&mut custom,
),
InputAction::None
);
assert_eq!(
custom
.modals
.connect_provider
.as_ref()
.unwrap()
.label
.value(),
""
);
let mut oauth = MissionControlState::default();
oauth.open_connect_provider(Vec::new(), None);
oauth.start_connect_oauth("openai-codex".to_string());
assert_eq!(
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::SHIFT | KeyModifiers::ALT),
&mut oauth,
),
InputAction::None
);
assert_eq!(
handle_key(
modified_key(
KeyCode::Char('a'),
KeyModifiers::SHIFT | KeyModifiers::CONTROL,
),
&mut oauth,
),
InputAction::None
);
assert_eq!(
oauth
.modals
.connect_provider
.as_ref()
.unwrap()
.fallback_input
.value(),
""
);
}
#[test]
fn logout_confirmation_keys_confirm_cancel_and_own_input() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("draft", 0),
..Default::default()
};
state.open_logout_confirmation("openai-codex".to_string(), "OpenAI Codex".to_string());
assert!(matches!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::ConfirmLogout(provider) if provider == "openai-codex"
));
assert!(state.modals.logout_confirmation.is_none());
assert_eq!(state.prompt_plain_text(), "draft");
state.open_logout_confirmation("openai-codex".to_string(), "OpenAI Codex".to_string());
assert_eq!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::CancelLogout
);
assert!(state.modals.logout_confirmation.is_none());
assert_eq!(state.prompt_plain_text(), "draft");
}
#[test]
fn logout_confirmation_requires_drawable_action_row_and_escape_always_closes() {
for (width, height) in [(11, 4), (12, 5), (12, 7)] {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("draft", 0),
..Default::default()
};
state.open_logout_confirmation("openai-codex".to_string(), "OpenAI Codex".to_string());
let area = ratatui::layout::Rect::new(0, 0, width, height);
let viewports = crate::tui::viewports::key_viewports(area, &state);
assert!(
viewports.logout_confirmation_action_row.is_none(),
"logout action row should be hidden at {width}x{height}"
);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Enter), &mut state, viewports, &[]),
InputAction::None
);
assert!(
state.modals.logout_confirmation.is_some(),
"hidden logout action must not remove credentials at {width}x{height}"
);
assert_eq!(state.prompt_plain_text(), "draft");
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Esc), &mut state, viewports, &[]),
InputAction::CancelLogout
);
assert!(state.modals.logout_confirmation.is_none());
}
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("draft", 0),
..Default::default()
};
state.open_logout_confirmation("openai-codex".to_string(), "OpenAI Codex".to_string());
let area = ratatui::layout::Rect::new(0, 0, 90, 20);
let viewports = crate::tui::viewports::key_viewports(area, &state);
assert_eq!(
viewports.logout_confirmation_action_row,
Some(ratatui::layout::Rect::new(19, 12, 52, 1))
);
assert!(matches!(
handle_key_with_all_viewports(key(KeyCode::Enter), &mut state, viewports, &[]),
InputAction::ConfirmLogout(provider) if provider == "openai-codex"
));
assert!(state.modals.logout_confirmation.is_none());
}
#[test]
fn connect_provider_replacement_keys_confirm_cancel_and_own_input() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("draft", 0),
..Default::default()
};
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
state.open_connect_provider_replacement_confirmation(
"custom-local".to_string(),
"Old Local".to_string(),
"New Local".to_string(),
"https://example.test".to_string(),
Some("LOCAL_API_KEY".to_string()),
);
assert_eq!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::ConfirmConnectProviderReplacement
);
assert!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.replacement
.is_some()
);
assert_eq!(state.prompt_plain_text(), "draft");
assert_eq!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::CancelConnectProviderReplacement
);
assert!(state.modals.connect_provider.is_some());
assert_eq!(state.prompt_plain_text(), "draft");
}
#[test]
fn connect_provider_custom_keys_emit_edit_and_submit_actions() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("draft", 0),
..Default::default()
};
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
assert_eq!(
handle_key(key(KeyCode::Char('L')), &mut state),
InputAction::InsertConnectProviderChar('L')
);
assert!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.label
.is_empty()
);
state.insert_modal_field_char('L');
assert_eq!(state.prompt_plain_text(), "draft");
assert_eq!(
handle_key(key(KeyCode::Backspace), &mut state),
InputAction::BackspaceConnectProviderField
);
state.backspace_modal_field();
assert!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.label
.is_empty()
);
assert_eq!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::SubmitConnectProviderStep
);
assert_eq!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::CancelConnectProvider
);
assert!(state.modals.connect_provider.is_some());
assert_eq!(state.prompt_plain_text(), "draft");
}
#[test]
fn modal_fields_emit_cursor_editing_and_unicode_actions() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
for ch in "a界c".chars() {
assert_eq!(
handle_key(key(KeyCode::Char(ch)), &mut state),
InputAction::InsertConnectProviderChar(ch)
);
}
assert_eq!(
handle_key(key(KeyCode::Left), &mut state),
InputAction::MoveConnectProviderFieldLeft
);
assert_eq!(
handle_key(key(KeyCode::Char('🙂')), &mut state),
InputAction::InsertConnectProviderChar('🙂')
);
assert_eq!(
handle_key(key(KeyCode::Home), &mut state),
InputAction::MoveConnectProviderFieldHome
);
assert_eq!(
handle_key(key(KeyCode::Delete), &mut state),
InputAction::DeleteConnectProviderField
);
assert_eq!(
handle_key(key(KeyCode::End), &mut state),
InputAction::MoveConnectProviderFieldEnd
);
assert_eq!(
handle_key(key(KeyCode::Backspace), &mut state),
InputAction::BackspaceConnectProviderField
);
let field = &state.modals.connect_provider.as_ref().unwrap().label;
assert!(field.is_empty());
assert_eq!(field.cursor(), 0);
}
#[test]
fn modal_paste_normalizes_lines_without_touching_prompt_or_status() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("draft", 0),
status: "waiting for OAuth input".to_string(),
..Default::default()
};
state.open_connect_provider(Vec::new(), None);
state.start_connect_oauth("openai-codex".to_string());
let action = handle_paste_with_viewport("first\r\nsecond\n界", &mut state, None, &[]);
let InputAction::InsertConnectProviderText(text) = action else {
panic!("expected typed modal paste action");
};
assert_eq!(format!("{text:?}"), "<redacted>");
state.insert_modal_field_text(&text.into_inner());
let oauth = state.modals.connect_provider.as_ref().unwrap();
assert_eq!(oauth.fallback_input.value(), "first second 界");
assert_eq!(state.prompt_plain_text(), "draft");
assert_eq!(state.status, "waiting for OAuth input");
}
#[test]
fn tab_controls_custom_provider_fields_without_entering_field_text() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
assert_eq!(
handle_key(key(KeyCode::Tab), &mut state),
InputAction::FocusNextConnectProviderField
);
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.selected_field,
crate::tui::state::ConnectProviderField::Label
);
state.connect_provider_next_field();
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.selected_field,
crate::tui::state::ConnectProviderField::BaseUrl
);
assert_eq!(
handle_key(key(KeyCode::BackTab), &mut state),
InputAction::FocusPreviousConnectProviderField
);
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.selected_field,
crate::tui::state::ConnectProviderField::BaseUrl
);
state.connect_provider_previous_field();
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.selected_field,
crate::tui::state::ConnectProviderField::Label
);
}
#[test]
fn saving_connect_provider_ignores_escape_until_worker_finishes() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
state.set_connect_provider_saving("local-ai".to_string());
assert_eq!(handle_key(key(KeyCode::Esc), &mut state), InputAction::None);
assert_eq!(
state.modals.connect_provider.as_ref().unwrap().stage,
crate::tui::state::ConnectProviderStage::Saving
);
}
#[test]
fn login_step_tabs_do_not_allow_skipping_to_future_steps() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
for code in [KeyCode::Tab, KeyCode::Right, KeyCode::Char('3')] {
assert_eq!(handle_key(key(code), &mut state), InputAction::None);
assert_eq!(
state.modals.connect_provider.as_ref().unwrap().stage,
ConnectProviderStage::ChooseProvider
);
}
state.start_connect_oauth("openai-codex".into());
assert_eq!(
handle_key(key(KeyCode::Tab), &mut state),
InputAction::FocusNextConnectProviderField
);
state.connect_provider_next_field();
assert_eq!(
state.modals.connect_provider.as_ref().unwrap().stage,
ConnectProviderStage::OAuth
);
}
#[test]
fn oauth_buttons_support_focus_activation_and_mouse_hit_testing() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_oauth("openai-codex".into());
state.set_connect_provider_instructions("https://example.test/auth".into(), "open".into());
assert_eq!(
handle_key(
modified_key(KeyCode::Char('o'), KeyModifiers::CONTROL),
&mut state
),
InputAction::None
);
state.connect_provider_next_field();
assert_eq!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::OpenOAuthUrl
);
assert_eq!(
handle_key(key(KeyCode::Char('x')), &mut state),
InputAction::None
);
state.connect_provider_next_field();
assert_eq!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::CopyOAuthUrl
);
state.connect_provider_previous_field();
assert_eq!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::OpenOAuthUrl
);
let area = ratatui::layout::Rect::new(0, 0, 80, 24);
let [open, copy] = crate::tui::render::connect_provider_oauth_button_areas(area).unwrap();
for (rect, action) in [
(open, InputAction::OpenOAuthUrl),
(copy, InputAction::CopyOAuthUrl),
] {
let mouse = crossterm::event::MouseEvent {
kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
column: rect.x,
row: rect.y,
modifiers: KeyModifiers::NONE,
};
assert_eq!(
super::super::mouse::classify_mouse_input(mouse, area, &state, 1),
InputCommand::action(action)
);
}
}
#[test]
fn custom_provider_fourth_field_edits_override_and_wraps_focus() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
for _ in 0..3 {
state.connect_provider_next_field();
}
state.insert_modal_field_text("openai");
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.models_dev_provider
.value(),
"openai"
);
state.connect_provider_next_field();
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.selected_field,
crate::tui::state::ConnectProviderField::Label
);
state.connect_provider_previous_field();
assert_eq!(
state
.modals
.connect_provider
.as_ref()
.unwrap()
.selected_field,
crate::tui::state::ConnectProviderField::ModelsDevProvider
);
}
#[test]
fn compact_login_accepts_custom_fields_and_oauth_submission() {
let area = ratatui::layout::Rect::new(0, 0, 80, 24);
let viewports = KeyViewports {
connect_provider_modal_visible: crate::tui::render::connect_provider_modal_visible(area),
connect_provider_custom_field_visible:
crate::tui::render::connect_provider_custom_field_visible(area),
connect_provider_oauth_controls_visible:
crate::tui::render::connect_provider_oauth_controls_visible(area),
connect_provider_oauth_fallback_visible:
crate::tui::render::connect_provider_oauth_fallback_visible(area),
..Default::default()
};
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
for value in [
"Local",
"https://example.test/v1",
"LOCAL_API_KEY",
"openai",
] {
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Char('x')), &mut state, viewports, &[]),
InputAction::InsertConnectProviderChar('x')
);
state.insert_modal_field_text(value);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Tab), &mut state, viewports, &[]),
InputAction::FocusNextConnectProviderField
);
state.connect_provider_next_field();
}
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Enter), &mut state, viewports, &[]),
InputAction::SubmitConnectProviderStep
);
let connect = state.modals.connect_provider.as_ref().unwrap();
assert_eq!(connect.label.value(), "Local");
assert_eq!(connect.base_url.value(), "https://example.test/v1");
assert_eq!(connect.api_key_env_var.value(), "LOCAL_API_KEY");
assert_eq!(connect.models_dev_provider.value(), "openai");
state.start_connect_oauth("openai-codex".into());
state.set_connect_provider_instructions("https://example.test/auth".into(), "open".into());
for (code, action) in [
(KeyCode::PageDown, InputAction::ScrollOAuthUrl(1)),
(KeyCode::PageUp, InputAction::ScrollOAuthUrl(-1)),
] {
assert_eq!(
handle_key_with_all_viewports(key(code), &mut state, viewports, &[]),
action
);
}
state.insert_modal_field_text("test-code");
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Enter), &mut state, viewports, &[]),
InputAction::SubmitOAuthFallback
);
state.connect_provider_next_field();
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Enter), &mut state, viewports, &[]),
InputAction::OpenOAuthUrl
);
state.connect_provider_next_field();
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Enter), &mut state, viewports, &[]),
InputAction::CopyOAuthUrl
);
}