use super::super::*;
use super::support::*;
#[test]
fn autocomplete_tab_accepts_when_visible() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("/n", 2),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
state.recompute_autocomplete(&autocomplete_candidates());
assert!(state.autocomplete_visible());
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Tab), &mut state),
InputAction::None
));
assert_eq!(state.prompt_plain_text(), "/new");
assert!(state.is_prompt_focused());
}
#[test]
fn autocomplete_enter_accepts_when_visible_without_submitting() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("/n", 2),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
state.recompute_autocomplete(&autocomplete_candidates());
assert!(state.autocomplete_visible());
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Enter), &mut state),
InputAction::None
));
assert_eq!(state.prompt_plain_text(), "/new");
assert!(state.is_prompt_focused());
}
#[test]
fn autocomplete_arrows_navigate_when_visible_and_move_cursor_when_hidden() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("/q", 2),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
state.recompute_autocomplete(&autocomplete_candidates());
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Down), &mut state),
InputAction::None
));
assert_eq!(state.autocomplete.as_ref().unwrap().selected, 0);
assert_eq!(state.prompt_cursor(), 2);
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Up), &mut state),
InputAction::None
));
assert_eq!(state.autocomplete.as_ref().unwrap().selected, 0);
state.hide_autocomplete();
state.set_prompt_text("a\nb", "a\nb".len());
assert!(matches!(
handle_key(key(KeyCode::Up), &mut state),
InputAction::None
));
assert!(state.prompt_cursor() < state.prompt_plain_text().len());
}
#[test]
fn autocomplete_esc_dismisses_before_focus_and_help_behavior() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("/n", 2),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
show_help: true,
..Default::default()
};
state.recompute_autocomplete(&autocomplete_candidates());
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Esc), &mut state),
InputAction::None
));
assert!(!state.autocomplete_visible());
assert!(state.show_help);
assert!(state.is_prompt_focused());
state.focus_activity_tree();
assert!(matches!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::None
));
assert!(state.is_prompt_focused());
assert!(!state.show_help);
}
#[test]
fn autocomplete_reopens_after_editing_an_escaped_token() {
let candidates = vec![
AutocompleteCandidate::slash_command("new", "new session"),
AutocompleteCandidate::file_tag("src/main.rs"),
AutocompleteCandidate::skill_tag("rust-dev"),
AutocompleteCandidate::context_injection("tree", "directory tree"),
];
for trigger in ['/', '@', '$', '#'] {
for edit in [KeyCode::Backspace, KeyCode::Char('z')] {
let mut state = MissionControlState {
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
handle_key_with_viewport(key(KeyCode::Char(trigger)), &mut state, None, &candidates);
assert!(state.autocomplete_visible(), "trigger {trigger}");
handle_key_with_viewport(key(KeyCode::Esc), &mut state, None, &candidates);
state.recompute_autocomplete(&candidates);
assert!(!state.autocomplete_visible());
handle_key_with_viewport(key(edit), &mut state, None, &candidates);
assert!(!state.autocomplete_visible());
let restore = if edit == KeyCode::Backspace {
KeyCode::Char(trigger)
} else {
KeyCode::Backspace
};
handle_key_with_viewport(key(restore), &mut state, None, &candidates);
assert!(
state.autocomplete_visible(),
"trigger {trigger}, edit {edit:?}"
);
}
}
}
#[test]
fn autocomplete_recomputes_and_hides_after_prompt_edits() {
let mut state = MissionControlState {
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Char('/')), &mut state),
InputAction::None
));
assert!(state.autocomplete_visible());
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Char('n')), &mut state),
InputAction::None
));
assert_eq!(
state.autocomplete.as_ref().unwrap().candidates[0].name,
"new"
);
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Backspace), &mut state),
InputAction::None
));
assert!(state.autocomplete_visible());
assert!(matches!(
handle_key_with_candidates(key(KeyCode::Backspace), &mut state),
InputAction::None
));
assert!(state.autocomplete.is_none());
}
#[test]
fn file_autocomplete_tab_accepts_active_at_token_anywhere_without_toggling_focus() {
let candidates = vec![AutocompleteCandidate::file_tag("src/tui/input/mod.rs")];
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new("check @tuiinp", "check @tuiinp".len()),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
assert!(matches!(
handle_key_with_viewport(key(KeyCode::Tab), &mut state, None, &candidates),
InputAction::None
));
assert_eq!(state.prompt_plain_text(), "check @src/tui/input/mod.rs ");
assert!(state.is_prompt_focused());
}
#[test]
fn skill_autocomplete_accepts_active_dollar_token_anywhere_without_submitting() {
let candidates = vec![AutocompleteCandidate::skill_tag_with_argument_hint(
"rust-dev",
Some("path to review"),
)];
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new(
"please use $rust",
"please use $rust".len(),
),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
assert!(matches!(
handle_key_with_viewport(key(KeyCode::Enter), &mut state, None, &candidates),
InputAction::None
));
assert_eq!(state.prompt_plain_text(), "please use $rust-dev ");
assert!(state.is_prompt_focused());
}
#[test]
fn autocomplete_uses_visible_offsets_around_collapsed_paste_atoms() {
let payload = "one\ntwo\n$rust";
let candidates = vec![AutocompleteCandidate::skill_tag("rust-dev")];
let mut state = MissionControlState {
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
state.prompt_editor.insert_paste(payload, 1, 80);
state.recompute_autocomplete(&candidates);
assert!(!state.autocomplete_visible());
state.prompt_editor.insert_paste(" $ru", 1, 80);
state.recompute_autocomplete(&candidates);
let autocomplete = state.autocomplete.as_ref().expect("skill suggestions");
let label = "[Pasted #1 · 3 lines]";
assert_eq!(autocomplete.token_start, label.len() + 1);
assert_eq!(
autocomplete.token_end,
state.prompt_editor.visible_text().len()
);
assert!(matches!(
handle_key_with_viewport(key(KeyCode::Enter), &mut state, None, &candidates),
InputAction::None
));
assert_eq!(
state.prompt_editor.visible_text(),
format!("{label} $rust-dev ")
);
assert_eq!(state.prompt_plain_text(), "one\ntwo\n$rust $rust-dev ");
}
#[test]
fn submitted_skill_tags_remain_literal_prompt_text() {
let mut state = MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new(
"please use $rust-dev",
"please use $rust-dev".len(),
),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
match handle_key_with_viewport(key(KeyCode::Enter), &mut state, None, &[]) {
InputAction::Submit(prompt) => assert_eq!(prompt, "please use $rust-dev"),
other => panic!("expected submit, got {other:?}"),
}
assert!(state.autocomplete.is_none());
}
fn history_state(prefix: &str) -> MissionControlState {
MissionControlState {
prompt_editor: crate::tui::state::PromptEditor::new(prefix, prefix.len()),
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
prompt_history: serde_json::from_value(serde_json::json!({"entries": [
{"text": "check café tests", "count": 2, "last_used": 1},
{"text": "check $rust-dev tests", "count": 1, "last_used": 1}
]}))
.unwrap(),
..Default::default()
}
}
fn paint_history(state: &MissionControlState) {
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(100, 30)).unwrap();
terminal
.draw(|frame| crate::tui::render::draw(frame, state))
.unwrap();
}
#[test]
fn prompt_history_right_accepts_only_painted_suggestion_at_draft_end() {
let mut state = history_state("check café ");
handle_key_with_candidates(key(KeyCode::Right), &mut state);
assert_eq!(state.prompt_plain_text(), "check café ");
paint_history(&state);
assert_eq!(state.visible_prompt_history_suffix(), Some("tests"));
handle_key_with_candidates(key(KeyCode::Left), &mut state);
handle_key_with_candidates(key(KeyCode::Right), &mut state);
assert_eq!(state.prompt_plain_text(), "check café ");
paint_history(&state);
handle_key_with_candidates(key(KeyCode::Right), &mut state);
assert_eq!(state.prompt_plain_text(), "check café tests");
state.undo_prompt_edit();
assert_eq!(state.prompt_plain_text(), "check café ");
}
#[test]
fn prompt_history_escape_dismisses_until_edit_and_tab_enter_are_unchanged() {
let mut state = history_state("check café ");
paint_history(&state);
handle_key_with_candidates(key(KeyCode::Esc), &mut state);
paint_history(&state);
assert_eq!(state.visible_prompt_history_suffix(), None);
handle_key_with_candidates(key(KeyCode::Char('t')), &mut state);
paint_history(&state);
assert_eq!(state.visible_prompt_history_suffix(), Some("ests"));
handle_key_with_candidates(key(KeyCode::Tab), &mut state);
assert_eq!(state.prompt_plain_text(), "check café t");
let mut state = history_state("check café ");
paint_history(&state);
assert!(
matches!(handle_key_with_candidates(key(KeyCode::Enter), &mut state), InputAction::Submit(text) if text == "check café ")
);
}
#[test]
fn prompt_history_yields_to_token_autocomplete() {
let mut state = history_state("check $ru");
let candidates = vec![AutocompleteCandidate::skill_tag("rust-dev")];
state.recompute_autocomplete(&candidates);
assert!(state.autocomplete_visible());
paint_history(&state);
assert_eq!(state.visible_prompt_history_suffix(), None);
handle_key_with_candidates(key(KeyCode::Right), &mut state);
assert_eq!(state.prompt_plain_text(), "check $ru");
}