use super::embedder::{LazySlotEmbedderAdapter, UdsEmbedderAdapter};
use super::*;
use crate::core::registry::StageStatus;
fn inputs() -> WarmBootInputs {
WarmBootInputs {
chunk_count: 0,
hnsw_snapshot_ready: false,
graph_node_count: 0,
lexical_only: false,
skip_kg: false,
corpus_open_failed: false,
}
}
#[test]
fn warm_boot_marks_lexical_ready_when_chunks_present() {
let stages = derive_warm_boot_stages(WarmBootInputs {
chunk_count: 14_823,
..inputs()
});
assert_eq!(stages.lexical.status, StageStatus::Ready);
assert!(stages.search_capabilities().contains(&"bm25"));
assert!(stages.search_capabilities().contains(&"literal"));
assert!(stages.search_capabilities().contains(&"exact_match"));
}
#[test]
fn warm_boot_marks_semantic_ready_when_hnsw_snapshot_exists() {
let stages = derive_warm_boot_stages(WarmBootInputs {
chunk_count: 14_823,
hnsw_snapshot_ready: true,
..inputs()
});
assert_eq!(stages.semantic.status, StageStatus::Ready);
assert!(stages.search_capabilities().contains(&"vector"));
}
#[test]
fn warm_boot_marks_graph_ready_when_symbol_graph_nonempty() {
let stages = derive_warm_boot_stages(WarmBootInputs {
chunk_count: 14_823,
hnsw_snapshot_ready: true,
graph_node_count: 7_402,
..inputs()
});
assert_eq!(stages.graph.status, StageStatus::Ready);
assert!(stages.search_capabilities().contains(&"kg"));
}
#[test]
fn warm_boot_marks_semantic_pending_when_no_snapshot() {
let stages = derive_warm_boot_stages(WarmBootInputs {
chunk_count: 14_823,
hnsw_snapshot_ready: false,
..inputs()
});
assert_eq!(stages.lexical.status, StageStatus::Ready);
assert_eq!(stages.semantic.status, StageStatus::Pending);
assert!(!stages.search_capabilities().contains(&"vector"));
}
#[test]
fn warm_boot_respects_lexical_only_flag() {
let stages = derive_warm_boot_stages(WarmBootInputs {
chunk_count: 14_823,
hnsw_snapshot_ready: true,
graph_node_count: 7_402,
lexical_only: true,
skip_kg: false,
corpus_open_failed: false,
});
assert_eq!(stages.lexical.status, StageStatus::Ready);
assert_eq!(stages.semantic.status, StageStatus::Skipped);
assert_eq!(stages.graph.status, StageStatus::Skipped);
let caps = stages.search_capabilities();
assert!(caps.contains(&"bm25"));
assert!(!caps.contains(&"vector"));
assert!(!caps.contains(&"kg"));
}
#[test]
fn warm_boot_marks_mid_reindex_as_in_progress() {
let stages = derive_warm_boot_stages(inputs());
assert_eq!(stages.lexical.status, StageStatus::InProgress);
assert_eq!(stages.lifecycle_status(), "walking");
assert!(stages.search_capabilities().is_empty());
}
#[test]
fn warm_boot_respects_skip_kg_flag() {
let stages = derive_warm_boot_stages(WarmBootInputs {
chunk_count: 14_823,
hnsw_snapshot_ready: true,
graph_node_count: 7_402,
lexical_only: false,
skip_kg: true,
corpus_open_failed: false,
});
assert_eq!(
stages.graph.status,
StageStatus::Skipped,
"skip_kg must force graph to Skipped even when on-disk graph is non-empty"
);
assert_eq!(
stages.semantic.status,
StageStatus::Ready,
"skip_kg must not affect the semantic lane"
);
let caps = stages.search_capabilities();
assert!(
!caps.contains(&"kg"),
"skip_kg must suppress the kg capability"
);
assert!(
caps.contains(&"vector"),
"skip_kg must not suppress the vector capability"
);
let stages_both = derive_warm_boot_stages(WarmBootInputs {
chunk_count: 14_823,
hnsw_snapshot_ready: true,
graph_node_count: 7_402,
lexical_only: true,
skip_kg: true,
corpus_open_failed: false,
});
assert_eq!(stages_both.semantic.status, StageStatus::Skipped);
assert_eq!(stages_both.graph.status, StageStatus::Skipped);
let caps = stages_both.search_capabilities();
assert!(!caps.contains(&"vector"));
assert!(!caps.contains(&"kg"));
}
#[test]
#[serial]
fn missing_binary_fails_fast_with_install_hint() {
use crate::service::embedder_supervisor::locate_embedderd_binary;
let prev = std::env::var("TRUSTY_EMBEDDERD_BIN").ok();
unsafe {
std::env::set_var(
"TRUSTY_EMBEDDERD_BIN",
"/nonexistent/path/trusty-embedderd-missing",
);
}
let result = locate_embedderd_binary();
unsafe {
match prev {
Some(v) => std::env::set_var("TRUSTY_EMBEDDERD_BIN", v),
None => std::env::remove_var("TRUSTY_EMBEDDERD_BIN"),
}
}
assert!(
result.is_err(),
"locate_embedderd_binary must return Err when binary is absent"
);
let locate_err = result.unwrap_err();
let wrapped = format!(
"{locate_err}\n\n\
ERROR: trusty-embedderd binary not found on PATH.\n\
\n\
trusty-search v0.13+ requires trusty-embedderd to be installed alongside it.\n\
\n\
Install it with:\n\
\x20 cargo install trusty-embedderd --locked\n\
\n\
Or set TRUSTY_EMBEDDERD_BIN to an absolute path:\n\
\x20 export TRUSTY_EMBEDDERD_BIN=/path/to/trusty-embedderd\n\
\n\
If you need to run without the sidecar (tests, debugging), use:\n\
\x20 TRUSTY_EMBEDDER=in-process trusty-search start"
);
assert!(
wrapped.contains("cargo install trusty-embedderd"),
"install hint must contain 'cargo install trusty-embedderd'; got: {wrapped}"
);
assert!(
wrapped.contains("TRUSTY_EMBEDDER=in-process"),
"escape hatch hint must mention TRUSTY_EMBEDDER=in-process; got: {wrapped}"
);
}
#[test]
fn no_auto_discover_resolution() {
fn should_skip_discovery(cli_flag: bool, env_val: Option<&str>) -> bool {
if cli_flag {
return true;
}
matches!(env_val, Some("1") | Some("true"))
}
assert!(
!should_skip_discovery(false, None),
"scan must be enabled by default"
);
assert!(
should_skip_discovery(true, None),
"--no-auto-discover must suppress scan"
);
assert!(
should_skip_discovery(false, Some("1")),
"TRUSTY_NO_AUTO_DISCOVER=1 must suppress scan"
);
assert!(
should_skip_discovery(false, Some("true")),
"TRUSTY_NO_AUTO_DISCOVER=true must suppress scan"
);
assert!(
should_skip_discovery(true, Some("1")),
"CLI flag must take precedence"
);
assert!(
!should_skip_discovery(false, Some("0")),
"TRUSTY_NO_AUTO_DISCOVER=0 must not suppress scan"
);
assert!(
!should_skip_discovery(false, Some("")),
"empty env value must not suppress scan"
);
}
use crate::commands::start_restore::try_locate_moved_root;
use crate::service::colocated_storage::COLOCATED_DIR_NAME;
use crate::service::persistence::PersistedIndex;
use serial_test::serial;
use tempfile::tempdir;
fn make_populated_ts(root: &std::path::Path) {
let ts_dir = root.join(COLOCATED_DIR_NAME);
std::fs::create_dir_all(&ts_dir).unwrap();
std::fs::write(ts_dir.join("index.redb"), b"notempty").unwrap();
}
#[test]
#[serial]
fn restore_moved_colocated_index_relinks_unique_candidate() {
let data_tmp = tempdir().unwrap();
let new_root = tempdir().unwrap();
make_populated_ts(new_root.path());
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data_tmp.path()) };
crate::service::roots_registry::upsert_root(new_root.path().to_path_buf()).unwrap();
let dead_root = std::path::PathBuf::from("/tmp/trusty-484-dead-root-xyz9999");
let entry = PersistedIndex {
id: "moved-project".to_string(),
root_path: dead_root.clone(),
colocated: true,
..Default::default()
};
let result = try_locate_moved_root(&entry, &[]);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
let new_path = result.expect("must find the unique candidate");
assert_eq!(
new_path.canonicalize().unwrap(),
new_root.path().canonicalize().unwrap(),
"must relink to the tracked root containing .trusty-search/"
);
}
#[test]
#[serial]
fn restore_missing_root_with_no_candidate_returns_none() {
let data_tmp = tempdir().unwrap();
let empty_root = tempdir().unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data_tmp.path()) };
crate::service::roots_registry::upsert_root(empty_root.path().to_path_buf()).unwrap();
let dead_root = std::path::PathBuf::from("/tmp/trusty-484-no-candidate-xyz9999");
let entry = PersistedIndex {
id: "no-candidate".to_string(),
root_path: dead_root.clone(),
colocated: true,
..Default::default()
};
let result = try_locate_moved_root(&entry, &[]);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
assert!(
result.is_none(),
"must return None when no candidate has a populated .trusty-search/"
);
let ghost = dead_root.join(COLOCATED_DIR_NAME);
assert!(
!ghost.exists(),
"must not create a ghost .trusty-search/ under the missing root"
);
}
#[test]
#[serial]
fn restore_missing_root_with_ambiguous_candidates_returns_none() {
let data_tmp = tempdir().unwrap();
let root_a = tempdir().unwrap();
let root_b = tempdir().unwrap();
make_populated_ts(root_a.path());
make_populated_ts(root_b.path());
unsafe { std::env::set_var("TRUSTY_DATA_DIR", data_tmp.path()) };
crate::service::roots_registry::upsert_root(root_a.path().to_path_buf()).unwrap();
crate::service::roots_registry::upsert_root(root_b.path().to_path_buf()).unwrap();
let dead_root = std::path::PathBuf::from("/tmp/trusty-484-ambiguous-xyz9999");
let entry = PersistedIndex {
id: "ambiguous".to_string(),
root_path: dead_root,
colocated: true,
..Default::default()
};
let result = try_locate_moved_root(&entry, &[]);
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
assert!(
result.is_none(),
"must return None when multiple candidates exist (ambiguous)"
);
}
#[test]
fn canonicalize_best_effort_resolves_existing_path() {
let tmp = tempdir().unwrap();
let expected = std::fs::canonicalize(tmp.path()).unwrap();
let got = canonicalize_best_effort(tmp.path());
assert_eq!(
got, expected,
"canonicalize_best_effort must return the canonical form for an existing path"
);
}
#[test]
fn canonicalize_best_effort_falls_back_for_missing_path() {
let missing = std::path::PathBuf::from("/tmp/trusty-541-definitely-does-not-exist-xyz");
let got = canonicalize_best_effort(&missing);
assert_eq!(
got, missing,
"canonicalize_best_effort must fall back to the input for a missing path"
);
}
#[cfg(unix)]
#[test]
fn canonicalize_best_effort_resolves_symlink() {
use std::os::unix::fs::symlink;
let real_dir = tempdir().unwrap();
let real_canonical = std::fs::canonicalize(real_dir.path()).unwrap();
let link = real_canonical
.parent()
.unwrap()
.join(format!("trusty-541-symlink-{}", std::process::id()));
let _ = std::fs::remove_file(&link);
symlink(&real_canonical, &link).expect("create symlink");
let got = canonicalize_best_effort(&link);
let _ = std::fs::remove_file(&link);
assert_eq!(
got, real_canonical,
"canonicalize_best_effort must resolve symlinks to their target"
);
}
#[test]
fn lazy_adapter_reports_resolved_provider() {
use crate::core::Embedder as _;
use crate::service::embedder_supervisor::{LazyEmbedderHandle, SupervisorConfig};
let handle = std::sync::Arc::new(LazyEmbedderHandle::new(
std::path::PathBuf::from("/nonexistent/trusty-embedderd"),
SupervisorConfig::default(),
));
let adapter = LazySlotEmbedderAdapter { handle };
assert_eq!(
adapter.provider(),
trusty_common::embedder::resolve_expected_provider(),
"lazy stdio adapter must report the sidecar's resolved provider, not the CPU default"
);
}
#[test]
fn uds_adapter_reports_resolved_provider() {
use crate::core::Embedder as _;
let adapter = UdsEmbedderAdapter {
client: trusty_common::embedder_client::UdsEmbedderClient::new(std::path::PathBuf::from(
"/tmp/nonexistent-trusty-604.sock",
)),
};
assert_eq!(
adapter.provider(),
trusty_common::embedder::resolve_expected_provider(),
"uds adapter must report the sidecar's resolved provider, not the CPU default"
);
}