use super::super::*;
use super::support::*;
#[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::CONTROL),
&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, 7);
let viewports = crate::tui::viewports::key_viewports(area, &state);
assert_eq!(
viewports.logout_confirmation_action_row,
Some(ratatui::layout::Rect::new(20, 4, 50, 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
);
}