use super::index_config::{
index_config_handler, patch_index_config_handler, IndexConfigView, PatchIndexConfigRequest,
};
use super::state::SearchAppState;
use crate::core::indexer::CodeIndexer;
use crate::core::registry::{IndexHandle, IndexId, IndexRegistry};
use axum::body::to_bytes;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::Json;
use std::sync::Arc;
use tokio::sync::RwLock;
fn state_with_index(id: &str) -> Arc<SearchAppState> {
let registry = IndexRegistry::new();
let tmp = std::env::temp_dir();
registry.register(IndexHandle::bare(
IndexId::new(id),
Arc::new(RwLock::new(CodeIndexer::new(id, &tmp))),
tmp,
));
Arc::new(SearchAppState::new(registry))
}
async fn body_json(resp: axum::response::Response) -> serde_json::Value {
let bytes = to_bytes(resp.into_body(), 1 << 20).await.expect("body");
serde_json::from_slice(&bytes).expect("json")
}
#[tokio::test]
async fn get_returns_current_config() {
let state = state_with_index("cfg-get");
let resp = index_config_handler(State(state), Path("cfg-get".to_string())).await;
assert_eq!(resp.status(), StatusCode::OK);
let v = body_json(resp).await;
let cfg: IndexConfigView = serde_json::from_value(v).expect("IndexConfigView");
assert_eq!(
cfg.data_file_max_bytes, 65_536,
"default 64 KiB cap surfaced"
);
assert!(
cfg.extra_skip_dirs.contains(&"data".to_string())
&& cfg.extra_skip_dirs.contains(&"reports".to_string()),
"default extra_skip_dirs surfaced: {:?}",
cfg.extra_skip_dirs
);
assert!(cfg.include_docs, "include_docs default true");
assert!(cfg.respect_gitignore, "respect_gitignore default true");
}
#[tokio::test]
async fn get_unknown_index_404() {
let state = Arc::new(SearchAppState::new(IndexRegistry::new()));
let resp = index_config_handler(State(state), Path("ghost".to_string())).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[serial_test::serial]
async fn get_then_patch_then_get_round_trips() {
let data = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data.path()) };
let state = state_with_index("cfg-rt");
let resp = index_config_handler(State(Arc::clone(&state)), Path("cfg-rt".to_string())).await;
let before: IndexConfigView = serde_json::from_value(body_json(resp).await).unwrap();
assert_eq!(before.data_file_max_bytes, 65_536);
let resp = patch_index_config_handler(
State(Arc::clone(&state)),
Path("cfg-rt".to_string()),
Json(PatchIndexConfigRequest {
extra_skip_dirs: Some(vec!["data".into(), "tmp".into()]),
data_file_max_bytes: Some(32_768),
..Default::default()
}),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let patched = body_json(resp).await;
assert_eq!(
patched["reindex_required"].as_bool(),
Some(true),
"PATCH must hint a reindex is required"
);
let resp = index_config_handler(State(Arc::clone(&state)), Path("cfg-rt".to_string())).await;
let after: IndexConfigView = serde_json::from_value(body_json(resp).await).unwrap();
assert_eq!(after.data_file_max_bytes, 32_768, "cap updated");
assert_eq!(
after.extra_skip_dirs,
vec!["data".to_string(), "tmp".to_string()]
);
assert_eq!(
after.include_docs, before.include_docs,
"unspecified field preserved"
);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}
#[tokio::test]
#[serial_test::serial]
async fn patch_updates_only_supplied_fields() {
let data = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data.path()) };
let state = state_with_index("cfg-partial");
let resp = patch_index_config_handler(
State(Arc::clone(&state)),
Path("cfg-partial".to_string()),
Json(PatchIndexConfigRequest {
extensions: Some(vec![".rs".into(), "py".into(), " ".into()]),
exclude_globs: Some(vec!["**/gen/**".into(), "".into()]),
..Default::default()
}),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let handle = state
.registry
.get(&IndexId::new("cfg-partial"))
.expect("handle");
assert_eq!(
handle.extensions,
vec!["rs".to_string(), "py".to_string()],
"leading dot stripped + blank dropped"
);
assert_eq!(handle.exclude_globs, vec!["**/gen/**".to_string()]);
assert_eq!(handle.data_file_max_bytes, 65_536);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}
#[tokio::test]
async fn patch_rejects_zero_cap() {
let state = state_with_index("cfg-zero");
let resp = patch_index_config_handler(
State(state),
Path("cfg-zero".to_string()),
Json(PatchIndexConfigRequest {
data_file_max_bytes: Some(0),
..Default::default()
}),
)
.await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
#[serial_test::serial]
async fn patch_persist_failure_returns_500() {
let tmp = tempfile::tempdir().unwrap();
let blocker = tmp.path().join("not-a-dir");
std::fs::write(&blocker, b"x").unwrap();
let bad_data_dir = blocker.join("data"); unsafe { std::env::set_var("TRUSTY_DATA_DIR", &bad_data_dir) };
let state = state_with_index("cfg-persist-fail");
let resp = patch_index_config_handler(
State(Arc::clone(&state)),
Path("cfg-persist-fail".to_string()),
Json(PatchIndexConfigRequest {
data_file_max_bytes: Some(16_384),
..Default::default()
}),
)
.await;
assert_eq!(
resp.status(),
StatusCode::INTERNAL_SERVER_ERROR,
"persist failure must surface as 500, not a silent 200"
);
let body = body_json(resp).await;
assert_eq!(
body["persisted"].as_bool(),
Some(false),
"body must report persisted:false: {body}"
);
assert!(
body["error"].as_str().is_some(),
"body must carry an error message: {body}"
);
let handle = state
.registry
.get(&IndexId::new("cfg-persist-fail"))
.expect("handle");
assert_eq!(handle.data_file_max_bytes, 16_384);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}
#[tokio::test]
async fn patch_unknown_index_404() {
let state = Arc::new(SearchAppState::new(IndexRegistry::new()));
let resp = patch_index_config_handler(
State(state),
Path("ghost".to_string()),
Json(PatchIndexConfigRequest::default()),
)
.await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[serial_test::serial]
async fn patch_persists_to_toml() {
let data = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data.path()) };
let state = state_with_index("cfg-persist");
let resp = patch_index_config_handler(
State(Arc::clone(&state)),
Path("cfg-persist".to_string()),
Json(PatchIndexConfigRequest {
data_file_max_bytes: Some(16_384),
extra_skip_dirs: Some(vec!["archive".into()]),
..Default::default()
}),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let entries = crate::service::persistence::load_index_registry().expect("load registry");
let entry = entries
.iter()
.find(|e| e.id == "cfg-persist")
.expect("entry persisted");
assert_eq!(entry.data_file_max_bytes, Some(16_384));
assert_eq!(entry.extra_skip_dirs, vec!["archive".to_string()]);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
}