use super::*;
use proptest::prelude::*;
#[test]
fn test_ai_suggestion_selection_complete_flow() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".initial");
app.focus = Focus::InputField;
app.input.editor_mode = EditorMode::Insert;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![
Suggestion {
query: ".name".to_string(),
description: "Get name field".to_string(),
suggestion_type: SuggestionType::Next,
},
Suggestion {
query: ".value".to_string(),
description: "Get value field".to_string(),
suggestion_type: SuggestionType::Optimize,
},
];
app.handle_key_event(key_with_mods(KeyCode::Down, KeyModifiers::ALT));
assert_eq!(app.ai.selection.get_selected(), Some(0));
assert!(app.ai.selection.is_navigation_active());
app.handle_key_event(key(KeyCode::Enter));
assert_eq!(app.query(), ".name");
assert!(app.ai.selection.get_selected().is_none());
assert!(!app.ai.selection.is_navigation_active());
assert!(!app.should_quit);
assert!(app.query.as_ref().unwrap().result.is_ok());
}
#[test]
fn test_ai_suggestion_direct_selection_alt_1() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".initial");
app.focus = Focus::InputField;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![
Suggestion {
query: ".first".to_string(),
description: "First suggestion".to_string(),
suggestion_type: SuggestionType::Next,
},
Suggestion {
query: ".second".to_string(),
description: "Second suggestion".to_string(),
suggestion_type: SuggestionType::Fix,
},
];
app.handle_key_event(key_with_mods(KeyCode::Char('1'), KeyModifiers::ALT));
assert_eq!(app.query(), ".first");
assert!(!app.should_quit);
}
#[test]
fn test_ai_suggestion_direct_selection_alt_2() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".initial");
app.focus = Focus::InputField;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![
Suggestion {
query: ".first".to_string(),
description: "First suggestion".to_string(),
suggestion_type: SuggestionType::Next,
},
Suggestion {
query: ".second".to_string(),
description: "Second suggestion".to_string(),
suggestion_type: SuggestionType::Fix,
},
];
app.handle_key_event(key_with_mods(KeyCode::Char('2'), KeyModifiers::ALT));
assert_eq!(app.query(), ".second");
assert!(!app.should_quit);
}
#[test]
fn test_ai_suggestion_multiple_selections_in_sequence() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".initial");
app.focus = Focus::InputField;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![
Suggestion {
query: ".first".to_string(),
description: "First suggestion".to_string(),
suggestion_type: SuggestionType::Next,
},
Suggestion {
query: ".second".to_string(),
description: "Second suggestion".to_string(),
suggestion_type: SuggestionType::Fix,
},
];
app.handle_key_event(key_with_mods(KeyCode::Char('1'), KeyModifiers::ALT));
assert_eq!(app.query(), ".first");
app.handle_key_event(key_with_mods(KeyCode::Char('2'), KeyModifiers::ALT));
assert_eq!(app.query(), ".second");
app.handle_key_event(key_with_mods(KeyCode::Down, KeyModifiers::ALT));
app.handle_key_event(key(KeyCode::Enter));
assert_eq!(app.query(), ".first");
assert!(!app.should_quit);
}
#[test]
fn test_ai_suggestion_selection_hides_autocomplete() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".na");
app.focus = Focus::InputField;
app.input.editor_mode = EditorMode::Insert;
let autocomplete_suggestions = vec![crate::autocomplete::Suggestion::new(
"name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete
.update_suggestions(autocomplete_suggestions);
assert!(app.autocomplete.is_visible());
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![Suggestion {
query: ".name | length".to_string(),
description: "Get name length".to_string(),
suggestion_type: SuggestionType::Next,
}];
app.handle_key_event(key_with_mods(KeyCode::Char('1'), KeyModifiers::ALT));
assert_eq!(app.query(), ".name | length");
assert!(!app.autocomplete.is_visible());
}
#[test]
fn test_ai_suggestion_selection_ignored_when_popup_hidden() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".initial");
app.focus = Focus::InputField;
app.ai.visible = false;
app.ai.enabled = true;
app.ai.suggestions = vec![Suggestion {
query: ".should_not_apply".to_string(),
description: "Should not apply".to_string(),
suggestion_type: SuggestionType::Next,
}];
app.handle_key_event(key_with_mods(KeyCode::Char('1'), KeyModifiers::ALT));
assert_eq!(app.query(), ".initial");
}
#[test]
fn test_ai_suggestion_selection_ignored_when_no_suggestions() {
let mut app = app_with_query(".initial");
app.focus = Focus::InputField;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![];
app.handle_key_event(key_with_mods(KeyCode::Char('1'), KeyModifiers::ALT));
assert_eq!(app.query(), ".initial");
}
#[test]
fn test_ai_suggestion_invalid_selection_ignored() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".initial");
app.focus = Focus::InputField;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![
Suggestion {
query: ".first".to_string(),
description: "First".to_string(),
suggestion_type: SuggestionType::Next,
},
Suggestion {
query: ".second".to_string(),
description: "Second".to_string(),
suggestion_type: SuggestionType::Fix,
},
];
app.handle_key_event(key_with_mods(KeyCode::Char('3'), KeyModifiers::ALT));
assert_eq!(app.query(), ".initial");
}
#[test]
fn test_ai_suggestion_navigation_stops_at_boundary() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".initial");
app.focus = Focus::InputField;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![
Suggestion {
query: ".first".to_string(),
description: "First".to_string(),
suggestion_type: SuggestionType::Next,
},
Suggestion {
query: ".second".to_string(),
description: "Second".to_string(),
suggestion_type: SuggestionType::Fix,
},
Suggestion {
query: ".third".to_string(),
description: "Third".to_string(),
suggestion_type: SuggestionType::Optimize,
},
];
app.handle_key_event(key_with_mods(KeyCode::Down, KeyModifiers::ALT)); app.handle_key_event(key_with_mods(KeyCode::Down, KeyModifiers::ALT)); app.handle_key_event(key_with_mods(KeyCode::Down, KeyModifiers::ALT)); app.handle_key_event(key_with_mods(KeyCode::Down, KeyModifiers::ALT));
assert_eq!(app.ai.selection.get_selected(), Some(2));
app.handle_key_event(key(KeyCode::Enter));
assert_eq!(app.query(), ".third");
}
#[test]
fn test_ai_suggestion_enter_without_navigation_exits() {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".");
app.focus = Focus::InputField;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![Suggestion {
query: ".name".to_string(),
description: "Get name".to_string(),
suggestion_type: SuggestionType::Next,
}];
assert!(!app.ai.selection.is_navigation_active());
app.handle_key_event(key(KeyCode::Enter));
assert!(app.should_quit);
assert_eq!(app.output_mode, Some(OutputMode::Results));
assert_eq!(app.query(), ".");
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_mode_independence_for_ai_suggestion_selection(
mode_idx in 0usize..3,
selection_digit in 1usize..=5,
suggestion_query in prop_oneof![
Just(".name"),
Just(".value"),
Just(".items[]"),
Just(".users | length"),
],
) {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".existing");
app.focus = Focus::InputField;
let mode = match mode_idx {
0 => EditorMode::Normal,
1 => EditorMode::Insert,
_ => EditorMode::Operator('d'),
};
app.input.editor_mode = mode;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = (0..5).map(|i| Suggestion {
query: if i == selection_digit - 1 {
suggestion_query.to_string()
} else {
format!(".field{}", i)
},
description: format!("Description {}", i),
suggestion_type: SuggestionType::Next,
}).collect();
prop_assume!(selection_digit <= app.ai.suggestions.len());
let key = key_with_mods(
KeyCode::Char(char::from_digit(selection_digit as u32, 10).unwrap()),
KeyModifiers::ALT,
);
app.handle_key_event(key);
prop_assert_eq!(
app.query(),
suggestion_query,
"Query should be replaced with selected suggestion"
);
prop_assert_eq!(
app.input.editor_mode,
mode,
"Editor mode should be preserved after suggestion selection"
);
prop_assert!(
!app.should_quit,
"Should not quit after selecting AI suggestion"
);
}
#[test]
fn prop_mode_independence_for_ai_navigation_selection(
mode_idx in 0usize..3,
nav_steps in 1usize..5,
suggestion_query in prop_oneof![
Just(".name"),
Just(".value"),
Just(".items[]"),
],
) {
use crate::ai::suggestion::{Suggestion, SuggestionType};
let mut app = app_with_query(".existing");
app.focus = Focus::InputField;
let mode = match mode_idx {
0 => EditorMode::Normal,
1 => EditorMode::Insert,
_ => EditorMode::Operator('d'),
};
app.input.editor_mode = mode;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.suggestions = vec![
Suggestion {
query: suggestion_query.to_string(),
description: "First suggestion".to_string(),
suggestion_type: SuggestionType::Next,
},
Suggestion {
query: ".other".to_string(),
description: "Second suggestion".to_string(),
suggestion_type: SuggestionType::Fix,
},
];
for _ in 0..nav_steps {
app.handle_key_event(key_with_mods(KeyCode::Down, KeyModifiers::ALT));
}
app.handle_key_event(key(KeyCode::Enter));
let query = app.query();
prop_assert!(
query == suggestion_query || query == ".other",
"Query '{}' should be one of the suggestions",
query
);
prop_assert_eq!(
app.input.editor_mode,
mode,
"Editor mode should be preserved after navigation selection"
);
prop_assert!(
!app.should_quit,
"Should not quit after selecting AI suggestion via navigation"
);
}
}
#[test]
fn test_ctrl_a_triggers_ai_request_when_becoming_visible() {
let mut app = app_with_query(".");
app.focus = Focus::InputField;
app.ai.visible = false;
app.ai.enabled = false;
app.ai.configured = true;
let (tx, rx) = std::sync::mpsc::channel();
let (_response_tx, response_rx) = std::sync::mpsc::channel();
app.ai.request_tx = Some(tx);
app.ai.response_rx = Some(response_rx);
app.ai.set_last_query_hash(".initial");
app.handle_key_event(key_with_mods(KeyCode::Char('a'), KeyModifiers::CONTROL));
assert!(app.ai.visible, "AI popup should be visible after Ctrl+A");
let mut found_request = false;
while let Ok(msg) = rx.try_recv() {
if matches!(msg, crate::ai::ai_state::AiRequest::Query { .. }) {
found_request = true;
break;
}
}
assert!(
found_request,
"Should have sent AI request when popup became visible"
);
}
#[test]
fn test_ctrl_a_no_request_when_not_configured() {
let mut app = app_with_query(".");
app.focus = Focus::InputField;
app.ai.visible = false;
app.ai.enabled = false;
app.ai.configured = false;
app.ai.request_tx = None;
app.ai.response_rx = None;
app.handle_key_event(key_with_mods(KeyCode::Char('a'), KeyModifiers::CONTROL));
assert!(app.ai.visible, "AI popup should be visible after Ctrl+A");
}
#[test]
fn test_ctrl_a_toggles_off_no_request() {
let mut app = app_with_query(".");
app.focus = Focus::InputField;
app.ai.visible = true;
app.ai.enabled = true;
app.ai.configured = true;
let (tx, rx) = std::sync::mpsc::channel();
let (_response_tx, response_rx) = std::sync::mpsc::channel();
app.ai.request_tx = Some(tx);
app.ai.response_rx = Some(response_rx);
while rx.try_recv().is_ok() {}
app.handle_key_event(key_with_mods(KeyCode::Char('a'), KeyModifiers::CONTROL));
assert!(!app.ai.visible, "AI popup should be hidden after Ctrl+A");
let mut found_request = false;
while let Ok(msg) = rx.try_recv() {
if matches!(msg, crate::ai::ai_state::AiRequest::Query { .. }) {
found_request = true;
break;
}
}
assert!(
!found_request,
"Should NOT send AI request when hiding popup"
);
}