use super::indexes::ListIndexesParams;
use super::*;
use axum::extract::{Query, State};
use axum::Json;
#[tokio::test]
async fn list_indexes_flat_default_unchanged() {
use crate::core::{
indexer::CodeIndexer,
registry::{IndexHandle, IndexId, IndexRegistry},
};
let registry = IndexRegistry::new();
for name in ["alpha", "beta"] {
let id = IndexId::new(name);
let indexer = CodeIndexer::new(name, format!("/tmp/{name}"));
registry.register(IndexHandle::bare(
id.clone(),
std::sync::Arc::new(tokio::sync::RwLock::new(indexer)),
format!("/tmp/{name}").into(),
));
}
let state = std::sync::Arc::new(SearchAppState::new(registry));
let resp = list_indexes_handler(
State(state),
Query(ListIndexesParams {
format: None,
details: false,
}),
)
.await;
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let arr = value["indexes"].as_array().expect("indexes array");
for item in arr {
assert!(
item.is_string(),
"flat default must return string IDs: {item:?}"
);
}
assert_eq!(arr.len(), 2);
}
#[tokio::test]
async fn list_indexes_tree_format_shape() {
use crate::core::{
indexer::CodeIndexer,
registry::{IndexHandle, IndexId, IndexRegistry},
};
use std::sync::Arc;
use tokio::sync::RwLock;
let registry = IndexRegistry::new();
let parent_id = IndexId::new("tree-parent");
let child_id = IndexId::new("tree-child");
let parent_root: std::path::PathBuf = "/nonexistent_test_root_abc".into();
let child_root: std::path::PathBuf = "/nonexistent_test_root_abc/services/billing".into();
registry.register(IndexHandle::bare(
parent_id.clone(),
Arc::new(RwLock::new(CodeIndexer::new(
"tree-parent",
"/nonexistent_test_root_abc",
))),
parent_root,
));
registry.register(IndexHandle::bare(
child_id.clone(),
Arc::new(RwLock::new(CodeIndexer::new(
"tree-child",
"/nonexistent_test_root_abc/services/billing",
))),
child_root,
));
let state = Arc::new(SearchAppState::new(registry));
let resp = list_indexes_handler(
State(state),
Query(ListIndexesParams {
format: Some("tree".to_string()),
details: false,
}),
)
.await;
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let arr = value["indexes"].as_array().expect("indexes array");
assert_eq!(arr.len(), 2);
for entry in arr {
assert!(entry["id"].is_string(), "id must be string");
assert!(entry["root_path"].is_string(), "root_path must be present");
assert!(
entry["priority_boost"].is_number(),
"priority_boost must be a number"
);
assert!(
entry["is_sub_index"].is_boolean(),
"is_sub_index must be bool"
);
assert!(entry["children"].is_array(), "children must be an array");
}
let child_entry = arr
.iter()
.find(|e| e["id"].as_str() == Some("tree-child"))
.expect("tree-child entry");
assert_eq!(
child_entry["is_sub_index"].as_bool(),
Some(true),
"tree-child must be a sub-index"
);
let parent_entry = arr
.iter()
.find(|e| e["id"].as_str() == Some("tree-parent"))
.expect("tree-parent entry");
assert_eq!(
parent_entry["is_sub_index"].as_bool(),
Some(false),
"tree-parent must not be a sub-index"
);
}
#[tokio::test]
async fn list_indexes_details_includes_size_bytes() {
use crate::core::{
indexer::CodeIndexer,
registry::{IndexHandle, IndexId, IndexRegistry},
};
let registry = IndexRegistry::new();
for name in ["detail-alpha", "detail-beta"] {
let id = IndexId::new(name);
let indexer = CodeIndexer::new(name, format!("/tmp/{name}"));
registry.register(IndexHandle::bare(
id.clone(),
std::sync::Arc::new(tokio::sync::RwLock::new(indexer)),
format!("/tmp/{name}").into(),
));
}
let state = std::sync::Arc::new(SearchAppState::new(registry));
let resp = list_indexes_handler(
State(state),
Query(ListIndexesParams {
format: None,
details: true,
}),
)
.await;
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let arr = value["indexes"].as_array().expect("indexes array");
assert_eq!(arr.len(), 2);
for entry in arr {
assert!(
entry["id"].is_string(),
"each detail entry must have a string id: {entry:?}"
);
assert!(
entry.get("size_bytes").is_some(),
"each detail entry must have a size_bytes field: {entry:?}"
);
assert!(
entry.get("root_path").is_some(),
"each detail entry must have a root_path field (issue #661): {entry:?}"
);
}
}
#[tokio::test]
async fn list_indexes_details_includes_root_path() {
use crate::core::{
indexer::CodeIndexer,
registry::{IndexHandle, IndexId, IndexRegistry},
};
let registry = IndexRegistry::new();
let id = IndexId::new("rp-test");
let indexer = CodeIndexer::new("rp-test", "/tmp/rp-test");
registry.register(IndexHandle::bare(
id.clone(),
std::sync::Arc::new(tokio::sync::RwLock::new(indexer)),
std::path::PathBuf::from("/tmp/rp-test"),
));
let state = std::sync::Arc::new(SearchAppState::new(registry));
let resp = list_indexes_handler(
State(state),
Query(ListIndexesParams {
format: None,
details: true,
}),
)
.await;
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let arr = value["indexes"].as_array().expect("indexes array");
assert_eq!(arr.len(), 1, "expected exactly one index entry");
let entry = &arr[0];
assert_eq!(
entry["id"].as_str(),
Some("rp-test"),
"id must match registered index id"
);
let rp = entry["root_path"]
.as_str()
.expect("root_path must be a non-null string");
assert_eq!(
rp, "/tmp/rp-test",
"root_path must match what was registered"
);
}
#[tokio::test]
async fn global_search_nested_hierarchy_dedup_count_present() {
use crate::core::{
indexer::CodeIndexer,
registry::{IndexHandle, IndexId, IndexRegistry},
};
use std::sync::Arc;
use tokio::sync::RwLock;
let registry = IndexRegistry::new();
for name in ["flat-a", "flat-b"] {
let id = IndexId::new(name);
let indexer = CodeIndexer::new(name, format!("/tmp/{name}"));
indexer
.index_file(
&format!("{name}/lib.rs"),
"fn beta_function() { println!(\"beta\"); }",
)
.await
.expect("index_file");
registry.register(IndexHandle::bare(
id.clone(),
Arc::new(RwLock::new(indexer)),
format!("/tmp/{name}").into(),
));
}
let state = Arc::new(SearchAppState::new(registry));
let Json(value) = global_search_handler(
State(state),
Json(GlobalSearchRequest {
query: "beta_function".into(),
top_k: 10,
full_content: false,
indexes: None,
routing: None,
routing_n: None,
routing_threshold: None,
}),
)
.await
.expect("handler ok");
assert!(
value["hierarchy_dedup_count"].is_number(),
"hierarchy_dedup_count must be present: {value:?}"
);
assert_eq!(
value["hierarchy_dedup_count"].as_u64(),
Some(0),
"flat peers must not trigger dedup"
);
}
#[tokio::test]
async fn global_search_sub_index_boost_applied() {
use crate::core::{
indexer::CodeIndexer,
registry::{IndexHandle, IndexId, IndexRegistry},
};
use std::sync::Arc;
use tokio::sync::RwLock;
let registry = IndexRegistry::new();
let parent_root: std::path::PathBuf = "/nonexistent_boost_root".into();
let child_root: std::path::PathBuf = "/nonexistent_boost_root/sub".into();
let parent_id = IndexId::new("boost-parent");
let child_id = IndexId::new("boost-child");
let parent_indexer = CodeIndexer::new("boost-parent", "/nonexistent_boost_root");
parent_indexer
.index_file("src/lib.rs", "fn gamma_function() { println!(\"gamma\"); }")
.await
.expect("parent index_file");
registry.register(IndexHandle::bare(
parent_id.clone(),
Arc::new(RwLock::new(parent_indexer)),
parent_root,
));
let child_indexer = CodeIndexer::new("boost-child", "/nonexistent_boost_root/sub");
child_indexer
.index_file(
"sub/lib.rs",
"fn gamma_function() { println!(\"gamma sub\"); }",
)
.await
.expect("child index_file");
registry.register(IndexHandle::bare(
child_id.clone(),
Arc::new(RwLock::new(child_indexer)),
child_root,
));
let state = Arc::new(SearchAppState::new(registry));
let Json(value) = global_search_handler(
State(state),
Json(GlobalSearchRequest {
query: "gamma_function".into(),
top_k: 10,
full_content: false,
indexes: None,
routing: None,
routing_n: None,
routing_threshold: None,
}),
)
.await
.expect("handler ok");
assert!(
value["hierarchy_dedup_count"].is_number(),
"hierarchy_dedup_count must be present: {value:?}",
);
let searched = value["indexes_searched"].as_array().unwrap();
assert_eq!(
searched.len(),
2,
"both parent and child should be searched"
);
let results = value["results"].as_array().unwrap();
assert!(!results.is_empty(), "expected at least one result");
let has_child_result = results
.iter()
.any(|r| r["index_id"].as_str() == Some("boost-child"));
assert!(
has_child_result,
"sub-index (boost-child) must contribute results to the fan-out"
);
}