use super::{
HttpSearchClient, ListIndexesResponse, SearchClient, SearchClientError, SearchRequest,
SearchResponse, SearchResult,
};
use crate::integrations::search_client::IndexInfo;
#[test]
fn search_client_trait_object_compiles() {
fn _accepts_dyn(_c: &dyn super::SearchClient) {}
}
#[test]
fn http_search_client_url_is_configurable() {
let client = HttpSearchClient::new("http://127.0.0.1:7878").expect("TLS init should succeed");
assert_eq!(client.base_url(), "http://127.0.0.1:7878");
}
#[test]
fn http_search_client_strips_trailing_slash() {
let client = HttpSearchClient::new("http://127.0.0.1:7878/").expect("TLS init should succeed");
assert_eq!(client.base_url(), "http://127.0.0.1:7878");
}
#[test]
fn http_search_client_from_config() {
let mut config = crate::config::ReviewConfig::load(None);
config.search_url = "http://localhost:9999".to_string();
let client = HttpSearchClient::from_config(&config).expect("TLS init should succeed");
assert_eq!(client.base_url(), "http://localhost:9999");
}
#[test]
fn index_info_deserialises() {
let json = r#"{"id":"main","name":"trusty-tools","root_path":"/home/user/trusty-tools"}"#;
let info: IndexInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.id, "main");
assert_eq!(info.name.as_deref(), Some("trusty-tools"));
}
#[test]
fn search_result_deserialises() {
let json = r#"{
"file": "src/lib.rs",
"snippet": "pub fn authenticate() {",
"score": 0.92,
"start_line": 42,
"end_line": 58
}"#;
let result: SearchResult = serde_json::from_str(json).unwrap();
assert_eq!(result.file, "src/lib.rs");
assert_eq!(result.snippet.as_deref(), Some("pub fn authenticate() {"));
assert!((result.score - 0.92_f32).abs() < 1e-5);
assert_eq!(result.start_line, Some(42));
assert_eq!(result.end_line, Some(58));
}
#[test]
fn search_result_missing_optional_fields() {
let json = r#"{"file":"src/main.rs"}"#;
let result: SearchResult = serde_json::from_str(json).unwrap();
assert_eq!(result.file, "src/main.rs");
assert!(result.snippet.is_none());
assert!((result.score - 0.0_f32).abs() < 1e-10);
}
#[test]
fn search_request_body_uses_text_field() {
let req = SearchRequest {
text: "fn authenticate".to_string(),
top_k: Some(10),
};
let json = serde_json::to_string(&req).unwrap();
assert!(
json.contains("\"text\""),
"SearchRequest must use 'text' field name, got: {json}"
);
assert!(
!json.contains("\"query\""),
"SearchRequest must NOT use 'query' field name, got: {json}"
);
assert!(json.contains("fn authenticate"));
assert!(json.contains("10"));
}
#[test]
fn search_request_omits_none_top_k() {
let req = SearchRequest {
text: "async fn".to_string(),
top_k: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("top_k"));
}
#[test]
fn search_response_deserialises() {
let json = r#"{"results":[{"file":"a.rs","score":0.5},{"file":"b.rs","score":0.3}]}"#;
let resp: SearchResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.results.len(), 2);
assert_eq!(resp.results[0].file, "a.rs");
}
#[test]
fn list_indexes_parses_daemon_envelope() {
let body = r#"{"indexes":[{"id":"trusty-tools","root_path":"/Volumes/SSD1/Projects/trusty-tools","size_bytes":123}]}"#;
let envelope: ListIndexesResponse =
serde_json::from_str(body).expect("must parse the daemon envelope without error");
assert_eq!(
envelope.indexes.len(),
1,
"must yield exactly one IndexInfo"
);
let info = &envelope.indexes[0];
assert_eq!(info.id, "trusty-tools");
assert_eq!(
info.root_path.as_deref(),
Some("/Volumes/SSD1/Projects/trusty-tools"),
"root_path must survive the envelope unwrap"
);
}
#[test]
fn list_indexes_bare_array_is_rejected() {
let body = r#"[{"id":"trusty-tools","root_path":"/foo","size_bytes":0}]"#;
let result: Result<ListIndexesResponse, _> = serde_json::from_str(body);
assert!(
result.is_err(),
"bare array must not parse as ListIndexesResponse — envelope is required"
);
}
#[test]
fn search_error_display() {
let err = SearchClientError::Transport("connection refused".to_string());
assert!(err.to_string().contains("connection refused"));
let err = SearchClientError::Api {
status: 503,
body: "overloaded".to_string(),
};
let s = err.to_string();
assert!(s.contains("503"));
assert!(s.contains("overloaded"));
}
#[tokio::test]
async fn health_check_transport_error_on_unreachable() {
let client = HttpSearchClient::new("http://127.0.0.1:1").expect("TLS init should succeed");
let result = client.health().await;
assert!(
result.is_err(),
"unreachable host must return an error, not panic"
);
match result.unwrap_err() {
SearchClientError::Unavailable(_) => {}
SearchClientError::Transport(_) => {}
other => panic!("expected Unavailable or Transport, got {other:?}"),
}
}
#[tokio::test]
async fn search_transport_error_on_unreachable() {
let client = HttpSearchClient::new("http://127.0.0.1:1").expect("TLS init should succeed");
let result = client.search("main", "fn auth", Some(5)).await;
assert!(
result.is_err(),
"unreachable host must return an error, not panic"
);
}