use super::*;
use crate::integrations::{
health::{EmbedderState, HealthResponse},
search_client::{IndexInfo, SearchClient, SearchClientError, SearchResult},
};
use async_trait::async_trait;
use serial_test::serial;
struct FixedIndexSearch(Vec<IndexInfo>);
#[async_trait]
impl SearchClient for FixedIndexSearch {
async fn health(&self) -> Result<HealthResponse, SearchClientError> {
Ok(HealthResponse {
status: "ok".to_string(),
embedder: EmbedderState::Bool(true),
})
}
async fn list_indexes(&self) -> Result<Vec<IndexInfo>, SearchClientError> {
Ok(self.0.clone())
}
async fn search(
&self,
_index_id: &str,
_query: &str,
_top_k: Option<u32>,
) -> Result<Vec<SearchResult>, SearchClientError> {
Ok(vec![])
}
}
struct FailListSearch;
#[async_trait]
impl SearchClient for FailListSearch {
async fn health(&self) -> Result<HealthResponse, SearchClientError> {
Err(SearchClientError::Unavailable("down".to_string()))
}
async fn list_indexes(&self) -> Result<Vec<IndexInfo>, SearchClientError> {
Err(SearchClientError::Unavailable("daemon down".to_string()))
}
async fn search(
&self,
_index_id: &str,
_query: &str,
_top_k: Option<u32>,
) -> Result<Vec<SearchResult>, SearchClientError> {
Err(SearchClientError::Unavailable("down".to_string()))
}
}
fn make_index_info(id: &str, root_path: Option<&str>) -> IndexInfo {
IndexInfo {
id: id.to_string(),
name: None,
root_path: root_path.map(|s| s.to_string()),
}
}
#[tokio::test]
async fn resolve_index_noop_when_explicit() {
let mut config = ReviewConfig::load(None);
config.search_index = "explicit-index".to_string();
config.search_index_explicit = true;
let indexes = vec![make_index_info("auto-index", Some("/tmp/some-project"))];
let client = FixedIndexSearch(indexes);
config.resolve_index(&client).await;
assert_eq!(
config.search_index, "explicit-index",
"explicit index must not be overwritten by auto-derive"
);
}
#[tokio::test]
#[serial]
async fn resolve_index_updates_when_match_found() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir(root.path().join(".git")).unwrap();
let canonical = root.path().canonicalize().unwrap();
let root_path_str = canonical.to_str().unwrap().to_string();
let mut config = ReviewConfig::load(None);
config.search_index = "main".to_string();
config.search_index_explicit = false;
let indexes = vec![make_index_info("my-project", Some(&root_path_str))];
let client = FixedIndexSearch(indexes);
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(root.path()).unwrap();
config.resolve_index(&client).await;
std::env::set_current_dir(original_dir).unwrap();
assert_eq!(
config.search_index, "my-project",
"auto-derive must update search_index to the matched index"
);
}
#[tokio::test]
async fn resolve_index_falls_back_on_daemon_error() {
let mut config = ReviewConfig::load(None);
config.search_index = "main".to_string();
config.search_index_explicit = false;
let client = FailListSearch;
config.resolve_index(&client).await;
assert_eq!(
config.search_index, "main",
"daemon error must leave search_index at fallback 'main'"
);
}
#[tokio::test]
async fn resolve_index_keeps_default_when_no_match() {
let mut config = ReviewConfig::load(None);
config.search_index = "main".to_string();
config.search_index_explicit = false;
let indexes = vec![make_index_info(
"other-project",
Some("/srv/totally-different"),
)];
let client = FixedIndexSearch(indexes);
config.resolve_index(&client).await;
assert_eq!(
config.search_index, "main",
"no-match must leave search_index at fallback 'main'"
);
}
#[tokio::test]
#[serial]
async fn wiring_cmd_run_resolve_index_updates_before_pipeline() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir(root.path().join(".git")).unwrap();
let canonical = root.path().canonicalize().unwrap();
let root_path_str = canonical.to_str().unwrap().to_string();
let mut config = ReviewConfig::load(None);
config.search_index = "main".to_string();
config.search_index_explicit = false;
let indexes = vec![make_index_info("run-project", Some(&root_path_str))];
let client = FixedIndexSearch(indexes);
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(root.path()).unwrap();
config.resolve_index(&client).await;
std::env::set_current_dir(original_dir).unwrap();
assert_eq!(
config.search_index, "run-project",
"cmd_run wiring: resolve_index must update search_index before pipeline"
);
}
#[tokio::test]
async fn wiring_build_app_state_explicit_index_unchanged() {
let mut config = ReviewConfig::load(None);
config.search_index = "operator-chosen".to_string();
config.search_index_explicit = true;
let indexes = vec![make_index_info("auto-derived", Some("/srv/some-project"))];
let client = FixedIndexSearch(indexes);
config.resolve_index(&client).await;
assert_eq!(
config.search_index, "operator-chosen",
"serve wiring: explicit TRUSTY_SEARCH_INDEX must survive resolve_index"
);
}
#[tokio::test]
#[serial]
async fn wiring_cmd_compare_resolve_index_applies_to_all_model_runs() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir(root.path().join(".git")).unwrap();
let canonical = root.path().canonicalize().unwrap();
let root_path_str = canonical.to_str().unwrap().to_string();
let mut config = ReviewConfig::load(None);
config.search_index = "main".to_string();
config.search_index_explicit = false;
let indexes = vec![make_index_info("compare-project", Some(&root_path_str))];
let client = FixedIndexSearch(indexes);
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(root.path()).unwrap();
config.resolve_index(&client).await;
std::env::set_current_dir(original_dir).unwrap();
for _ in 0..3 {
assert_eq!(
config.search_index, "compare-project",
"compare wiring: all model runs must see the resolved index"
);
}
}