use super::*;
use std::fs;
use std::path::PathBuf;
fn scratch_dir(tag: &str) -> PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let p = std::env::temp_dir().join(format!("trusty-search-index-{tag}-{pid}-{nanos}"));
let _ = fs::remove_dir_all(&p);
p
}
#[test]
fn ensure_project_indexed_returns_derived_id_when_daemon_down() {
let _guard = crate::data_dir::ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let data_dir = scratch_dir("data");
fs::create_dir_all(&data_dir).unwrap();
unsafe {
std::env::set_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV, &data_dir);
}
let project = scratch_dir("git");
fs::create_dir_all(project.join(".git")).unwrap();
let nested = project.join("crates/inner");
fs::create_dir_all(&nested).unwrap();
let id = ensure_project_indexed(&nested, true);
let expected = crate::derive_index_id(&project);
unsafe {
std::env::remove_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV);
}
let _ = fs::remove_dir_all(&project);
let _ = fs::remove_dir_all(&data_dir);
assert_eq!(id, Some(expected), "id is the git-root basename");
}
#[test]
fn ensure_project_indexed_none_for_root() {
assert_eq!(ensure_project_indexed(Path::new("/"), true), None);
assert_eq!(ensure_project_indexed(Path::new("/"), false), None);
}
#[test]
fn index_files_inner_is_noop_for_empty_paths() {
index_files_inner(Path::new("/"), &[]);
}
#[test]
fn index_files_inner_skips_when_index_id_empty() {
index_files_inner(Path::new("/"), &[PathBuf::from("some/file.rs")]);
}
#[test]
fn index_files_inner_skips_gracefully_when_daemon_down() {
let _guard = crate::data_dir::ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let data_dir = scratch_dir("data-incr");
fs::create_dir_all(&data_dir).unwrap();
unsafe {
std::env::set_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV, &data_dir);
}
let project = scratch_dir("git-incr");
fs::create_dir_all(project.join(".git")).unwrap();
fs::write(project.join("main.rs"), "fn main() {}\n").unwrap();
index_files_inner(&project, &[PathBuf::from("main.rs")]);
unsafe {
std::env::remove_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV);
}
let _ = fs::remove_dir_all(&project);
let _ = fs::remove_dir_all(&data_dir);
}
#[test]
fn relative_index_path_strips_root_prefix() {
let root = Path::new("/Users/dev/my-project");
let abs = root.join("src/main.rs");
assert_eq!(relative_index_path(root, &abs), "src/main.rs");
}
#[test]
fn relative_index_path_falls_back_for_paths_outside_root() {
let root = Path::new("/Users/dev/my-project");
let elsewhere = Path::new("/somewhere/else/file.py");
assert_eq!(
relative_index_path(root, elsewhere),
"/somewhere/else/file.py"
);
}
#[test]
fn index_file_request_body_targets_relative_path_and_content() {
let body = index_file_request_body("src/main.rs", "fn main() {}\n");
assert_eq!(
body.get("path").and_then(serde_json::Value::as_str),
Some("src/main.rs")
);
assert_eq!(
body.get("content").and_then(serde_json::Value::as_str),
Some("fn main() {}\n")
);
assert!(
body.get("allow_sensitive_path").is_none(),
"the per-file endpoint does not re-check the denylist, so no bypass \
flag should be sent: {body:?}"
);
}
#[test]
fn create_index_request_body_respects_allow_sensitive_path_param() {
for root in [
Path::new("/Users/dev/projects/my-repo"),
Path::new("/private/var/folders/xx/scratch-project"),
] {
for allow in [true, false] {
let body = create_index_request_body("my-index", root, allow);
assert_eq!(
body.get("allow_sensitive_path"),
Some(&serde_json::Value::Bool(allow)),
"request body for root {root:?} must set allow_sensitive_path: {allow}"
);
assert_eq!(
body.get("id").and_then(serde_json::Value::as_str),
Some("my-index")
);
}
}
}
#[test]
fn ensure_project_indexed_sends_allow_sensitive_path_through_to_create_body() {
for allow in [true, false] {
let _guard = crate::data_dir::ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let data_dir = scratch_dir(&format!("wire-{allow}"));
fs::create_dir_all(&data_dir).unwrap();
unsafe {
std::env::set_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV, &data_dir);
}
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = std::sync::mpsc::channel();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
let (mut stream, _) = listener.accept().unwrap();
drop(listener);
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).unwrap();
let request = String::from_utf8_lossy(&buf[..n]).to_string();
let _ = stream
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\nconnection: close\r\n\r\n");
let _ = stream.flush();
let body = request.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
let _ = tx.send(body);
});
let search_data_dir = data_dir.join("trusty-search");
fs::create_dir_all(&search_data_dir).unwrap();
fs::write(search_data_dir.join("http_addr"), addr.to_string()).unwrap();
let project = scratch_dir(&format!("wire-project-{allow}"));
fs::create_dir_all(project.join(".git")).unwrap();
let _ = ensure_project_indexed(&project, allow);
let body_json: serde_json::Value = serde_json::from_str(
&rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("fake daemon must have received the create-index POST"),
)
.expect("captured body must be valid JSON");
let _ = server.join();
unsafe {
std::env::remove_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV);
}
let _ = fs::remove_dir_all(&project);
let _ = fs::remove_dir_all(&data_dir);
assert_eq!(
body_json.get("allow_sensitive_path"),
Some(&serde_json::Value::Bool(allow)),
"POST /indexes body must carry allow_sensitive_path={allow} \
all the way from ensure_project_indexed's parameter; got {body_json:?}"
);
}
}
#[test]
fn index_is_fresh_true_when_recently_indexed_with_chunks() {
let now = chrono::Utc::now();
let status = serde_json::json!({
"chunk_count": 42,
"last_indexed": now.to_rfc3339(),
});
assert!(index_is_fresh(&status));
}
#[test]
fn index_is_fresh_false_when_no_chunks() {
let now = chrono::Utc::now();
let status = serde_json::json!({
"chunk_count": 0,
"last_indexed": now.to_rfc3339(),
});
assert!(!index_is_fresh(&status));
}
#[test]
fn index_is_fresh_false_when_stale() {
let stale = chrono::Utc::now() - chrono::Duration::hours(2);
let status = serde_json::json!({
"chunk_count": 10,
"last_indexed": stale.to_rfc3339(),
});
assert!(!index_is_fresh(&status));
}
#[test]
fn index_is_fresh_false_when_last_indexed_missing_or_malformed() {
assert!(!index_is_fresh(&serde_json::json!({ "chunk_count": 10 })));
assert!(!index_is_fresh(&serde_json::json!({
"chunk_count": 10,
"last_indexed": "not-a-timestamp",
})));
assert!(!index_is_fresh(&serde_json::json!({})));
}
#[test]
fn retry_backoff_is_bounded_and_increasing() {
use std::time::Duration;
assert_eq!(retry_backoff(1), Duration::from_millis(50));
assert_eq!(retry_backoff(2), Duration::from_millis(150));
assert_eq!(retry_backoff(3), Duration::from_millis(450));
assert!(retry_backoff(2) > retry_backoff(1));
assert!(retry_backoff(3) > retry_backoff(2));
assert_eq!(retry_backoff(100), Duration::from_millis(1000));
}
fn drive_retry_test(
server_fn: impl FnOnce(std::net::TcpListener, std::sync::mpsc::Sender<usize>) + Send + 'static,
) -> (IndexOutcome, usize) {
use std::net::TcpListener;
use std::sync::mpsc;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = mpsc::channel();
let server = std::thread::spawn(move || server_fn(listener, tx));
let client = build_index_client().unwrap();
let url = format!("http://{addr}/indexes/test-index/index-file");
let body = index_file_request_body("src/main.rs", "fn main() {}\n");
let outcome = post_index_file_with_retries(&client, &url, &body);
let accepted = rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect("server thread should have reported an accepted-connection count");
let _ = server.join();
(outcome, accepted)
}
#[test]
fn post_index_file_retries_transient_send_failure() {
use std::io::{Read, Write};
let (outcome, accepted) = drive_retry_test(|listener, tx| {
let mut accepted = 0usize;
for stream in listener.incoming() {
let Ok(mut stream) = stream else { break };
accepted += 1;
if accepted == 1 {
drop(stream);
continue;
}
let mut buf = [0u8; 4096];
let _ = stream.read(&mut buf);
let _ = stream
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\nconnection: close\r\n\r\n");
let _ = stream.flush();
let _ = tx.send(accepted);
break;
}
});
assert_eq!(outcome, IndexOutcome::Indexed);
assert_eq!(accepted, 2);
}
#[test]
fn post_index_file_exhausts_retries_and_returns_send_failed() {
let (outcome, accepted) = drive_retry_test(|listener, tx| {
let mut accepted = 0usize;
for stream in listener.incoming() {
let Ok(stream) = stream else { break };
accepted += 1;
drop(stream);
if accepted >= MAX_INDEX_ATTEMPTS as usize {
let _ = tx.send(accepted);
break;
}
}
});
assert_eq!(outcome, IndexOutcome::SendFailed);
assert_eq!(accepted, MAX_INDEX_ATTEMPTS as usize);
}