use super::*;
#[test]
fn picker_clamp_resets_empty_lists() {
let mut session = SessionPickerState {
rows: Vec::new(),
selected: 4,
focus: SessionPickerFocus::List,
};
session.clamp();
assert_eq!(session.selected, 0);
let mut skills = SkillsModalState {
rows: Vec::new(),
selected: 4,
settings_scope: crate::config::SettingsScope::Global,
scope_label: "Global".to_string(),
};
skills.clamp();
assert_eq!(skills.selected, 0);
}
#[test]
fn picker_clamp_bounds_selected_past_end_for_all_pickers() {
assert_all_picker_clamps(9, 0, 2, (2, 1));
}
#[test]
fn picker_clamp_moves_scroll_up_when_selected_is_above_visible_window_for_all_pickers() {
assert_all_picker_clamps(1, 2, 2, (1, 1));
}
#[test]
fn picker_clamp_moves_scroll_up_when_selected_is_below_visible_window_for_all_pickers() {
assert_all_picker_clamps(2, 0, 2, (2, 1));
}
#[test]
fn picker_clamp_treats_zero_visible_rows_as_one_for_all_pickers() {
assert_all_picker_clamps(1, 0, 0, (1, 1));
}
#[test]
fn matching_session_title_update_updates_footer_and_picker() {
let mut state = MissionControlState {
footer_session_id: Some("session-abcdef12".to_string()),
footer_session: "abcd...ef12".to_string(),
..MissionControlState::default()
};
state.open_session_picker(
vec![
SessionPickerRow {
id: "session-abcdef12".to_string(),
label: "abcd...ef12".to_string(),
is_current: true,
preview: None,
},
SessionPickerRow {
id: "session-other99".to_string(),
label: "othe...er99".to_string(),
is_current: false,
preview: None,
},
],
crate::tui::input::DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
state.apply_session_title_update("session-abcdef12", "Friendly title");
assert_eq!(state.footer_session, "Friendly title [sess...ef12]");
let rows = &state.modals.session_picker.as_ref().unwrap().rows;
assert_eq!(rows[0].label, "Friendly title [sess...ef12]");
assert_eq!(rows[1].label, "othe...er99");
}
#[test]
fn stale_session_title_update_does_not_update_active_footer() {
let mut state = MissionControlState {
footer_session_id: Some("session-active1".to_string()),
footer_session: "acti...ive1".to_string(),
..MissionControlState::default()
};
state.open_session_picker(
vec![SessionPickerRow {
id: "session-stale99".to_string(),
label: "stal...le99".to_string(),
is_current: false,
preview: None,
}],
crate::tui::input::DEFAULT_PICKER_VISIBLE_ROWS as usize,
);
state.apply_session_title_update("session-stale99", "Old run title");
assert_eq!(state.footer_session, "acti...ive1");
assert_eq!(
state.modals.session_picker.as_ref().unwrap().rows[0].label,
"Old run title [sess...le99]"
);
}
#[test]
fn session_picker_open_hides_autocomplete_and_clamps_empty_state() {
let mut state = MissionControlState {
autocomplete: Some(AutocompleteState {
candidates: Vec::new(),
selected: 0,
token_start: 0,
token_end: 2,
token: "/s".into(),
}),
..Default::default()
};
state.open_session_picker(Vec::new(), 3);
let picker = state.modals.session_picker.as_ref().unwrap();
assert_eq!(picker.selected, 0);
assert_eq!(state.selected_session_id(), None);
assert!(!state.autocomplete_visible());
}
#[test]
fn session_picker_keyboard_navigation_keeps_selection_visible() {
let mut state = MissionControlState::default();
state.open_session_picker(
(0..5)
.map(|index| SessionPickerRow {
id: format!("session-{index}"),
label: format!("session-{index}"),
is_current: false,
preview: None,
})
.collect(),
2,
);
for _ in 0..3 {
state.session_picker_down(2);
}
let picker = state.modals.session_picker.as_ref().unwrap();
assert_eq!(picker.selected, 3);
assert!(state.scroll_offset(&state.scroll_views.session_list) + 2 > picker.selected);
}
#[test]
fn session_picker_scroll_clamps_at_bounds() {
let mut state = MissionControlState::default();
state.open_session_picker(
(0..2)
.map(|index| SessionPickerRow {
id: format!("session-{index}"),
label: format!("session-{index}"),
is_current: false,
preview: None,
})
.collect(),
1,
);
assert!(!state.session_picker_up(1));
state.session_picker_down(1);
assert!(!state.session_picker_down(1));
let picker = state.modals.session_picker.as_ref().unwrap();
assert_eq!(
(
picker.selected,
state.scroll_offset(&state.scroll_views.session_list)
),
(1, 1)
);
}
#[test]
fn reset_for_new_session_closes_session_picker() {
let mut state = MissionControlState::default();
state.open_session_picker(
vec![SessionPickerRow {
id: "abc".into(),
label: "abc".into(),
is_current: false,
preview: None,
}],
1,
);
state.reset_for_new_session();
assert!(!state.session_picker_visible());
}
#[test]
fn reset_for_new_session_clears_previous_session_display_state() {
let mut state = MissionControlState {
selected: 1,
context_usage: Some(ContextUsageState {
current_tokens: 12,
max_tokens: 34,
reasoning_tokens: None,
source: ContextUsageSource::FallbackEstimate,
request_sequence: 0,
}),
status: "keep status".to_string(),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
show_help: true,
autocomplete: Some(AutocompleteState {
candidates: vec![AutocompleteCandidate::slash_command(
"new",
"start a new session",
)],
selected: 0,
token_start: 0,
token_end: 2,
token: "/n".to_string(),
}),
autocomplete_dismissed_token: Some("/n".to_string()),
..Default::default()
};
state.apply_output_event(&OutputEvent::UserPrompt {
text: "old prompt".to_string(),
});
let root = ActivityId::new("root");
let child = ActivityId::new("child");
state.apply_activity_event(ActivityEvent::Started {
id: root.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("root"),
});
state.apply_activity_event(ActivityEvent::Started {
id: child,
parent_id: Some(root),
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("child"),
});
state.reset_for_new_session();
assert!(state.nodes.is_empty());
assert!(state.roots.is_empty());
assert!(state.children.is_empty());
assert!(state.expanded.is_empty());
assert!(state.transcript.is_empty());
assert!(state.context_usage.is_none());
assert_eq!(state.selected_activity_text(), "No activity selected.");
assert_eq!(state.selected, 0);
assert_eq!(state.transcript_scroll_offset(), 0);
assert_eq!(state.activity_scroll_offset(), 0);
assert_eq!(state.detail_offset(), 0);
assert_eq!(state.help_offset(), 0);
assert!(!state.autocomplete_visible());
assert!(state.autocomplete_dismissed_token.is_none());
assert_eq!(state.status, "keep status");
assert!(state.is_prompt_focused());
assert!(state.show_help);
}
#[test]
fn prompt_up_down_move_between_wrapped_rows_and_clamp_column() {
let mut state = MissionControlState {
input: "abcdefghijkl".to_string(),
prompt_cursor: 2,
..Default::default()
};
state.move_prompt_cursor_down(2, 4);
assert_eq!(state.prompt_cursor, 6);
state.move_prompt_cursor_down(2, 4);
assert_eq!(state.prompt_cursor, 10);
state.move_prompt_cursor_down(2, 4);
assert_eq!(state.prompt_cursor, 10);
state.move_prompt_cursor_up(2, 4);
assert_eq!(state.prompt_cursor, 6);
}
#[test]
fn prompt_manual_scroll_clamps_without_changing_cursor_or_input() {
let mut state = MissionControlState {
input: "x".repeat(20),
prompt_cursor: 2,
..Default::default()
};
assert!(state.scroll_prompt_down_by(10, 2, 4));
assert_eq!(state.prompt_offset(), 3);
assert_eq!(state.prompt_cursor, 2);
assert_eq!(state.input, "x".repeat(20));
assert!(!state.scroll_prompt_down_by(10, 2, 4));
assert!(state.scroll_prompt_up_by(10, 2, 4));
assert_eq!(state.prompt_offset(), 0);
}
#[test]
fn transcript_mouse_scroll_clamps_stored_scroll_and_reports_boundaries() {
let mut state = MissionControlState::default();
for line in 0..6 {
state.push_transcript(format!("line {line}"));
}
let expected_overflow = state.transcript_visual_rows(80).saturating_sub(4);
assert_eq!(state.transcript_scroll_overflow(4, 80), expected_overflow);
while state.scroll_transcript_mouse_up(4, 80) {}
assert_eq!(state.transcript_scroll_offset(), expected_overflow);
assert!(!state.scroll_transcript_mouse_up(4, 80));
assert_eq!(state.transcript_scroll_offset(), expected_overflow);
while state.scroll_transcript_mouse_down(4, 80) {}
assert_eq!(state.transcript_scroll_offset(), 0);
assert!(!state.scroll_transcript_mouse_down(4, 80));
}
#[test]
fn detail_mouse_scroll_clamps_to_wrapped_overflow() {
let mut state = MissionControlState::default();
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("tool-1"),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("long detail"),
});
state.apply_activity_event(ActivityEvent::Delta {
id: ActivityId::new("tool-1"),
preview: "abcdefghij".repeat(10),
});
let overflow = state.detail_overflow(3, 10);
assert!(overflow >= usize::from(MOUSE_WHEEL_SCROLL_ROWS));
assert!(state.scroll_detail_mouse_down(3, 10));
assert_eq!(state.detail_offset(), usize::from(MOUSE_WHEEL_SCROLL_ROWS));
while state.scroll_detail_mouse_down(3, 10) {}
assert_eq!(state.detail_offset(), overflow);
assert!(!state.scroll_detail_mouse_down(3, 10));
assert_eq!(state.detail_offset(), overflow);
assert!(state.scroll_detail_up_by(MOUSE_WHEEL_SCROLL_ROWS, 3, 10));
}
#[test]
fn help_clamps_to_wrapped_overflow() {
let mut state = MissionControlState::default();
let help_text = (0..12)
.map(|line| format!("help line {line}"))
.collect::<Vec<_>>()
.join("\n");
let overflow = state.help_overflow(4, 80, &help_text);
assert!(overflow >= usize::from(MOUSE_WHEEL_SCROLL_ROWS));
assert_eq!(state.help_render_offset(4, 80, &help_text), 0);
assert!(state.scroll_help_down_by(MOUSE_WHEEL_SCROLL_ROWS, 4, 80, &help_text));
assert_eq!(state.help_offset(), usize::from(MOUSE_WHEEL_SCROLL_ROWS));
assert_eq!(
state.help_render_offset(4, 80, &help_text),
usize::from(MOUSE_WHEEL_SCROLL_ROWS)
);
while state.scroll_help_down_by(MOUSE_WHEEL_SCROLL_ROWS, 4, 80, &help_text) {}
assert_eq!(state.help_offset(), overflow);
assert!(!state.scroll_help_down_by(MOUSE_WHEEL_SCROLL_ROWS, 4, 80, &help_text));
assert!(state.scroll_help_up_by(MOUSE_WHEEL_SCROLL_ROWS, 4, 80, &help_text));
assert_eq!(
state.help_offset(),
overflow.saturating_sub(usize::from(MOUSE_WHEEL_SCROLL_ROWS))
);
}
#[test]
fn keyboard_detail_keeps_five_row_step_and_render_clamps() {
let mut state = MissionControlState::default();
state.apply_activity_event(ActivityEvent::Started {
id: ActivityId::new("tool-1"),
parent_id: None,
kind: ActivityKind::Tool,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new("detail"),
});
state.apply_activity_event(ActivityEvent::Delta {
id: ActivityId::new("tool-1"),
preview: "detail row 1\ndetail row 2".to_string(),
});
state.scroll_detail_down();
assert_eq!(
state.detail_offset(),
usize::from(KEYBOARD_DETAIL_SCROLL_ROWS)
);
assert_eq!(state.detail_offset(), 1);
state.scroll_detail_up();
assert_eq!(state.detail_offset(), 0);
}
#[test]
fn autocomplete_selection_clamps_or_resets_after_filtering_changes() {
let candidates = autocomplete_candidates();
let mut state = MissionControlState {
input: "/q".to_string(),
prompt_cursor: 2,
..Default::default()
};
state.recompute_autocomplete(&candidates);
state.move_autocomplete_selection_down();
assert_eq!(state.autocomplete.as_ref().unwrap().selected, 0);
state.input = "/n".to_string();
state.prompt_cursor = 2;
state.recompute_autocomplete(&candidates);
let autocomplete = state.autocomplete.as_ref().unwrap();
assert_eq!(autocomplete.candidates.len(), 1);
assert_eq!(autocomplete.candidates[0].name, "new");
assert_eq!(autocomplete.selected, 0);
}