use super::*;
use crate::core::embed::Embedder;
use crate::core::registry::IndexRegistry;
use axum::extract::State;
use axum::http::StatusCode;
use axum::Json;
use std::sync::Arc;
#[tokio::test]
async fn relocate_index_returns_404_for_unknown_id() {
use super::indexes_relocate::{relocate_index_handler, RelocateIndexRequest};
use axum::body::to_bytes;
use axum::extract::Path;
let state = SearchAppState::new(IndexRegistry::new());
let embedder: Arc<dyn Embedder> = Arc::new(crate::core::embed::MockEmbedder::new(8));
state.install_embedder(embedder).await;
let state_arc = Arc::new(state);
let resp = relocate_index_handler(
State(Arc::clone(&state_arc)),
Path("no-such-index-xyz".to_string()),
Json(RelocateIndexRequest {
root_path: std::path::PathBuf::from("/tmp"),
}),
)
.await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let body = to_bytes(resp.into_body(), 4096).await.expect("body");
let v: serde_json::Value = serde_json::from_slice(&body).expect("json");
let err = v.get("error").and_then(|x| x.as_str()).unwrap_or("");
assert!(
err.contains("no-such-index-xyz"),
"error should name the id: {err}"
);
}
#[tokio::test]
async fn relocate_index_updates_root_path() {
use super::indexes_relocate::{relocate_index_handler, RelocateIndexRequest};
use super::router::CreateIndexRequest;
use axum::body::to_bytes;
use axum::extract::Path;
let state = SearchAppState::new(IndexRegistry::new());
let embedder: Arc<dyn Embedder> = Arc::new(crate::core::embed::MockEmbedder::new(8));
state.install_embedder(embedder).await;
let state_arc = Arc::new(state);
let cwd = std::env::current_dir().expect("cwd");
let base = cwd.join("target");
std::fs::create_dir_all(&base).expect("create target/");
let old_dir = tempfile::Builder::new()
.prefix("ts-relocate-old-")
.tempdir_in(&base)
.expect("create old_dir");
let new_dir = tempfile::Builder::new()
.prefix("ts-relocate-new-")
.tempdir_in(&base)
.expect("create new_dir");
let old_root = old_dir.path().canonicalize().expect("canonicalize old_dir");
let new_root = new_dir.path().canonicalize().expect("canonicalize new_dir");
let create_resp = super::indexes::create_index_handler(
State(Arc::clone(&state_arc)),
Json(CreateIndexRequest {
id: "relocate-test".into(),
root_path: old_root.clone(),
include_paths: None,
exclude_globs: None,
extensions: None,
domain_terms: None,
path_filter: None,
include_docs: None,
respect_gitignore: None,
lexical_only: None,
skip_kg: None,
defer_embed: None,
extra_skip_dirs: None,
data_file_max_bytes: None,
}),
)
.await;
assert_eq!(
create_resp.status(),
StatusCode::OK,
"initial create must succeed"
);
let patch_resp = relocate_index_handler(
State(Arc::clone(&state_arc)),
Path("relocate-test".to_string()),
Json(RelocateIndexRequest {
root_path: new_root.clone(),
}),
)
.await;
assert_eq!(patch_resp.status(), StatusCode::OK, "relocate must succeed");
let body = to_bytes(patch_resp.into_body(), 4096).await.expect("body");
let v: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(
v.get("relocated").and_then(|x| x.as_bool()),
Some(true),
"response must carry relocated:true"
);
assert_eq!(
v.get("id").and_then(|x| x.as_str()),
Some("relocate-test"),
"response must echo the index id"
);
let handle = state_arc
.registry
.get(&crate::core::registry::IndexId::new("relocate-test"))
.expect("handle must still be in registry after relocate");
assert_eq!(
handle.root_path, new_root,
"handle.root_path must point at the new directory after relocate"
);
assert_ne!(
handle.root_path, old_root,
"handle.root_path must not retain the old directory"
);
}
#[test]
fn hash_cache_relative_key_matches_after_load() {
let map: dashmap::DashMap<std::path::PathBuf, String> = dashmap::DashMap::new();
let rel_path = std::path::PathBuf::from("src/main.rs");
let hash_value = "abc123def456".to_string(); map.insert(rel_path.clone(), hash_value.clone());
let lookup_key = std::path::PathBuf::from("src/main.rs");
let got = map.get(&lookup_key).map(|v| v.clone());
assert_eq!(
got.as_deref(),
Some(hash_value.as_str()),
"relative-key lookup must hit the relative-key entry in the DashMap"
);
let abs_key = std::path::PathBuf::from("/some/project/root/src/main.rs");
let miss = map.get(&abs_key).map(|v| v.clone());
assert!(
miss.is_none(),
"absolute-key lookup must NOT match a relative-key entry (old bug)"
);
}
#[test]
fn colocated_fallback_is_false_when_disk_entry_absent() {
use crate::service::persistence::load_index_registry_at;
use std::path::PathBuf;
let missing = PathBuf::from("/tmp/nonexistent-trusty-search-test-xyz/indexes.toml");
let on_disk_colocated = load_index_registry_at(&missing)
.ok()
.and_then(|entries| entries.into_iter().find(|e| e.id == "any-index"))
.map(|e| e.colocated)
.unwrap_or(false);
assert!(
!on_disk_colocated,
"colocated fallback must be false when disk entry is absent/unreadable (issue #1097)"
);
let tmp = tempfile::tempdir().expect("tempdir");
let toml_path = tmp.path().join("indexes.toml");
crate::service::persistence::upsert_index_registry_entry_at(
&toml_path,
crate::service::persistence::PersistedIndex {
id: "existing-colocated".to_string(),
root_path: PathBuf::from("/some/root"),
colocated: true,
..crate::service::persistence::PersistedIndex::default()
},
)
.expect("write entry");
let found = load_index_registry_at(&toml_path)
.ok()
.and_then(|entries| entries.into_iter().find(|e| e.id == "existing-colocated"))
.map(|e| e.colocated)
.unwrap_or(false);
assert!(
found,
"colocated must be true when the disk entry explicitly says so"
);
}
#[test]
fn relocate_preserves_lru_timestamps() {
use crate::service::persistence::{
load_index_registry_at, upsert_index_registry_entry_at, PersistedIndex,
};
use std::path::PathBuf;
let tmp = tempfile::tempdir().expect("tempdir");
let toml_path = tmp.path().join("indexes.toml");
let entry = PersistedIndex {
id: "lru-relocate-test".to_string(),
root_path: PathBuf::from("/projects/lru-relocate-test"),
last_queried_unix: Some(1_700_000_000),
last_indexed_unix: Some(1_699_000_000),
..PersistedIndex::default()
};
upsert_index_registry_entry_at(&toml_path, entry).expect("write entry");
let on_disk = load_index_registry_at(&toml_path)
.ok()
.and_then(|entries| entries.into_iter().find(|e| e.id == "lru-relocate-test"));
let on_disk_last_queried = on_disk.as_ref().and_then(|e| e.last_queried_unix);
let on_disk_last_indexed = on_disk.as_ref().and_then(|e| e.last_indexed_unix);
assert_eq!(
on_disk_last_queried,
Some(1_700_000_000),
"last_queried_unix must be preserved after a PATCH (PR #1103)"
);
assert_eq!(
on_disk_last_indexed,
Some(1_699_000_000),
"last_indexed_unix must be preserved after a PATCH (PR #1103)"
);
let entry_no_ts = PersistedIndex {
id: "lru-no-ts-test".to_string(),
root_path: PathBuf::from("/projects/lru-no-ts"),
..PersistedIndex::default()
};
upsert_index_registry_entry_at(&toml_path, entry_no_ts).expect("write entry-no-ts");
let on_disk2 = load_index_registry_at(&toml_path)
.ok()
.and_then(|entries| entries.into_iter().find(|e| e.id == "lru-no-ts-test"));
assert!(
on_disk2
.as_ref()
.and_then(|e| e.last_queried_unix)
.is_none(),
"None timestamps must remain None after round-trip"
);
}
#[test]
fn patch_index_a_does_not_strip_exclude_globs_of_index_b() {
use crate::service::persistence::load_index_registry_at;
use crate::service::persistence::{upsert_index_registry_entry_at, PersistedIndex};
use std::path::PathBuf;
let tmp = tempfile::tempdir().expect("tempdir");
let toml_path = tmp.path().join("indexes.toml");
let entry_a = PersistedIndex {
id: "index-a".to_string(),
root_path: PathBuf::from("/projects/index-a"),
..PersistedIndex::default()
};
let entry_b = PersistedIndex {
id: "index-b".to_string(),
root_path: PathBuf::from("/projects/index-b"),
exclude_globs: vec!["**/vendor/**".to_string(), "*.generated.ts".to_string()],
..PersistedIndex::default()
};
upsert_index_registry_entry_at(&toml_path, entry_a).expect("write entry-a");
upsert_index_registry_entry_at(&toml_path, entry_b).expect("write entry-b");
let patched_a = PersistedIndex {
id: "index-a".to_string(),
root_path: PathBuf::from("/projects/index-a-new"),
..PersistedIndex::default()
};
upsert_index_registry_entry_at(&toml_path, patched_a).expect("patch entry-a");
let entries = load_index_registry_at(&toml_path).expect("reload");
let b = entries
.iter()
.find(|e| e.id == "index-b")
.expect("index-b must still be present after patching index-a");
assert_eq!(
b.exclude_globs,
vec!["**/vendor/**".to_string(), "*.generated.ts".to_string()],
"index-b's exclude_globs must survive a PATCH to index-a (issue #1089)"
);
let a = entries
.iter()
.find(|e| e.id == "index-a")
.expect("index-a must still be present");
assert_eq!(
a.root_path,
PathBuf::from("/projects/index-a-new"),
"index-a's root_path must reflect the PATCH"
);
}