use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
use std::fs;
use std::path::PathBuf;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(name)
}
fn create_test_loader(json: String) -> jiq::input::FileLoader {
use jiq::input::loader::LoadingState;
use std::sync::mpsc::channel;
let (tx, rx) = channel();
let _ = tx.send(Ok(json));
jiq::input::FileLoader {
state: LoadingState::Loading,
rx: Some(rx),
}
}
#[test]
fn test_cli_help_flag() {
cargo_bin_cmd!()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Interactive JSON query tool"));
}
#[test]
fn test_cli_version_flag() {
cargo_bin_cmd!()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains("jiq"));
}
#[test]
fn test_fixture_files_exist() {
assert!(fixture_path("simple.json").exists());
assert!(fixture_path("array.json").exists());
assert!(fixture_path("nested.json").exists());
assert!(fixture_path("invalid.json").exists());
}
#[test]
fn test_fixture_simple_json_content() {
let content = fs::read_to_string(fixture_path("simple.json")).unwrap();
assert!(content.contains("Alice"));
assert!(content.contains("Seattle"));
}
#[test]
fn test_fixture_array_json_content() {
let content = fs::read_to_string(fixture_path("array.json")).unwrap();
assert!(content.contains("Alice"));
assert!(content.contains("Bob"));
assert!(content.contains("Charlie"));
}
#[test]
fn test_fixture_nested_json_content() {
let content = fs::read_to_string(fixture_path("nested.json")).unwrap();
assert!(content.contains("TechCorp"));
assert!(content.contains("engineering"));
assert!(content.contains("departments"));
}
#[test]
fn test_ai_full_flow_error_to_response() {
let (request_tx, request_rx) = mpsc::channel::<String>();
let (response_tx, response_rx) = mpsc::channel::<String>();
let worker_handle = thread::spawn(move || {
if let Ok(prompt) = request_rx.recv_timeout(Duration::from_secs(5)) {
assert!(
prompt.contains("error"),
"Prompt should contain error context"
);
let chunks = vec![
"The error ",
"you're seeing ",
"is because ",
"the query ",
"syntax is invalid.",
];
for chunk in chunks {
response_tx.send(chunk.to_string()).unwrap();
thread::sleep(Duration::from_millis(10));
}
}
});
let error_context = "Query error: unexpected token at position 5";
request_tx.send(error_context.to_string()).unwrap();
let mut accumulated_response = String::new();
let mut chunk_count = 0;
while let Ok(chunk) = response_rx.recv_timeout(Duration::from_millis(500)) {
accumulated_response.push_str(&chunk);
chunk_count += 1;
}
assert!(chunk_count > 1, "Response should arrive in multiple chunks");
assert_eq!(
accumulated_response,
"The error you're seeing is because the query syntax is invalid."
);
worker_handle.join().expect("Worker thread should complete");
}
#[test]
fn test_ai_state_transitions() {
#[derive(Debug, PartialEq)]
enum State {
Idle,
Loading,
Streaming,
Complete,
}
let mut state = State::Idle;
let mut response = String::new();
assert_eq!(state, State::Idle);
let previous_response = if !response.is_empty() {
Some(response.clone())
} else {
None
};
response.clear();
state = State::Loading;
assert_eq!(state, State::Loading);
assert!(response.is_empty());
assert!(previous_response.is_none());
let chunk1 = "Hello ";
response.push_str(chunk1);
state = State::Streaming;
assert_eq!(state, State::Streaming);
assert_eq!(response, "Hello ");
let chunk2 = "World!";
response.push_str(chunk2);
assert_eq!(response, "Hello World!");
state = State::Complete;
assert_eq!(state, State::Complete);
assert_eq!(response, "Hello World!");
}
#[test]
fn test_ai_previous_response_preservation() {
let mut response = "Previous AI response".to_string();
let mut previous_response: Option<String> = None;
if !response.is_empty() {
previous_response = Some(response.clone());
}
response.clear();
assert!(response.is_empty());
assert_eq!(previous_response, Some("Previous AI response".to_string()));
response.push_str("New response");
previous_response = None;
assert_eq!(response, "New response");
assert!(previous_response.is_none());
}
#[test]
fn test_ai_streaming_concatenation() {
let chunks = vec![
"First chunk. ",
"Second chunk. ",
"Third chunk. ",
"Final chunk.",
];
let mut accumulated = String::new();
for chunk in &chunks {
accumulated.push_str(chunk);
}
let expected = chunks.join("");
assert_eq!(accumulated, expected);
assert_eq!(
accumulated,
"First chunk. Second chunk. Third chunk. Final chunk."
);
}
#[test]
fn test_ai_full_flow_with_sse_simulation() {
use std::io::{BufRead, BufReader, Cursor};
let sse_data = r#"event: message_start
data: {"type":"message_start","message":{"id":"msg_test"}}
event: content_block_start
data: {"type":"content_block_start","index":0}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The error "}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"in your query "}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"is a syntax issue."}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_stop
data: {"type":"message_stop"}
"#;
fn parse_sse_chunks(data: &str) -> Vec<String> {
let reader = BufReader::new(Cursor::new(data.as_bytes()));
let mut chunks = Vec::new();
for line in reader.lines() {
let line = line.unwrap();
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" {
break;
}
if let Ok(json) = serde_json::from_str::<serde_json::Value>(data)
&& json.get("type").and_then(|t| t.as_str()) == Some("content_block_delta")
&& let Some(text) = json
.get("delta")
.and_then(|d| d.get("text"))
.and_then(|t| t.as_str())
&& !text.is_empty()
{
chunks.push(text.to_string());
}
}
}
chunks
}
let chunks = parse_sse_chunks(sse_data);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0], "The error ");
assert_eq!(chunks[1], "in your query ");
assert_eq!(chunks[2], "is a syntax issue.");
let (request_tx, request_rx) = mpsc::channel::<String>();
let (response_tx, response_rx) = mpsc::channel::<String>();
let worker_handle = thread::spawn(move || {
if let Ok(_prompt) = request_rx.recv_timeout(Duration::from_secs(5)) {
for chunk in chunks {
response_tx.send(chunk).unwrap();
thread::sleep(Duration::from_millis(5));
}
}
});
let error_context = "jq: error: syntax error, unexpected IDENT";
request_tx.send(error_context.to_string()).unwrap();
let mut accumulated = String::new();
let mut chunk_count = 0;
while let Ok(chunk) = response_rx.recv_timeout(Duration::from_millis(500)) {
accumulated.push_str(&chunk);
chunk_count += 1;
}
assert_eq!(chunk_count, 3, "Should receive 3 chunks");
assert_eq!(accumulated, "The error in your query is a syntax issue.");
worker_handle.join().expect("Worker should complete");
}
#[test]
fn test_ai_visibility_configurations() {
struct MockAiState {
visible: bool,
enabled: bool,
}
impl MockAiState {
fn should_send_request(&self) -> bool {
self.enabled && self.visible
}
}
let state = MockAiState {
visible: true,
enabled: true,
};
assert!(state.should_send_request());
let state = MockAiState {
visible: false,
enabled: true,
};
assert!(!state.should_send_request());
let state = MockAiState {
visible: true,
enabled: false,
};
assert!(!state.should_send_request());
let state = MockAiState {
visible: false,
enabled: false,
};
assert!(!state.should_send_request());
}
#[test]
fn test_initial_visibility_ai_enabled() {
use jiq::app::App;
use jiq::config::{AiConfig, AiProviderType, AnthropicConfig, Config};
let config = Config {
ai: AiConfig {
enabled: true,
provider: Some(AiProviderType::Anthropic),
anthropic: AnthropicConfig {
api_key: Some("test-key".to_string()),
model: Some("claude-3-5-sonnet-20241022".to_string()),
..Default::default()
},
..Default::default()
},
..Default::default()
};
let json_input = r#"{"test": "data"}"#.to_string();
let loader = create_test_loader(json_input);
let app = App::new_with_loader(loader, &config);
assert!(
app.ai.visible,
"AI popup should be visible when AI is enabled in config"
);
assert!(app.ai.enabled, "AI should be enabled");
assert!(app.ai.configured, "AI should be configured with API key");
}
#[test]
fn test_initial_visibility_ai_disabled() {
use jiq::app::App;
use jiq::config::Config;
let config = Config::default();
let json_input = r#"{"test": "data"}"#.to_string();
let loader = create_test_loader(json_input);
let app = App::new_with_loader(loader, &config);
assert!(
!app.ai.visible,
"AI popup should be hidden when AI is disabled in config"
);
assert!(!app.ai.enabled, "AI should be disabled");
}
#[test]
fn test_tooltip_hidden_when_ai_visible_on_startup() {
use jiq::app::App;
use jiq::config::{AiConfig, AiProviderType, AnthropicConfig, Config, TooltipConfig};
let config = Config {
ai: AiConfig {
enabled: true,
provider: Some(AiProviderType::Anthropic),
anthropic: AnthropicConfig {
api_key: Some("test-key".to_string()),
model: Some("claude-3-5-sonnet-20241022".to_string()),
..Default::default()
},
..Default::default()
},
tooltip: TooltipConfig { auto_show: true },
..Default::default()
};
let json_input = r#"{"test": "data"}"#.to_string();
let loader = create_test_loader(json_input);
let app = App::new_with_loader(loader, &config);
assert!(app.ai.visible, "AI popup should be visible");
assert!(
!app.tooltip.enabled,
"Tooltip should be hidden when AI popup is visible on startup"
);
}
#[test]
fn test_tooltip_visible_when_ai_disabled_on_startup() {
use jiq::app::App;
use jiq::config::{Config, TooltipConfig};
let config = Config {
tooltip: TooltipConfig { auto_show: true },
..Default::default()
};
let json_input = r#"{"test": "data"}"#.to_string();
let loader = create_test_loader(json_input);
let app = App::new_with_loader(loader, &config);
assert!(!app.ai.visible, "AI popup should be hidden");
assert!(
app.tooltip.enabled,
"Tooltip should be visible when AI popup is hidden on startup"
);
}
#[test]
fn test_handle_execution_result_does_not_change_visibility_on_error() {
use jiq::ai::ai_events::handle_execution_result;
use jiq::ai::ai_state::AiState;
let mut ai_state = AiState {
visible: false,
..Default::default()
};
let initial_visibility = ai_state.visible;
let error_result: Result<String, String> = Err("syntax error".to_string());
handle_execution_result(
&mut ai_state,
&error_result,
".invalid",
0,
jiq::ai::context::ContextParams {
input_schema: None,
base_query: None,
base_query_result: None,
is_empty_result: false,
},
);
assert_eq!(
ai_state.visible, initial_visibility,
"handle_execution_result should NOT change visibility on error"
);
let mut ai_state = AiState {
visible: true,
..Default::default()
};
let initial_visibility = ai_state.visible;
let error_result: Result<String, String> = Err("syntax error".to_string());
handle_execution_result(
&mut ai_state,
&error_result,
".invalid2",
0,
jiq::ai::context::ContextParams {
input_schema: None,
base_query: None,
base_query_result: None,
is_empty_result: false,
},
);
assert_eq!(
ai_state.visible, initial_visibility,
"handle_execution_result should NOT change visibility on error (even when visible)"
);
}
#[test]
fn test_handle_execution_result_does_not_change_visibility_on_success() {
use jiq::ai::ai_events::handle_execution_result;
use jiq::ai::ai_state::AiState;
let mut ai_state = AiState {
visible: false,
..Default::default()
};
let initial_visibility = ai_state.visible;
let success_result: Result<String, String> = Ok(r#"{"result": "value"}"#.to_string());
handle_execution_result(
&mut ai_state,
&success_result,
".test",
0,
jiq::ai::context::ContextParams {
input_schema: None,
base_query: None,
base_query_result: None,
is_empty_result: false,
},
);
assert_eq!(
ai_state.visible, initial_visibility,
"handle_execution_result should NOT change visibility on success"
);
let mut ai_state = AiState {
visible: true,
..Default::default()
};
let initial_visibility = ai_state.visible;
let success_result: Result<String, String> = Ok(r#"{"result": "value2"}"#.to_string());
handle_execution_result(
&mut ai_state,
&success_result,
".test2",
0,
jiq::ai::context::ContextParams {
input_schema: None,
base_query: None,
base_query_result: None,
is_empty_result: false,
},
);
assert_eq!(
ai_state.visible, initial_visibility,
"handle_execution_result should NOT change visibility on success (even when visible)"
);
}
#[test]
fn test_visibility_control_mechanisms_complete() {
use jiq::ai::ai_events::handle_execution_result;
use jiq::ai::ai_state::AiState;
use jiq::app::App;
use jiq::config::{AiConfig, AiProviderType, AnthropicConfig, Config};
let config_enabled = Config {
ai: AiConfig {
enabled: true,
provider: Some(AiProviderType::Anthropic),
anthropic: AnthropicConfig {
api_key: Some("test-key".to_string()),
model: Some("claude-3-5-sonnet-20241022".to_string()),
..Default::default()
},
..Default::default()
},
..Default::default()
};
let loader_enabled = create_test_loader(r#"{"test": "data"}"#.to_string());
let app_enabled = App::new_with_loader(loader_enabled, &config_enabled);
assert!(
app_enabled.ai.visible,
"Config with AI enabled should set initial visibility to true"
);
let config_disabled = Config::default();
let loader_disabled = create_test_loader(r#"{"test": "data"}"#.to_string());
let app_disabled = App::new_with_loader(loader_disabled, &config_disabled);
assert!(
!app_disabled.ai.visible,
"Config with AI disabled should set initial visibility to false"
);
let mut ai_state = AiState {
visible: false,
..Default::default()
};
ai_state.toggle(); assert!(ai_state.visible, "Toggle should change visibility");
ai_state.toggle(); assert!(!ai_state.visible, "Toggle should change visibility back");
let mut ai_state = AiState {
visible: false,
..Default::default()
};
let error_result: Result<String, String> = Err("error".to_string());
handle_execution_result(
&mut ai_state,
&error_result,
".err",
0,
jiq::ai::context::ContextParams {
input_schema: None,
base_query: None,
base_query_result: None,
is_empty_result: false,
},
);
assert!(
!ai_state.visible,
"Error result should NOT change visibility"
);
let success_result: Result<String, String> = Ok(r#"{"ok": true}"#.to_string());
handle_execution_result(
&mut ai_state,
&success_result,
".ok",
0,
jiq::ai::context::ContextParams {
input_schema: None,
base_query: None,
base_query_result: None,
is_empty_result: false,
},
);
assert!(
!ai_state.visible,
"Success result should NOT change visibility"
);
}