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_withholds_id_when_nothing_was_registered() {
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 report = ensure_project_indexed_reporting(
&nested,
IndexOptions::default().with_allow_sensitive_path(true),
);
let pinnable = 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!(
report.index_id,
Some(expected),
"id is the git-root basename"
);
assert_ne!(
report.registration,
IndexRegistration::Confirmed,
"no daemon was contacted, so nothing can be confirmed"
);
assert_eq!(
pinnable, None,
"an unregistered index must not come back as a pinnable id (#5091)"
);
}
#[test]
fn reporting_says_skipped_under_test_harness() {
let _guard = crate::data_dir::ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let project = scratch_dir("report-skip");
fs::create_dir_all(project.join(".git")).unwrap();
let report = ensure_project_indexed_reporting(&project, IndexOptions::default());
let _ = fs::remove_dir_all(&project);
assert_eq!(
report.registration,
IndexRegistration::SkippedUnderTest,
"a test process suppresses the write (#4255) and must say so"
);
assert!(
report.index_id.is_some(),
"the id is still returned — the fail-open contract is unchanged"
);
}
#[test]
fn reporting_says_daemon_unreachable_when_no_daemon_is_discoverable() {
let _guard = crate::data_dir::ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let data_dir = scratch_dir("report-nodaemon-data");
fs::create_dir_all(&data_dir).unwrap();
let project = scratch_dir("report-nodaemon");
fs::create_dir_all(project.join(".git")).unwrap();
unsafe {
std::env::set_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV, &data_dir);
std::env::set_var(crate::test_harness::ALLOW_PRODUCTION_ENV, "1");
}
let report = ensure_project_indexed_reporting(&project, IndexOptions::default());
unsafe {
std::env::remove_var(crate::test_harness::ALLOW_PRODUCTION_ENV);
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!(
report.registration,
IndexRegistration::DaemonUnreachable,
"no address file means nothing was sent — that is not a registration"
);
assert!(report.index_id.is_some(), "the id is still returned");
}
#[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,
IndexOptions {
allow_sensitive_path: allow,
..IndexOptions::default()
},
);
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 create_index_request_body_sets_skip_vector() {
let root = Path::new("/Users/dev/projects/my-repo/.worktrees/feat-x");
for allow in [true, false] {
for skip_vector in [true, false] {
let body = create_index_request_body(
"feat-x",
root,
IndexOptions {
allow_sensitive_path: allow,
skip_vector,
},
);
assert_eq!(
body.get("skip_vector"),
Some(&serde_json::Value::Bool(skip_vector)),
"body must set skip_vector: {skip_vector} (allow={allow})"
);
assert_eq!(
body.get("allow_sensitive_path"),
Some(&serde_json::Value::Bool(allow)),
"skip_vector must not disturb allow_sensitive_path"
);
}
}
}
#[test]
fn index_options_default_matches_legacy_ensure_call() {
let root = Path::new("/Users/dev/projects/my-repo");
assert_eq!(
create_index_request_body("my-repo", root, IndexOptions::default()),
create_index_request_body(
"my-repo",
root,
IndexOptions {
allow_sensitive_path: false,
skip_vector: false,
}
)
);
}
#[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);
std::env::set_var(crate::test_harness::ALLOW_PRODUCTION_ENV, "1");
}
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);
std::env::remove_var(crate::test_harness::ALLOW_PRODUCTION_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);
}
fn daemon_was_contacted_during(body: impl FnOnce()) -> bool {
use crate::data_dir::{DATA_DIR_OVERRIDE_ENV, ENV_LOCK};
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind stand-in daemon");
let addr = listener.local_addr().expect("stand-in daemon local_addr");
listener
.set_nonblocking(true)
.expect("stand-in daemon set_nonblocking");
let guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let data_dir = scratch_dir("4255-daemon");
fs::create_dir_all(&data_dir).expect("create isolated data dir");
let previous = std::env::var(DATA_DIR_OVERRIDE_ENV).ok();
unsafe { std::env::set_var(DATA_DIR_OVERRIDE_ENV, &data_dir) };
crate::write_daemon_addr("trusty-search", &addr.to_string()).expect("publish daemon addr");
assert_eq!(
crate::resolve_daemon_base_url("trusty-search"),
Some(format!("http://{addr}")),
"the stand-in daemon must be discoverable, or this test proves nothing"
);
body();
match previous {
Some(p) => unsafe { std::env::set_var(DATA_DIR_OVERRIDE_ENV, p) },
None => unsafe { std::env::remove_var(DATA_DIR_OVERRIDE_ENV) },
}
drop(guard);
let _ = fs::remove_dir_all(&data_dir);
std::thread::sleep(std::time::Duration::from_millis(250));
!matches!(
listener.accept(),
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock
)
}
#[test]
fn ensure_project_indexed_never_writes_to_a_daemon_under_test() {
let root = scratch_dir("4255-ensure");
fs::create_dir_all(&root).expect("create fixture root");
let mut id = None;
let contacted = daemon_was_contacted_during(|| {
id = ensure_project_indexed(&root, true);
});
assert!(
!contacted,
"ensure_project_indexed contacted a live trusty-search daemon from a test \
process — that is the issue #4255 registry leak"
);
assert!(
id.is_none(),
"the guard suppressed the write, so no index was registered — handing \
back a pinnable id anyway is the #5091 fail-open shape"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn index_files_inner_never_writes_to_a_daemon_under_test() {
let root = scratch_dir("4255-incremental");
fs::create_dir_all(&root).expect("create fixture root");
let file = root.join("fixture.rs");
fs::write(&file, "fn fixture() {}\n").expect("write fixture file");
let contacted = daemon_was_contacted_during(|| {
index_files_inner(&root, std::slice::from_ref(&file));
});
assert!(
!contacted,
"index_files_inner pushed fixture content to a live trusty-search daemon \
from a test process (issue #4255)"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn index_options_builders_match_field_construction() {
assert_eq!(
IndexOptions::default().with_skip_vector(true),
IndexOptions {
allow_sensitive_path: false,
skip_vector: true,
}
);
assert_eq!(
IndexOptions::default().with_allow_sensitive_path(true),
IndexOptions {
allow_sensitive_path: true,
skip_vector: false,
}
);
assert_eq!(
IndexOptions::default()
.with_skip_vector(true)
.with_allow_sensitive_path(true),
IndexOptions {
allow_sensitive_path: true,
skip_vector: true,
}
);
}
fn one_shot_daemon(
status_line: &'static str,
) -> (std::net::SocketAddr, std::thread::JoinHandle<()>) {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind fake daemon");
let addr = listener.local_addr().expect("fake daemon local_addr");
let handle = std::thread::spawn(move || {
use std::io::{Read, Write};
let Ok((mut stream, _)) = listener.accept() else {
return;
};
drop(listener);
let mut buf = [0u8; 4096];
let _ = stream.read(&mut buf);
let _ = stream.write_all(
format!("{status_line}\r\ncontent-length: 0\r\nconnection: close\r\n\r\n").as_bytes(),
);
let _ = stream.flush();
});
(addr, handle)
}
fn publish_daemon_addr(data_dir: &Path, addr: std::net::SocketAddr) {
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();
}
fn with_refusing_daemon<T>(
tag: &str,
status_line: &'static str,
body: impl FnOnce(&Path) -> T,
) -> T {
let _guard = crate::data_dir::ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let data_dir = scratch_dir(&format!("5091-data-{tag}"));
fs::create_dir_all(&data_dir).unwrap();
unsafe {
std::env::set_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV, &data_dir);
std::env::set_var(crate::test_harness::ALLOW_PRODUCTION_ENV, "1");
}
let (addr, server) = one_shot_daemon(status_line);
publish_daemon_addr(&data_dir, addr);
let project = scratch_dir(&format!("5091-project-{tag}"));
fs::create_dir_all(project.join(".git")).unwrap();
let out = body(&project);
let _ = server.join();
unsafe {
std::env::remove_var(crate::data_dir::DATA_DIR_OVERRIDE_ENV);
std::env::remove_var(crate::test_harness::ALLOW_PRODUCTION_ENV);
}
let _ = fs::remove_dir_all(&project);
let _ = fs::remove_dir_all(&data_dir);
out
}
#[test]
fn create_rejected_by_the_daemon_withholds_the_pinnable_id() {
let refused = "HTTP/1.1 500 Internal Server Error";
let id = with_refusing_daemon("ensure", refused, |project| {
ensure_project_indexed(project, false)
});
assert_eq!(
id, None,
"ensure_project_indexed returned a pinnable id after the daemon REFUSED \
the create (HTTP 500) — pinning it makes every later search 404 (#5091)"
);
let id = with_refusing_daemon("ensure-with", refused, |project| {
ensure_project_indexed_with(project, IndexOptions::default().with_skip_vector(true))
});
assert_eq!(
id, None,
"ensure_project_indexed_with returned a pinnable id after the daemon \
REFUSED the create (HTTP 500) — #5091"
);
let (report, expected) = with_refusing_daemon("report", refused, |project| {
(
ensure_project_indexed_reporting(project, IndexOptions::default()),
crate::derive_index_id(project),
)
});
assert_eq!(
report.registration,
IndexRegistration::NotConfirmed,
"a 500 on the create is not a registration"
);
assert_eq!(
report.index_id,
Some(expected),
"the derived id stays available for logging and GC — it is the PIN that \
is withheld, not the id"
);
}