use super::super::*;
use super::support::*;
#[test]
fn skills_modal_keys_toggle_and_do_not_edit_prompt() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("keep", 0),
..Default::default()
};
state.set_scroll_offset(&state.scroll_views.transcript, 3);
state.open_skills_modal(
vec![
crate::tui::state::SkillToggleRow {
name: "a".to_string(),
source: "test".to_string(),
enabled: true,
},
crate::tui::state::SkillToggleRow {
name: "b".to_string(),
source: "test".to_string(),
enabled: true,
},
],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
assert!(matches!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
));
match handle_key(key(KeyCode::Enter), &mut state) {
InputAction::SetSkillEnabled { name, enabled } => {
assert_eq!(name, "b");
assert!(!enabled);
}
other => panic!("unexpected action: {other:?}"),
}
assert_eq!(state.prompt_plain_text(), "keep");
assert_eq!(state.transcript_scroll_offset(), 3);
assert!(state.skills_modal_visible());
assert!(matches!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::None
));
assert!(!state.skills_modal_visible());
}
#[test]
fn tools_modal_keys_toggle_and_do_not_edit_prompt() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("keep", 0),
..Default::default()
};
state.set_scroll_offset(&state.scroll_views.transcript, 3);
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: true,
},
],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
assert!(matches!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
));
match handle_key(key(KeyCode::Enter), &mut state) {
InputAction::SetToolEnabled { name, enabled } => {
assert_eq!(name, "mcp__mock__echo");
assert!(!enabled);
}
other => panic!("unexpected action: {other:?}"),
}
assert_eq!(state.prompt_plain_text(), "keep");
assert_eq!(state.transcript_scroll_offset(), 3);
assert!(state.tools_modal_visible());
assert!(matches!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::None
));
assert!(!state.tools_modal_visible());
}
#[test]
fn subagents_modal_keys_toggle_and_do_not_edit_prompt() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("keep", 0),
..Default::default()
};
state.set_scroll_offset(&state.scroll_views.transcript, 3);
state.open_subagents_modal(
vec![
crate::tui::state::SubagentProfileToggleRow {
id: "a".to_string(),
description: "A".to_string(),
enabled: true,
},
crate::tui::state::SubagentProfileToggleRow {
id: "b".to_string(),
description: "B".to_string(),
enabled: true,
},
],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
assert!(matches!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
));
match handle_key(key(KeyCode::Enter), &mut state) {
InputAction::SetSubagentProfileEnabled { id, enabled } => {
assert_eq!(id, "b");
assert!(!enabled);
}
other => panic!("unexpected action: {other:?}"),
}
assert_eq!(state.prompt_plain_text(), "keep");
assert_eq!(state.transcript_scroll_offset(), 3);
assert!(state.subagents_modal_visible());
assert!(matches!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::None
));
assert!(!state.subagents_modal_visible());
}
#[test]
fn models_modal_keys_toggle_move_close_and_do_not_edit_prompt() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("keep", 0),
..Default::default()
};
state.set_scroll_offset(&state.scroll_views.transcript, 3);
state.open_models_modal_scoped(
vec![
crate::tui::state::ModelToggleRow {
id: "openai/a".to_string(),
provider: "openai".to_string(),
display_name: "A".to_string(),
enabled: true,
},
crate::tui::state::ModelToggleRow {
id: "openai/b".to_string(),
provider: "openai".to_string(),
display_name: "B".to_string(),
enabled: true,
},
],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
crate::config::SettingsScope::Global,
"Global".to_string(),
false,
);
assert_eq!(
handle_key(key(KeyCode::Tab), &mut state),
InputAction::ToggleModelsSettingsScope
);
assert!(matches!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
));
assert!(matches!(
handle_key(key(KeyCode::Up), &mut state),
InputAction::None
));
assert!(matches!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
));
match handle_key(key(KeyCode::Enter), &mut state) {
InputAction::SetModelEnabled { id, enabled } => {
assert_eq!(id, "openai/b");
assert!(!enabled);
}
other => panic!("unexpected action: {other:?}"),
}
assert_eq!(state.prompt_plain_text(), "keep");
assert_eq!(state.transcript_scroll_offset(), 3);
assert!(state.models_modal_visible());
assert!(matches!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::None
));
assert!(!state.models_modal_visible());
}
#[test]
fn models_modal_enter_on_loading_or_empty_is_noop_and_consumes_input() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("draft", 0),
..Default::default()
};
state.open_models_modal_scoped(
Vec::new(),
DEFAULT_PICKER_VISIBLE_ROWS as usize,
crate::config::SettingsScope::Global,
"Global".to_string(),
true,
);
assert_eq!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::None
);
assert_eq!(state.prompt_plain_text(), "draft");
assert!(state.models_modal_visible());
state.set_models_rows(Vec::new(), false, DEFAULT_PICKER_VISIBLE_ROWS as usize);
assert_eq!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::None
);
assert_eq!(state.prompt_plain_text(), "draft");
}
#[test]
fn models_modal_uses_actual_viewport_to_keep_selection_visible() {
let mut state = MissionControlState::default();
state.open_models_modal_scoped(
(0..6)
.map(|index| crate::tui::state::ModelToggleRow {
id: format!("openai/gpt-{index}"),
provider: "openai".to_string(),
display_name: format!("GPT {index}"),
enabled: true,
})
.collect(),
2,
crate::config::SettingsScope::Global,
"Global".to_string(),
false,
);
for _ in 0..3 {
assert!(matches!(
handle_key_with_all_viewports(
key(KeyCode::Down),
&mut state,
KeyViewports {
models_modal: Some(PickerViewport { visible_rows: 2 }),
..KeyViewports::default()
},
&[]
),
InputAction::None
));
}
let modal = state.modals.models_modal.as_ref().unwrap();
assert_eq!(modal.selected, 3);
assert_eq!(state.scroll_offset(&state.scroll_views.models_modal), 2);
}
#[test]
fn tab_in_settings_modals_returns_scope_toggle_actions() {
let mut skills = MissionControlState::default();
skills.open_skills_modal(
vec![crate::tui::state::SkillToggleRow {
name: "a".to_string(),
source: "test".to_string(),
enabled: true,
}],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
assert_eq!(
handle_key(key(KeyCode::Tab), &mut skills),
InputAction::ToggleSkillsSettingsScope
);
let mut tools = MissionControlState::default();
tools.open_tools_modal(
vec![crate::tui::state::ToolToggleRow {
name: "bash".to_string(),
kind: "built-in".to_string(),
enabled: true,
}],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
assert_eq!(
handle_key(key(KeyCode::Tab), &mut tools),
InputAction::ToggleToolsSettingsScope
);
let mut subagents = MissionControlState::default();
subagents.open_subagents_modal(
vec![crate::tui::state::SubagentProfileToggleRow {
id: "reviewer".to_string(),
description: String::new(),
enabled: true,
}],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
assert_eq!(
handle_key(key(KeyCode::Tab), &mut subagents),
InputAction::ToggleSubagentsSettingsScope
);
let mut models = MissionControlState::default();
models.open_models_modal_scoped(
vec![crate::tui::state::ModelToggleRow {
id: "openai/gpt-test".to_string(),
provider: "openai".to_string(),
display_name: "GPT Test".to_string(),
enabled: true,
}],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
crate::config::SettingsScope::Global,
"Global".to_string(),
false,
);
assert_eq!(
handle_key(key(KeyCode::Tab), &mut models),
InputAction::ToggleModelsSettingsScope
);
}
#[test]
fn skills_modal_uses_actual_viewport_to_keep_selection_visible_and_clamps() {
let mut state = MissionControlState::default();
state.open_skills_modal(
(0..6)
.map(|index| crate::tui::state::SkillToggleRow {
name: format!("skill-{index}"),
source: "test".to_string(),
enabled: true,
})
.collect(),
2,
);
let viewports = KeyViewports {
skills_modal: Some(PickerViewport { visible_rows: 2 }),
..KeyViewports::default()
};
for _ in 0..3 {
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]),
InputAction::None
);
}
let modal = state.modals.skills_modal.as_ref().unwrap();
assert_eq!(modal.selected, 3);
assert_eq!(state.scroll_offset(&state.scroll_views.skills_modal), 2);
for _ in 0..10 {
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]);
}
assert_eq!(state.modals.skills_modal.as_ref().unwrap().selected, 5);
assert_eq!(state.scroll_offset(&state.scroll_views.skills_modal), 4);
for _ in 0..10 {
handle_key_with_all_viewports(key(KeyCode::Up), &mut state, viewports, &[]);
}
assert_eq!(state.modals.skills_modal.as_ref().unwrap().selected, 0);
assert_eq!(state.scroll_offset(&state.scroll_views.skills_modal), 0);
}
#[test]
fn mcp_modal_uses_actual_viewport_to_keep_selection_visible_and_clamps() {
let mut state = MissionControlState::default();
state.open_mcp_modal(
(0..6)
.map(|index| crate::tui::state::McpServerToggleRow {
name: format!("server-{index}"),
kind: "stdio",
enabled: true,
})
.collect(),
2,
);
let viewports = KeyViewports {
mcp_modal: Some(PickerViewport { visible_rows: 2 }),
..KeyViewports::default()
};
for _ in 0..3 {
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]);
}
assert_eq!(state.modals.mcp_modal.as_ref().unwrap().selected, 3);
assert_eq!(state.scroll_offset(&state.scroll_views.mcp_modal), 2);
for _ in 0..10 {
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]);
}
assert_eq!(state.modals.mcp_modal.as_ref().unwrap().selected, 5);
assert_eq!(state.scroll_offset(&state.scroll_views.mcp_modal), 4);
for _ in 0..10 {
handle_key_with_all_viewports(key(KeyCode::Up), &mut state, viewports, &[]);
}
assert_eq!(state.modals.mcp_modal.as_ref().unwrap().selected, 0);
assert_eq!(state.scroll_offset(&state.scroll_views.mcp_modal), 0);
}
#[test]
fn tools_modal_uses_actual_viewport_to_keep_selection_visible_and_clamps() {
let mut state = MissionControlState::default();
state.open_tools_modal(
(0..6)
.map(|index| crate::tui::state::ToolToggleRow {
name: format!("tool-{index}"),
kind: "built-in".to_string(),
enabled: true,
})
.collect(),
2,
);
let viewports = KeyViewports {
tools_modal: Some(PickerViewport { visible_rows: 2 }),
..KeyViewports::default()
};
for _ in 0..3 {
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]);
}
assert_eq!(state.modals.tools_modal.as_ref().unwrap().selected, 3);
assert_eq!(state.scroll_offset(&state.scroll_views.tools_modal), 2);
for _ in 0..10 {
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]);
}
assert_eq!(state.modals.tools_modal.as_ref().unwrap().selected, 5);
assert_eq!(state.scroll_offset(&state.scroll_views.tools_modal), 4);
for _ in 0..10 {
handle_key_with_all_viewports(key(KeyCode::Up), &mut state, viewports, &[]);
}
assert_eq!(state.modals.tools_modal.as_ref().unwrap().selected, 0);
assert_eq!(state.scroll_offset(&state.scroll_views.tools_modal), 0);
}
#[test]
fn subagents_modal_uses_actual_viewport_to_keep_selection_visible_and_clamps() {
let mut state = MissionControlState::default();
state.open_subagents_modal(
(0..6)
.map(|index| crate::tui::state::SubagentProfileToggleRow {
id: format!("agent-{index}"),
description: format!("Agent {index}"),
enabled: true,
})
.collect(),
2,
);
let viewports = KeyViewports {
subagents_modal: Some(PickerViewport { visible_rows: 2 }),
..KeyViewports::default()
};
for _ in 0..3 {
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]);
}
assert_eq!(state.modals.subagents_modal.as_ref().unwrap().selected, 3);
assert_eq!(state.scroll_offset(&state.scroll_views.subagents_modal), 2);
for _ in 0..10 {
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]);
}
assert_eq!(state.modals.subagents_modal.as_ref().unwrap().selected, 5);
assert_eq!(state.scroll_offset(&state.scroll_views.subagents_modal), 4);
for _ in 0..10 {
handle_key_with_all_viewports(key(KeyCode::Up), &mut state, viewports, &[]);
}
assert_eq!(state.modals.subagents_modal.as_ref().unwrap().selected, 0);
assert_eq!(state.scroll_offset(&state.scroll_views.subagents_modal), 0);
}
#[test]
fn mcp_modal_does_not_navigate_or_toggle_when_list_is_not_rendered() {
let mut state = MissionControlState::default();
state.open_mcp_modal(
vec![
crate::tui::state::McpServerToggleRow {
name: "server-a".to_string(),
kind: "stdio",
enabled: true,
},
crate::tui::state::McpServerToggleRow {
name: "server-b".to_string(),
kind: "http",
enabled: true,
},
],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
let viewports =
crate::tui::viewports::key_viewports(ratatui::layout::Rect::new(0, 0, 12, 5), &state);
assert!(viewports.mcp_modal.is_none());
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]),
InputAction::None
);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Up), &mut state, viewports, &[]),
InputAction::None
);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Enter), &mut state, viewports, &[]),
InputAction::None
);
let modal = state.modals.mcp_modal.as_ref().unwrap();
assert_eq!(modal.selected, 0);
assert!(modal.rows.iter().all(|row| row.enabled));
assert!(state.mcp_modal_visible());
}
#[test]
fn mcp_modal_uses_one_row_viewport_at_its_smallest_usable_height() {
let mut state = MissionControlState::default();
state.open_mcp_modal(
vec![
crate::tui::state::McpServerToggleRow {
name: "server-a".to_string(),
kind: "stdio",
enabled: true,
},
crate::tui::state::McpServerToggleRow {
name: "server-b".to_string(),
kind: "http",
enabled: true,
},
],
DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
let area = ratatui::layout::Rect::new(0, 0, 12, 10);
let viewports = crate::tui::viewports::key_viewports(area, &state);
assert_eq!(
crate::tui::render::mcp_modal_list_area(area).map(|area| area.height),
Some(1)
);
assert_eq!(
viewports.mcp_modal.map(|viewport| viewport.visible_rows),
Some(1)
);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Up), &mut state, viewports, &[]),
InputAction::None
);
assert_eq!(state.modals.mcp_modal.as_ref().unwrap().selected, 0);
assert_eq!(state.scroll_offset(&state.scroll_views.mcp_modal), 0);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]),
InputAction::None
);
let modal = state.modals.mcp_modal.as_ref().unwrap();
assert_eq!(modal.selected, 1);
assert_eq!(state.scroll_offset(&state.scroll_views.mcp_modal), 1);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Down), &mut state, viewports, &[]),
InputAction::None
);
assert_eq!(state.modals.mcp_modal.as_ref().unwrap().selected, 1);
assert_eq!(state.scroll_offset(&state.scroll_views.mcp_modal), 1);
assert_eq!(
handle_key_with_all_viewports(key(KeyCode::Enter), &mut state, viewports, &[]),
InputAction::SetMcpServerEnabled {
name: "server-b".to_string(),
enabled: false,
}
);
}