use super::*;
use crate::ai::ai_state::lifecycle::TEST_MAX_CONTEXT_LENGTH;
use crate::ai::render::text::wrap_text;
use crate::theme;
use proptest::prelude::*;
#[test]
fn test_wrap_text_short() {
let result = wrap_text("hello world", 50);
assert_eq!(result, vec!["hello world"]);
}
#[test]
fn test_wrap_text_long() {
let result = wrap_text("hello world this is a long line", 15);
assert_eq!(result, vec!["hello world", "this is a long", "line"]);
}
#[test]
fn test_wrap_text_empty() {
let result = wrap_text("", 50);
assert_eq!(result, vec![""]);
}
#[test]
fn test_wrap_text_multiline() {
let result = wrap_text("line one\nline two", 50);
assert_eq!(result, vec!["line one", "line two"]);
}
#[test]
fn test_build_content_empty_state() {
let state = AiState::new_with_config(
true,
true,
"Anthropic".to_string(),
"claude-3-5-sonnet-20241022".to_string(),
TEST_MAX_CONTEXT_LENGTH,
);
let content = build_content(&state, 60);
assert!(content.lines.is_empty());
}
#[test]
fn test_build_content_not_configured() {
let state = AiState::new_with_config(
true,
false,
"Anthropic".to_string(),
"claude-3-5-sonnet-20241022".to_string(),
TEST_MAX_CONTEXT_LENGTH,
);
let content = build_content(&state, 60);
let text: String = content
.lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(text.contains("AI provider not configured"));
assert!(text.contains("[ai]"));
assert!(text.contains("provider"));
assert!(text.contains("api_key"));
assert!(text.contains("https://github.com/bellicose100xp/jiq#configuration"));
}
#[test]
fn test_build_content_loading() {
let mut state = AiState::new_with_config(
true,
true,
"Anthropic".to_string(),
"claude-3-5-sonnet-20241022".to_string(),
TEST_MAX_CONTEXT_LENGTH,
);
state.loading = true;
let content = build_content(&state, 60);
let text: String = content
.lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(text.contains("Thinking"));
}
#[test]
fn test_build_content_error() {
let mut state = AiState::new_with_config(
true,
true,
"Anthropic".to_string(),
"claude-3-5-sonnet-20241022".to_string(),
TEST_MAX_CONTEXT_LENGTH,
);
state.error = Some("Network error".to_string());
let content = build_content(&state, 60);
let text: String = content
.lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(text.contains("Error"));
assert!(text.contains("Network error"));
}
#[test]
fn test_build_content_response() {
let mut state = AiState::new_with_config(
true,
true,
"Anthropic".to_string(),
"claude-3-5-sonnet-20241022".to_string(),
TEST_MAX_CONTEXT_LENGTH,
);
state.response = "Try using .foo instead".to_string();
let content = build_content(&state, 60);
let text: String = content
.lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(text.contains("Try using .foo instead"));
}
#[test]
fn test_build_content_loading_with_previous() {
let mut state = AiState::new_with_config(
true,
true,
"Anthropic".to_string(),
"claude-3-5-sonnet-20241022".to_string(),
TEST_MAX_CONTEXT_LENGTH,
);
state.loading = true;
state.previous_response = Some("Previous answer".to_string());
let content = build_content(&state, 60);
let text: String = content
.lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
assert!(text.contains("Previous answer"));
assert!(text.contains("Thinking"));
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_unconfigured_state_shows_setup_message(
enabled in proptest::bool::ANY,
provider_name in "[a-zA-Z]{3,15}",
model_name in "[a-zA-Z0-9-]{5,30}",
max_width in 40u16..200u16
) {
let state = AiState::new_with_config(
enabled,
false, provider_name,
model_name,
TEST_MAX_CONTEXT_LENGTH,
);
let content = build_content(&state, max_width);
let text: String = content
.lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
prop_assert!(
text.contains("AI provider not configured"),
"Content should contain 'AI provider not configured' message"
);
prop_assert!(
text.contains("provider"),
"Content should contain 'provider' configuration guidance"
);
prop_assert!(
text.contains("anthropic") || text.contains("openai") || text.contains("gemini") || text.contains("bedrock"),
"Content should show provider selection options"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_unconfigured_state_includes_readme_url(
enabled in proptest::bool::ANY,
provider_name in "[a-zA-Z]{3,15}",
model_name in "[a-zA-Z0-9-]{5,30}",
max_width in 40u16..200u16
) {
let state = AiState::new_with_config(
enabled,
false, provider_name,
model_name,
TEST_MAX_CONTEXT_LENGTH,
);
let content = build_content(&state, max_width);
let text: String = content
.lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
prop_assert!(
text.contains("https://github.com/bellicose100xp/jiq#configuration"),
"Content should contain the README configuration URL"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_selection_number_rendering(suggestion_count in 1usize..=5) {
use crate::ai::ai_state::{Suggestion, SuggestionType};
let mut state = AiState::new_with_config(true, true, "Anthropic".to_string(), "claude-3-5-sonnet-20241022".to_string(), TEST_MAX_CONTEXT_LENGTH);
state.visible = true;
state.response = "AI response".to_string();
state.suggestions = (0..suggestion_count)
.map(|i| Suggestion {
query: format!(".query{}", i),
description: format!("Description {}", i),
suggestion_type: SuggestionType::Fix,
})
.collect();
let content = build_content(&state, 80);
let text: String = content
.lines
.iter()
.flat_map(|l| l.spans.iter())
.map(|s| s.content.as_ref())
.collect();
for i in 1..=suggestion_count {
prop_assert!(
text.contains(&format!("{}.", i)),
"Suggestion {} should have selection number '{}.'",
i, i
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_selection_number_limit(suggestion_count in 6usize..15) {
use crate::ai::ai_state::{Suggestion, SuggestionType};
let mut state = AiState::new_with_config(true, true, "Anthropic".to_string(), "claude-3-5-sonnet-20241022".to_string(), TEST_MAX_CONTEXT_LENGTH);
state.visible = true;
state.response = "AI response".to_string();
state.suggestions = (0..suggestion_count)
.map(|i| Suggestion {
query: format!(".query{}", i),
description: format!("Description {}", i),
suggestion_type: SuggestionType::Fix,
})
.collect();
let content = build_content(&state, 80);
let mut numbered_suggestions = 0;
for line in &content.lines {
let line_text: String = line.spans.iter()
.map(|s| s.content.as_ref())
.collect();
for i in 1..=5 {
if line_text.trim_start().starts_with(&format!("{}. ", i)) {
numbered_suggestions += 1;
break;
}
}
}
prop_assert_eq!(
numbered_suggestions, 5,
"Should have exactly 5 numbered suggestions, found {}",
numbered_suggestions
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_selection_highlight_visibility(
suggestion_count in 1usize..10,
selected_index in 0usize..10
) {
use crate::ai::ai_state::{Suggestion, SuggestionType};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::layout::Rect;
prop_assume!(selected_index < suggestion_count);
let mut state = AiState::new_with_config(true, true, "Anthropic".to_string(), "claude-3-5-sonnet-20241022".to_string(), TEST_MAX_CONTEXT_LENGTH);
state.visible = true;
state.response = "AI response".to_string();
state.suggestions = (0..suggestion_count)
.map(|i| Suggestion {
query: format!(".query{}", i),
description: format!("Description {}", i),
suggestion_type: SuggestionType::Fix,
})
.collect();
for _ in 0..=selected_index {
state.selection.navigate_next(suggestion_count);
}
let terminal_height = 30 + (suggestion_count as u16 * 3);
let backend = TestBackend::new(100, terminal_height);
let mut terminal = Terminal::new(backend).unwrap();
let mut state_mut = state; terminal.draw(|f| {
let input_area = Rect {
x: 0,
y: terminal_height - 4,
width: 100,
height: 3,
};
render_popup(&mut state_mut, f, input_area);
}).unwrap();
let buffer = terminal.backend().buffer();
let expected_bg = theme::ai::SUGGESTION_SELECTED_BG;
let has_background = buffer.content.iter().any(|cell| {
cell.bg == expected_bg
});
prop_assert!(
has_background,
"Selected suggestion should have cells with selected background color"
);
}
}