use super::*;
#[test]
fn sessions_modal_renders_title_help_current_marker_and_selection() {
let mut state = MissionControlState::default();
state.open_session_picker(
vec![
crate::tui::state::SessionPickerRow {
id: "abcd1234efgh".into(),
label: "abcd...efgh".into(),
is_current: true,
preview: None,
},
crate::tui::state::SessionPickerRow {
id: "wxyz9876lmno".into(),
label: "wxyz...lmno".into(),
is_current: false,
preview: None,
},
],
TEST_PICKER_VISIBLE_ROWS,
);
state.modals.session_picker.as_mut().unwrap().selected = 1;
let backend = TestBackend::new(120, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("Sessions"), "{rendered}");
assert!(rendered.contains("Mouse wheel scroll"), "{rendered}");
assert!(rendered.contains("● current abcd...efgh"), "{rendered}");
assert!(rendered.contains("> wxyz...lmno"), "{rendered}");
}
#[test]
fn sessions_modal_uses_two_columns_when_space_permits() {
let areas = sessions_modal_areas(ratatui::layout::Rect::new(0, 0, 100, 30)).unwrap();
let preview = areas.preview.unwrap();
assert!(areas.list.width < preview.width);
assert!(areas.list.height > 0);
assert!(preview.height > 0);
}
#[test]
fn sessions_modal_empty_state_and_small_terminal_do_not_panic() {
let mut state = MissionControlState::default();
state.open_session_picker(Vec::new(), TEST_PICKER_VISIBLE_ROWS);
let backend = TestBackend::new(120, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("No saved sessions found"), "{rendered}");
for (width, height) in [(10, 4), (12, 5)] {
let backend = TestBackend::new(width, height);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
}
}
#[test]
fn skills_modal_renders_title_toggles_help_selection_and_empty_state() {
let mut state = MissionControlState::default();
state.open_skills_modal(
vec![
crate::tui::state::SkillToggleRow {
name: "enabled-skill".to_string(),
source: "test".to_string(),
enabled: true,
},
crate::tui::state::SkillToggleRow {
name: "disabled-skill".to_string(),
source: "test".to_string(),
enabled: false,
},
],
TEST_PICKER_VISIBLE_ROWS,
);
state.modals.skills_modal.as_mut().unwrap().selected = 1;
let backend = TestBackend::new(80, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("Skills"), "{rendered}");
assert!(rendered.contains("[x] enabled-skill (test)"), "{rendered}");
assert!(rendered.contains("[ ] disabled-skill (test)"), "{rendered}");
assert!(rendered.contains("[Up]/[Down] move"), "{rendered}");
assert!(
rendered.contains("> [ ] disabled-skill (test)"),
"{rendered}"
);
state.open_skills_modal(Vec::new(), TEST_PICKER_VISIBLE_ROWS);
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("No skills discovered"), "{rendered}");
}
#[test]
fn skills_modal_small_viewports_do_not_panic_or_overflow() {
for (width, height) in [(10, 4), (12, 5)] {
let mut state = MissionControlState::default();
state.open_skills_modal(
vec![crate::tui::state::SkillToggleRow {
name: "tiny-skill".to_string(),
source: "test".to_string(),
enabled: true,
}],
TEST_PICKER_VISIBLE_ROWS,
);
let backend = TestBackend::new(width, height);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
}
}
#[test]
fn tools_modal_renders_title_toggles_kind_help_selection_and_empty_state() {
let mut state = MissionControlState::default();
state.open_tools_modal(
vec![
crate::tui::state::ToolToggleRow {
name: "bash".to_string(),
kind: "built-in".to_string(),
enabled: true,
},
crate::tui::state::ToolToggleRow {
name: "mcp__mock__echo".to_string(),
kind: "mcp:mock".to_string(),
enabled: false,
},
],
TEST_PICKER_VISIBLE_ROWS,
);
state.modals.tools_modal.as_mut().unwrap().selected = 1;
let backend = TestBackend::new(90, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("Tools"), "{rendered}");
assert!(rendered.contains("[x] bash (built-in)"), "{rendered}");
assert!(
rendered.contains("[ ] mcp__mock__echo (mcp:mock)"),
"{rendered}"
);
assert!(rendered.contains("> [ ] mcp__mock__echo"), "{rendered}");
state.open_tools_modal(Vec::new(), TEST_PICKER_VISIBLE_ROWS);
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("No tools available"), "{rendered}");
}
fn connect_provider_render_state(
stage: crate::tui::state::ConnectProviderStage,
) -> MissionControlState {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
match stage {
crate::tui::state::ConnectProviderStage::ChooseProvider => {}
crate::tui::state::ConnectProviderStage::ConfigureCustom => {
state.start_connect_custom_configuration();
}
crate::tui::state::ConnectProviderStage::AnthropicGuidance => {
state.start_connect_anthropic_guidance();
}
crate::tui::state::ConnectProviderStage::OAuth => {
state.start_connect_oauth("openai-codex".to_string());
}
crate::tui::state::ConnectProviderStage::ReplacementConfirmation => {
state.start_connect_custom_configuration();
state.open_connect_provider_replacement_confirmation(
"local-ai".to_string(),
"Old Local AI".to_string(),
"Local AI".to_string(),
"https://provider.example/v1".to_string(),
Some("LOCAL_AI_API_KEY".to_string()),
);
}
crate::tui::state::ConnectProviderStage::Saving => {
state.set_connect_provider_saving("local-ai".to_string());
}
crate::tui::state::ConnectProviderStage::Success => {
state.set_connect_provider_saving("local-ai".to_string());
assert!(state.set_connect_provider_success(
"local-ai",
"Local AI connected".to_string(),
false,
));
}
crate::tui::state::ConnectProviderStage::Error => {
state.start_connect_custom_configuration();
assert!(state.set_connect_provider_error("custom provider setup failed"));
}
}
state
}
#[test]
fn connect_provider_modal_stages_render_at_80x24_and_exact_minimum() {
let cases = [
(
crate::tui::state::ConnectProviderStage::ChooseProvider,
"Choose a provider",
Some("[Up]/[Down] move"),
),
(
crate::tui::state::ConnectProviderStage::ConfigureCustom,
"Configure custom provider metadata",
Some("[Tab] next field"),
),
(
crate::tui::state::ConnectProviderStage::AnthropicGuidance,
"Anthropic uses an API key",
Some("[Enter] back to providers"),
),
(
crate::tui::state::ConnectProviderStage::OAuth,
"Starting login…",
Some("[Ctrl-Y] copy URL"),
),
(
crate::tui::state::ConnectProviderStage::ReplacementConfirmation,
"Replace",
Some("[Enter] replace"),
),
(
crate::tui::state::ConnectProviderStage::Saving,
"Saving is in progress",
None,
),
(
crate::tui::state::ConnectProviderStage::Success,
"Local AI connected",
Some("[Enter] close"),
),
(
crate::tui::state::ConnectProviderStage::Error,
"Connection failed",
Some("[Enter] retry"),
),
];
assert_eq!(
modal_rect(
ratatui::layout::Rect::new(0, 0, 60, 18),
CONNECT_PROVIDER_MODAL,
),
Some(ratatui::layout::Rect::new(0, 0, 60, 18))
);
for (stage, content, controls) in cases {
for (width, height) in [(80, 24), (60, 18)] {
let state = connect_provider_render_state(stage);
let backend = TestBackend::new(width, height);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(
rendered.contains("Connect Provider"),
"stage={stage:?} size={width}x{height}: {rendered}"
);
assert!(
rendered.contains(content),
"stage={stage:?} size={width}x{height}: {rendered}"
);
if let Some(controls) = controls {
assert!(
rendered.contains(controls),
"stage={stage:?} size={width}x{height}: {rendered}"
);
}
}
}
}
#[test]
fn connect_provider_modal_is_safe_below_supported_minimum() {
assert!(
modal_rect(
ratatui::layout::Rect::new(0, 0, 59, 18),
CONNECT_PROVIDER_MODAL
)
.is_none()
);
assert!(
modal_rect(
ratatui::layout::Rect::new(0, 0, 60, 17),
CONNECT_PROVIDER_MODAL
)
.is_none()
);
let stages = [
crate::tui::state::ConnectProviderStage::ChooseProvider,
crate::tui::state::ConnectProviderStage::ConfigureCustom,
crate::tui::state::ConnectProviderStage::AnthropicGuidance,
crate::tui::state::ConnectProviderStage::OAuth,
crate::tui::state::ConnectProviderStage::ReplacementConfirmation,
crate::tui::state::ConnectProviderStage::Saving,
crate::tui::state::ConnectProviderStage::Success,
crate::tui::state::ConnectProviderStage::Error,
];
for stage in stages {
let state = connect_provider_render_state(stage);
for (width, height) in [(59, 18), (60, 17), (20, 10)] {
let backend = TestBackend::new(width, height);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
}
}
}
#[test]
fn connect_provider_oauth_modal_wraps_long_url_and_shows_keyboard_actions() {
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_oauth("openai-codex".to_string());
let url = "https://auth.openai.com/oauth/authorize?code_challenge=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz&originator=pi";
state.set_connect_provider_instructions(
url.to_string(),
format!(
"OpenAI Codex login: open this URL in your browser:\n{url}\nWaiting for browser callback. If callback cannot reach this terminal, paste the final redirect URL or authorization code into the active login prompt."
),
);
let backend = TestBackend::new(80, 24);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("[Ctrl-Y] copy URL"));
assert!(rendered.contains("[Ctrl-O] open browser"));
assert!(rendered.contains("Manual fallback is transient and redacted."));
assert!(rendered.contains("OAuth redirect or code"));
assert!(rendered.contains("paste redirect URL or authorization code"));
assert!(rendered.contains("originator=pi"));
assert_eq!(rendered.matches("https://auth.openai.com").count(), 1);
}
#[test]
fn connect_provider_custom_modal_shows_metadata_fields_and_redacts_no_key_value() {
let fake_secret = "sk-tui-custom-provider-secret-7f3c";
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
{
let connect = state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Local Llama");
connect.provider_id = Some("local-llama".to_string());
connect.selected_field = crate::tui::state::ConnectProviderField::ApiKeyEnvVar;
connect.error = Some("API key environment variable name must match".to_string());
}
let backend = TestBackend::new(90, 24);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("Configure custom provider metadata"));
assert!(rendered.contains("Provider label"));
assert!(rendered.contains("Base URL"));
assert!(rendered.contains("API key environment variable"));
assert!(rendered.contains("Error: API key environment variable name must match"));
assert!(
rendered.contains("no API key value is requested"),
"{rendered}"
);
assert_eq!(rendered.matches("API key value").count(), 1, "{rendered}");
assert!(!rendered.contains(fake_secret), "{rendered}");
}
#[test]
fn connect_provider_custom_renderer_masks_direct_secret_env_value_and_reports_error() {
let fake_secret = "sk-direct-render-secret";
let mut state = MissionControlState::default();
state.open_connect_provider(Vec::new(), None);
state.start_connect_custom_configuration();
{
let connect = state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Local Llama");
connect.base_url.set_value("https://provider.example/v1");
connect.selected_field = crate::tui::state::ConnectProviderField::ApiKeyEnvVar;
connect.api_key_env_var.set_value(fake_secret);
}
let backend = TestBackend::new(90, 24);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(!rendered.contains(fake_secret), "{rendered}");
assert!(rendered.contains("looks like a secret value"), "{rendered}");
assert!(rendered.contains('•'), "{rendered}");
}
#[test]
fn render_prompt_prompts_login_when_provider_is_not_ready() {
let state = MissionControlState {
provider: "openai-codex".to_string(),
model: "gpt-5.5".to_string(),
..Default::default()
};
let backend = TestBackend::new(100, 20);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let buffer = terminal.backend().buffer();
let metadata = find_text(buffer, "/login").unwrap();
let layout = layout_for(Rect::new(0, 0, 100, 20), &state);
assert_eq!(metadata.1, layout.prompt.y);
assert!(metadata.0 > layout.prompt.x);
assert!(find_text(buffer, "openai-codex/gpt-5.5").is_none());
}
#[test]
fn render_logout_confirmation_shows_provider_and_keyboard_controls() {
let mut state = MissionControlState {
provider: "openai-codex".to_string(),
model: "gpt-5.5".to_string(),
provider_ready: true,
..Default::default()
};
state.open_logout_confirmation("openai-codex".to_string(), "OpenAI Codex".to_string());
let backend = TestBackend::new(100, 24);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("Confirm Logout"));
assert!(rendered.contains("Remove local auth for OpenAI Codex (openai-codex)?"));
assert!(rendered.contains("[Enter] remove local auth"));
assert!(rendered.contains("[Esc] cancel"));
assert!(rendered.contains("[Alt-M] openai-codex/gpt-5.5"));
}
#[test]
fn render_logout_confirmation_requires_complete_target_before_action_row() {
let mut state = MissionControlState::default();
let label = "OpenAI Codex";
let provider_id = "openai-codex";
state.open_logout_confirmation(provider_id.to_string(), label.to_string());
for (width, action_visible) in [(12, false), (89, false), (90, true)] {
let area = Rect::new(0, 0, width, 7);
let backend = TestBackend::new(area.width, area.height);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = rendered_text_in_area(terminal.backend().buffer(), area);
assert!(rendered.contains("Remove"), "{width}x7: {rendered}");
let action_area = logout_confirmation_action_row_area(area, label, provider_id);
if action_visible {
let action_area = action_area.expect("action row geometry at the visible boundary");
assert!(
rendered_text_in_area(terminal.backend().buffer(), action_area).contains("[Enter]"),
"{width}x7: {rendered}"
);
} else {
assert!(action_area.is_none());
assert!(!rendered.contains("[Enter] remove local auth"));
}
}
}
#[test]
fn render_model_picker_modal_shows_current_model_and_available_models() {
let mut state = MissionControlState {
provider: "openai-codex".to_string(),
model: "gpt-current".to_string(),
..Default::default()
};
state.open_model_picker(
vec![
crate::model_catalog::ModelCatalogEntry::new_codex("gpt-current"),
crate::model_catalog::ModelCatalogEntry::new_codex("gpt-next"),
],
std::collections::BTreeSet::new(),
Some("using stale openai-codex model catalog".to_string()),
5,
);
let backend = TestBackend::new(100, 30);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("Model Picker"));
assert!(rendered.contains("current: openai-codex/gpt-current"));
assert!(rendered.contains("openai-codex/gpt-next"));
assert!(!rendered.contains("[disabled]"));
assert!(rendered.contains("using stale openai-codex model catalog"));
}
#[test]
fn models_modal_renders_title_help_scope_states_rows_and_selection() {
let mut state = MissionControlState::default();
state.open_models_modal_scoped(
vec![
crate::tui::state::ModelToggleRow {
id: "openai-codex/gpt-enabled".to_string(),
provider: "openai-codex".to_string(),
display_name: "Enabled".to_string(),
enabled: true,
},
crate::tui::state::ModelToggleRow {
id: "openai-codex/gpt-disabled".to_string(),
provider: "openai-codex".to_string(),
display_name: "Disabled".to_string(),
enabled: false,
},
],
TEST_PICKER_VISIBLE_ROWS,
crate::config::SettingsScope::Global,
"Global".to_string(),
false,
);
state.modals.models_modal.as_mut().unwrap().selected = 1;
let backend = TestBackend::new(110, 24);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("Models"), "{rendered}");
assert!(rendered.contains("[Tab] switch scope"), "{rendered}");
assert!(rendered.contains("Scope: Global"), "{rendered}");
assert!(
rendered.contains("[x] openai-codex/gpt-enabled Enabled"),
"{rendered}"
);
assert!(
rendered.contains("[ ] openai-codex/gpt-disabled Disabled [disabled]"),
"{rendered}"
);
assert!(
rendered.contains("> [ ] openai-codex/gpt-disabled"),
"{rendered}"
);
state.open_models_modal_scoped(
Vec::new(),
TEST_PICKER_VISIBLE_ROWS,
crate::config::SettingsScope::Global,
"Local".to_string(),
true,
);
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("Scope: Local"), "{rendered}");
assert!(rendered.contains("Loading models..."), "{rendered}");
state.set_models_rows(Vec::new(), false, TEST_PICKER_VISIBLE_ROWS);
terminal.draw(|frame| draw(frame, &state)).unwrap();
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("No models discovered"), "{rendered}");
}
#[test]
fn picker_redraw_preserves_scroll_state_and_keeps_offset_visible() {
let mut state = MissionControlState::default();
state.open_skills_modal(
(0..12)
.map(|index| crate::tui::state::SkillToggleRow {
name: format!("skill-{index}"),
source: "test".to_string(),
enabled: true,
})
.collect(),
TEST_PICKER_VISIBLE_ROWS,
);
state.modals.skills_modal.as_mut().unwrap().selected = 3;
state.set_scroll_offset(&state.scroll_views.skills_modal, 3);
let before = *state.scroll_views.skills_modal.borrow();
let backend = TestBackend::new(100, 24);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal.draw(|frame| draw(frame, &state)).unwrap();
assert_eq!(*state.scroll_views.skills_modal.borrow(), before);
let rendered = terminal
.backend()
.buffer()
.content()
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(rendered.contains("> [x] skill-3 (test)"), "{rendered}");
terminal.draw(|frame| draw(frame, &state)).unwrap();
assert_eq!(*state.scroll_views.skills_modal.borrow(), before);
}