use serde::{Deserialize, Serialize};
use std::io::Write;
use std::path::{Path, PathBuf};
pub const LEINDEX_HOME_ENV: &str = "LEINDEX_HOME";
const DEFAULT_MODEL_DIR_SUFFIX: &str = "models";
const DEFAULT_MODEL_NAME: &str = "qwen3-embed-0.6b-dynamic-uint8";
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct LeIndexConfig {
#[serde(default = "default_legacy_neural_config")]
pub neural: NeuralConfig,
#[serde(default)]
pub search: SearchConfig,
#[serde(default = "default_legacy_indexing_config")]
pub indexing: IndexingConfig,
#[serde(default)]
pub mcp: McpConfig,
#[serde(default)]
pub system_one: SystemOneConfig,
#[serde(default)]
pub services: std::collections::BTreeMap<String, ServiceConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ServiceConfig {
#[serde(default = "default_service_binary")]
pub binary: String,
#[serde(default)]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
#[serde(default = "default_service_host")]
pub host: String,
#[serde(default)]
pub port: u16,
#[serde(default)]
pub extra_args: Vec<String>,
#[serde(default)]
pub auto_start: bool,
#[serde(default = "default_service_start_timeout_secs")]
pub start_timeout_secs: u64,
}
impl Default for ServiceConfig {
fn default() -> Self {
Self {
binary: default_service_binary(),
model: None,
alias: None,
host: default_service_host(),
port: 0,
extra_args: Vec::new(),
auto_start: false,
start_timeout_secs: default_service_start_timeout_secs(),
}
}
}
fn default_service_binary() -> String {
"llama-server".to_string()
}
fn default_service_host() -> String {
"127.0.0.1".to_string()
}
const fn default_service_start_timeout_secs() -> u64 {
120
}
impl ServiceConfig {
pub fn base_url(&self) -> String {
let host = if self.host.contains(':') {
format!("[{}]", self.host)
} else {
self.host.clone()
};
format!("http://{}:{}", host, self.port)
}
pub fn served_name(&self) -> Option<String> {
if let Some(alias) = self.alias.as_deref() {
if !alias.trim().is_empty() {
return Some(alias.to_string());
}
}
self.model.as_deref().map(|path| {
std::path::Path::new(path)
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string())
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct SystemOnePreset {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_env: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NeuralConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_execution_provider")]
pub execution_provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ort_dylib_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ort_version: Option<String>,
#[serde(default = "default_model_dir")]
pub model_dir: String,
#[serde(default = "default_model_name")]
pub model_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SearchConfig {
#[serde(default = "default_search_mode")]
pub search_mode: String,
#[serde(default = "default_neural_weight")]
pub neural_weight: f64,
#[serde(default)]
pub rerank_enabled: bool,
#[serde(default = "default_rerank_top_n")]
pub rerank_top_n: u32,
#[serde(default)]
pub fragment_index_enabled: bool,
#[serde(default = "default_fragment_max_bytes")]
pub fragment_max_bytes: u64,
#[serde(default = "default_fragment_weight")]
pub fragment_weight: f64,
#[serde(default = "default_true")]
pub fragment_orphan_enabled: bool,
#[serde(default = "default_true")]
pub fragment_naive_fallback: bool,
}
pub fn query_type_for_mode(search_mode: &str) -> Option<crate::search::ranking::QueryType> {
match search_mode {
"text" => Some(crate::search::ranking::QueryType::Text),
"neural" => Some(crate::search::ranking::QueryType::Semantic),
_ => None, }
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IndexingConfig {
#[serde(default = "default_batch_size")]
pub batch_size: u64,
#[serde(default = "default_max_files")]
pub max_files: u64,
#[serde(default = "default_legacy_embedding_config")]
pub embeddings: EmbeddingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbeddingConfig {
#[serde(default = "default_enable_hybrid")]
pub enable_hybrid: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_service: Option<String>,
#[serde(default = "default_remote_provider")]
pub remote_provider: String,
#[serde(default)]
pub remote_endpoint: Option<String>,
#[serde(default)]
pub remote_api_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_api_key_env: Option<String>,
#[serde(default)]
pub remote_model: Option<String>,
#[serde(default)]
pub remote_dimension: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_doc_template: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_query_template: Option<String>,
#[serde(default)]
pub onnx_model_path: Option<String>,
}
pub const EMBEDDINGGEMMA2_DEFAULT_ENDPOINT: &str = "http://127.0.0.1:8080/v1";
pub const EMBEDDINGGEMMA2_MODEL: &str = "embeddinggemma-2";
pub const EMBEDDINGGEMMA2_DIMENSION: usize = 768;
pub const EMBEDDINGGEMMA2_DOC_TEMPLATE: &str = "title: none | text: {text}";
pub const EMBEDDINGGEMMA2_QUERY_TEMPLATE: &str = "task: code retrieval | query: {text}";
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedRemote {
pub endpoint: String,
pub model: Option<String>,
pub remote_dimension: Option<usize>,
pub doc_template: Option<String>,
pub query_template: Option<String>,
}
impl EmbeddingConfig {
pub fn resolved_api_key(&self) -> Result<Option<String>, String> {
if let Some(name) = &self.remote_api_key_env {
return std::env::var(name)
.ok()
.filter(|value| !value.trim().is_empty())
.map(Some)
.ok_or_else(|| {
format!("Embedding API key environment variable {name} is unset or empty")
});
}
Ok(self.remote_api_key.clone())
}
pub fn remote_preset_label(&self) -> &str {
if self.embeddinggemma2_preset_active() {
"embeddinggemma-2"
} else {
"custom"
}
}
pub fn embeddinggemma2_preset_active(&self) -> bool {
matches!(
self.remote_provider.trim().to_ascii_lowercase().as_str(),
"embeddinggemma-2" | "embeddinggemma2"
)
}
pub fn resolved_remote(&self) -> Option<ResolvedRemote> {
self.resolved_remote_with(&LeIndexConfig::load_cached().services)
}
pub fn resolved_remote_with(
&self,
services: &std::collections::BTreeMap<String, ServiceConfig>,
) -> Option<ResolvedRemote> {
if !self.enable_hybrid {
return None;
}
let preset = self.embeddinggemma2_preset_active();
let service = self
.remote_service
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.and_then(|name| services.get(name));
let endpoint = match self.remote_endpoint.as_deref().map(str::trim) {
Some(e) if !e.is_empty() => Some(e.to_string()),
_ => service
.map(|s| format!("{}/v1", s.base_url()))
.or_else(|| preset.then(|| EMBEDDINGGEMMA2_DEFAULT_ENDPOINT.to_string())),
}?;
let fill = |value: &Option<String>, preset_value: &str| match value {
Some(v) if !v.trim().is_empty() => Some(v.clone()),
_ if preset => Some(preset_value.to_string()),
_ => None,
};
Some(ResolvedRemote {
endpoint,
model: fill(&self.remote_model, EMBEDDINGGEMMA2_MODEL)
.or_else(|| service.and_then(|s| s.served_name())),
doc_template: fill(&self.remote_doc_template, EMBEDDINGGEMMA2_DOC_TEMPLATE),
query_template: fill(&self.remote_query_template, EMBEDDINGGEMMA2_QUERY_TEMPLATE),
remote_dimension: self
.remote_dimension
.or_else(|| preset.then_some(EMBEDDINGGEMMA2_DIMENSION)),
})
}
pub fn remote_endpoint_configured(&self) -> bool {
self.resolved_remote().is_some()
}
}
impl Default for EmbeddingConfig {
fn default() -> Self {
Self {
enable_hybrid: true,
remote_provider: "embeddinggemma-2".to_string(),
remote_service: None,
remote_endpoint: None,
remote_api_key: None,
remote_api_key_env: None,
remote_model: None,
remote_dimension: None,
remote_doc_template: None,
remote_query_template: None,
onnx_model_path: None,
}
}
}
fn default_legacy_embedding_config() -> EmbeddingConfig {
EmbeddingConfig {
enable_hybrid: default_enable_hybrid(),
remote_provider: default_remote_provider(),
remote_service: None,
remote_endpoint: None,
remote_api_key: None,
remote_api_key_env: None,
remote_model: None,
remote_dimension: None,
remote_doc_template: None,
remote_query_template: None,
onnx_model_path: None,
}
}
const fn default_enable_hybrid() -> bool {
false
}
fn default_remote_provider() -> String {
"custom".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct McpConfig {
#[serde(default = "default_mcp_idle_timeout_secs")]
pub idle_timeout_secs: u64,
#[serde(default = "default_mcp_engine_max_idle_secs")]
pub engine_max_idle_secs: u64,
#[serde(default = "default_mcp_prewarm")]
pub prewarm: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SystemOneConfig {
#[serde(default)]
pub mode: String,
#[serde(default = "default_system_one_provider")]
pub provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_env: Option<String>,
#[serde(default = "default_system_one_timeout_ms")]
pub timeout_ms: u64,
#[serde(default = "default_true")]
pub pre_gate: bool,
#[serde(default = "default_true")]
pub post_gate: bool,
#[serde(default = "default_system_one_relevant_min")]
pub relevant_min: f32,
#[serde(default = "default_system_one_instructions_max")]
pub instructions_max: f32,
#[serde(default = "default_system_one_top_n")]
pub top_n: usize,
#[serde(default = "default_system_one_tau_apply")]
pub tau_apply: f32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_preset: Option<String>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub presets: std::collections::BTreeMap<String, SystemOnePreset>,
}
impl SystemOneConfig {
pub fn effective(&self) -> Self {
let Some(name) = self.active_preset.as_deref().map(str::trim) else {
return self.clone();
};
let Some(preset) = self.presets.get(name) else {
return self.clone();
};
let provider = preset
.provider
.clone()
.unwrap_or_else(|| self.provider.clone());
let provider_identity = |value: &str| match value.trim().to_ascii_lowercase().as_str() {
"typesafe" => "jev".to_string(),
value => value.to_string(),
};
let same_provider = provider_identity(&provider) == provider_identity(&self.provider);
let overlay = |value: &Option<String>, base: &Option<String>| {
value
.clone()
.or_else(|| if same_provider { base.clone() } else { None })
};
Self {
provider,
model: overlay(&preset.model, &self.model),
base_url: overlay(&preset.base_url, &self.base_url),
service: overlay(&preset.service, &self.service),
api_key_env: overlay(&preset.api_key_env, &self.api_key_env),
..self.clone()
}
}
}
impl Default for SystemOneConfig {
fn default() -> Self {
Self {
mode: String::new(),
provider: default_system_one_provider(),
base_url: None,
model: None,
api_key_env: None,
timeout_ms: default_system_one_timeout_ms(),
pre_gate: true,
post_gate: true,
relevant_min: default_system_one_relevant_min(),
instructions_max: default_system_one_instructions_max(),
top_n: default_system_one_top_n(),
tau_apply: default_system_one_tau_apply(),
service: None,
active_preset: None,
presets: std::collections::BTreeMap::new(),
}
}
}
fn default_system_one_provider() -> String {
"laya".to_string()
}
fn default_system_one_timeout_ms() -> u64 {
150
}
fn default_system_one_relevant_min() -> f32 {
0.45
}
fn default_system_one_instructions_max() -> f32 {
0.70
}
fn default_system_one_top_n() -> usize {
10
}
fn default_system_one_tau_apply() -> f32 {
0.80
}
fn default_legacy_neural_config() -> NeuralConfig {
NeuralConfig {
enabled: false,
..NeuralConfig::default()
}
}
impl Default for NeuralConfig {
fn default() -> Self {
Self {
enabled: true,
execution_provider: default_execution_provider(),
ort_dylib_path: None,
ort_version: None,
model_dir: default_model_dir(),
model_name: default_model_name(),
}
}
}
impl Default for SearchConfig {
fn default() -> Self {
Self {
search_mode: default_search_mode(),
neural_weight: default_neural_weight(),
rerank_enabled: false,
rerank_top_n: default_rerank_top_n(),
fragment_index_enabled: false,
fragment_max_bytes: default_fragment_max_bytes(),
fragment_weight: default_fragment_weight(),
fragment_orphan_enabled: default_true(),
fragment_naive_fallback: default_true(),
}
}
}
impl Default for IndexingConfig {
fn default() -> Self {
Self {
batch_size: default_batch_size(),
max_files: default_max_files(),
embeddings: EmbeddingConfig::default(),
}
}
}
fn default_legacy_indexing_config() -> IndexingConfig {
IndexingConfig {
batch_size: default_batch_size(),
max_files: default_max_files(),
embeddings: default_legacy_embedding_config(),
}
}
impl Default for McpConfig {
fn default() -> Self {
Self {
idle_timeout_secs: default_mcp_idle_timeout_secs(),
engine_max_idle_secs: default_mcp_engine_max_idle_secs(),
prewarm: default_mcp_prewarm(),
}
}
}
fn default_execution_provider() -> String {
"auto".to_string()
}
fn default_model_dir() -> String {
resolve_leindex_home()
.map(|h| h.join(DEFAULT_MODEL_DIR_SUFFIX).display().to_string())
.unwrap_or_else(|| format!("~/.leindex/{}", DEFAULT_MODEL_DIR_SUFFIX))
}
fn default_model_name() -> String {
DEFAULT_MODEL_NAME.to_string()
}
fn default_search_mode() -> String {
"hybrid".to_string()
}
fn default_neural_weight() -> f64 {
0.4
}
fn default_fragment_max_bytes() -> u64 {
12_000
}
fn default_fragment_weight() -> f64 {
0.35
}
fn default_true() -> bool {
true
}
fn default_rerank_top_n() -> u32 {
80
}
fn default_batch_size() -> u64 {
500
}
fn default_max_files() -> u64 {
50_000
}
fn default_mcp_idle_timeout_secs() -> u64 {
1800
}
fn default_mcp_prewarm() -> String {
"full".to_string()
}
fn default_mcp_engine_max_idle_secs() -> u64 {
600
}
pub fn resolve_leindex_home() -> Option<PathBuf> {
if let Ok(custom) = std::env::var(LEINDEX_HOME_ENV) {
let p = PathBuf::from(custom);
if p.is_absolute() {
return Some(p);
}
}
dirs::home_dir().map(|h| h.join(".leindex"))
}
pub fn config_file_path() -> Option<PathBuf> {
resolve_leindex_home().map(|h| h.join("config").join("leindex.toml"))
}
pub fn model_dir_path() -> Option<PathBuf> {
resolve_leindex_home().map(|h| h.join(DEFAULT_MODEL_DIR_SUFFIX))
}
impl LeIndexConfig {
pub fn save(&self) -> Result<PathBuf, ConfigError> {
let config_path = config_file_path().ok_or(ConfigError::NoHomeDir)?;
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| ConfigError::Io(config_path.clone(), e.to_string()))?;
}
let toml_str =
toml::to_string_pretty(self).map_err(|e| ConfigError::Serialize(e.to_string()))?;
let parent = config_path.parent().ok_or(ConfigError::NoHomeDir)?;
let mut temporary = tempfile::NamedTempFile::new_in(parent)
.map_err(|e| ConfigError::Io(config_path.clone(), e.to_string()))?;
temporary
.write_all(toml_str.as_bytes())
.and_then(|_| temporary.as_file().sync_all())
.map_err(|e| ConfigError::Io(config_path.clone(), e.to_string()))?;
temporary
.persist(&config_path)
.map_err(|e| ConfigError::Io(config_path.clone(), e.error.to_string()))?;
Ok(config_path)
}
pub fn load() -> Result<Self, ConfigError> {
Self::load_from_path(&config_file_path().ok_or(ConfigError::NoHomeDir)?)
}
pub fn neural_weight_f32(&self) -> f32 {
self.search.neural_weight as f32
}
pub fn load_cached() -> &'static LeIndexConfig {
static CACHED: std::sync::OnceLock<LeIndexConfig> = std::sync::OnceLock::new();
CACHED.get_or_init(|| {
Self::load().unwrap_or_else(|err| {
tracing::warn!(
error = %err,
"failed to load leindex.toml for caching; preserving legacy disabled defaults"
);
Self {
neural: default_legacy_neural_config(),
indexing: default_legacy_indexing_config(),
..Self::default()
}
})
})
}
pub fn load_from_path(path: &Path) -> Result<Self, ConfigError> {
if !path.exists() {
return Ok(Self::default());
}
let contents = std::fs::read_to_string(path)
.map_err(|e| ConfigError::Io(path.to_path_buf(), e.to_string()))?;
Self::parse_toml(&contents).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))
}
fn parse_toml(toml_str: &str) -> Result<Self, String> {
toml::from_str(toml_str).map_err(|e| format!("Failed to parse leindex.toml: {}", e))
}
pub fn load_or_recover() -> Result<(Self, RecoveryAction), ConfigError> {
let config_path = match config_file_path() {
Some(p) => p,
None => return Ok((Self::default(), RecoveryAction::CreatedDefault)),
};
if !config_path.exists() {
return Ok((Self::default(), RecoveryAction::CreatedDefault));
}
let contents = match std::fs::read_to_string(&config_path) {
Ok(c) => c,
Err(e) => {
return Err(ConfigError::Io(
config_path,
format!("Cannot read config file: {}", e),
));
}
};
match Self::parse_toml(&contents) {
Ok(config) => Ok((config, RecoveryAction::Loaded)),
Err(parse_err) => {
let backup_path = config_path.with_extension("toml.bak");
if let Err(e) = std::fs::rename(&config_path, &backup_path) {
return Err(ConfigError::Io(
config_path,
format!(
"config corrupted ({parse_err}); backup rename to {} failed: {e}",
backup_path.display()
),
));
}
tracing::warn!(
"Config corrupted: {}. Backed up to {}",
parse_err,
backup_path.display()
);
Ok((
Self {
neural: default_legacy_neural_config(),
indexing: default_legacy_indexing_config(),
..Self::default()
},
RecoveryAction::RecoveredFromCorrupt(backup_path),
))
}
}
}
}
#[derive(Debug, Clone)]
pub enum RecoveryAction {
Loaded,
CreatedDefault,
RecoveredFromCorrupt(PathBuf),
}
#[derive(Debug, Clone)]
pub enum ConfigError {
NoHomeDir,
Io(PathBuf, String),
Serialize(String),
Parse(PathBuf, String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::NoHomeDir => {
write!(
f,
"Cannot resolve LeIndex home directory. Set LEINDEX_HOME or ensure HOME is set."
)
}
ConfigError::Io(path, msg) => {
write!(f, "I/O error on {}: {}", path.display(), msg)
}
ConfigError::Serialize(msg) => {
write!(f, "Failed to serialize config: {}", msg)
}
ConfigError::Parse(path, msg) => {
write!(f, "Failed to parse {}: {}", path.display(), msg)
}
}
}
}
impl std::error::Error for ConfigError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_round_trip() {
let config = LeIndexConfig::default();
let toml_str = toml::to_string(&config).unwrap();
let parsed: LeIndexConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(config, parsed);
}
#[test]
fn test_fresh_neural_default_and_legacy_toml_boundary() {
let fresh = LeIndexConfig::default();
assert!(fresh.neural.enabled);
assert!(fresh.indexing.embeddings.enable_hybrid);
assert_eq!(
fresh.indexing.embeddings.remote_preset_label(),
"embeddinggemma-2"
);
for existing in [
"",
"[neural]\nexecution_provider = \"cpu\"\n",
"[neural]\nenabled = false\n",
] {
let parsed: LeIndexConfig = toml::from_str(existing).expect("existing TOML");
assert!(!parsed.neural.enabled);
}
let explicit: LeIndexConfig =
toml::from_str("[neural]\nenabled = true\n").expect("opt-in TOML");
assert!(explicit.neural.enabled);
}
#[test]
fn test_neural_config_schema() {
let config = NeuralConfig {
enabled: true,
execution_provider: "cpu".to_string(),
ort_dylib_path: Some("/usr/local/lib/libonnxruntime.so".to_string()),
ort_version: Some("1.25.0".to_string()),
model_dir: "/home/user/.leindex/models".to_string(),
model_name: "qwen3-embed-0.6b-dynamic-uint8".to_string(),
};
let toml_str = toml::to_string(&config).unwrap();
assert!(toml_str.contains("enabled = true"));
assert!(toml_str.contains("execution_provider = \"cpu\""));
assert!(toml_str.contains("ort_dylib_path"));
assert!(toml_str.contains("ort_version"));
assert!(toml_str.contains("model_dir"));
}
#[test]
fn test_parse_malformed_returns_error() {
let bad_toml = "[neural\nenabled = true\n";
let result = LeIndexConfig::parse_toml(bad_toml);
assert!(result.is_err());
}
#[test]
fn test_config_roundtrip_preserves_rerank_fields() {
let mut config = LeIndexConfig::default();
config.search.rerank_enabled = true;
config.search.rerank_top_n = 80;
let decoded: LeIndexConfig = toml::from_str(&toml::to_string(&config).unwrap()).unwrap();
assert!(decoded.search.rerank_enabled);
assert_eq!(decoded.search.rerank_top_n, 80);
}
#[test]
fn test_default_execution_provider_is_auto() {
assert_eq!(LeIndexConfig::default().neural.execution_provider, "auto");
}
#[test]
fn test_default_neural_weight_is_0_4() {
assert_eq!(LeIndexConfig::default().search.neural_weight, 0.4);
}
#[test]
fn test_config_missing_keys_uses_defaults() {
let toml_str = "[neural]\nenabled = true\n";
let config: LeIndexConfig = toml::from_str(toml_str).unwrap();
assert!(config.neural.enabled);
assert_eq!(config.search.search_mode, "hybrid");
assert_eq!(config.search.neural_weight, 0.4);
assert_eq!(config.indexing.batch_size, 500);
assert_eq!(config.neural.model_name, "qwen3-embed-0.6b-dynamic-uint8");
}
#[test]
fn test_config_empty_uses_defaults() {
let config: LeIndexConfig = toml::from_str("").unwrap();
assert!(!config.neural.enabled);
assert_eq!(config.search.search_mode, "hybrid");
assert_eq!(config.search.neural_weight, 0.4);
}
#[test]
fn test_fragment_config_defaults() {
let config = LeIndexConfig::default();
assert!(!config.search.fragment_index_enabled);
assert_eq!(config.search.fragment_max_bytes, 12_000);
assert_eq!(config.search.fragment_weight, 0.35);
assert!(config.search.fragment_orphan_enabled);
assert!(config.search.fragment_naive_fallback);
let parsed: LeIndexConfig = toml::from_str("").unwrap();
assert_eq!(parsed.search, config.search);
assert!(!parsed.indexing.embeddings.enable_hybrid);
assert_eq!(parsed.indexing.embeddings.remote_preset_label(), "custom");
assert!(config.indexing.embeddings.enable_hybrid);
assert_eq!(
config.indexing.embeddings.remote_preset_label(),
"embeddinggemma-2"
);
}
#[test]
fn test_fragment_config_round_trip() {
let mut config = LeIndexConfig::default();
config.search.fragment_index_enabled = true;
config.search.fragment_max_bytes = 24_000;
config.search.fragment_weight = 0.20;
config.search.fragment_orphan_enabled = false;
config.search.fragment_naive_fallback = false;
let decoded: LeIndexConfig = toml::from_str(&toml::to_string(&config).unwrap()).unwrap();
assert_eq!(config, decoded);
}
#[test]
fn test_full_config_round_trip() {
let config = LeIndexConfig {
neural: NeuralConfig {
enabled: true,
execution_provider: "migraphx".to_string(),
ort_dylib_path: Some("/usr/local/lib/libonnxruntime.so.1.25.0".to_string()),
ort_version: Some("1.25.0".to_string()),
model_dir: "/home/user/.leindex/models".to_string(),
model_name: "qwen3-embed-0.6b".to_string(),
},
search: SearchConfig {
search_mode: "hybrid".to_string(),
neural_weight: 0.35,
rerank_enabled: true,
rerank_top_n: 80,
fragment_index_enabled: false,
fragment_max_bytes: 12_000,
fragment_weight: 0.35,
fragment_orphan_enabled: true,
fragment_naive_fallback: true,
},
indexing: IndexingConfig {
batch_size: 1000,
max_files: 100_000,
embeddings: EmbeddingConfig::default(),
},
mcp: McpConfig {
idle_timeout_secs: 3600,
engine_max_idle_secs: 1200,
prewarm: "graph".to_string(),
},
system_one: SystemOneConfig::default(),
services: std::collections::BTreeMap::new(),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
let parsed: LeIndexConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(config, parsed);
}
#[test]
fn test_mcp_config_defaults() {
let config = LeIndexConfig::default();
assert_eq!(config.mcp.idle_timeout_secs, 1800);
assert_eq!(config.mcp.engine_max_idle_secs, 600);
assert_eq!(config.mcp.prewarm, "full");
let parsed: LeIndexConfig = toml::from_str("").unwrap();
assert_eq!(parsed.mcp, McpConfig::default());
let legacy = "[neural]\nenabled = true\n";
let parsed_legacy: LeIndexConfig = toml::from_str(legacy).unwrap();
assert_eq!(parsed_legacy.mcp, McpConfig::default());
}
#[test]
fn test_mcp_config_round_trip() {
let mut config = LeIndexConfig::default();
config.mcp.idle_timeout_secs = 60;
config.mcp.engine_max_idle_secs = 0;
let decoded: LeIndexConfig = toml::from_str(&toml::to_string(&config).unwrap()).unwrap();
assert_eq!(config.mcp, decoded.mcp);
}
#[test]
fn test_ort_dylib_path_skip_serializing_if_none() {
let config = NeuralConfig {
enabled: true,
execution_provider: "cpu".to_string(),
ort_dylib_path: None,
ort_version: None,
model_dir: "/models".to_string(),
model_name: "qwen3-embed-0.6b".to_string(),
};
let toml_str = toml::to_string(&config).unwrap();
assert!(!toml_str.contains("ort_dylib_path"));
}
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn test_load_or_recover_corrupt_file() {
let _g = ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
let config_path = tmp.path().join("config").join("leindex.toml");
std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
std::fs::write(&config_path, "[neural\nbroken toml").unwrap();
unsafe { std::env::set_var(LEINDEX_HOME_ENV, tmp.path()) };
let (config, action) = LeIndexConfig::load_or_recover().unwrap();
assert!(matches!(action, RecoveryAction::RecoveredFromCorrupt(_)));
assert!(!config.neural.enabled);
assert!(config_path.with_extension("toml.bak").exists());
unsafe { std::env::remove_var(LEINDEX_HOME_ENV) };
}
#[test]
fn test_config_load_returns_default_when_missing() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var(LEINDEX_HOME_ENV, "/nonexistent/path/for/testing") };
let (config, action) = LeIndexConfig::load_or_recover().unwrap();
assert!(matches!(action, RecoveryAction::CreatedDefault));
assert!(config.neural.enabled);
unsafe { std::env::remove_var(LEINDEX_HOME_ENV) };
}
#[test]
fn test_resolve_leindex_home_env_override() {
let _g = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var(LEINDEX_HOME_ENV, "/custom/leindex") };
assert_eq!(
resolve_leindex_home(),
Some(PathBuf::from("/custom/leindex"))
);
unsafe { std::env::remove_var(LEINDEX_HOME_ENV) };
}
fn service(
name: &str,
host: &str,
port: u16,
model: Option<&str>,
alias: Option<&str>,
) -> (String, ServiceConfig) {
(
name.to_string(),
ServiceConfig {
host: host.to_string(),
port,
model: model.map(str::to_string),
alias: alias.map(str::to_string),
..ServiceConfig::default()
},
)
}
#[test]
fn test_service_base_url_and_served_name() {
let (_, laya) = service(
"laya",
"127.0.0.1",
8181,
Some("/models/laya-bf16.gguf"),
None,
);
assert_eq!(laya.base_url(), "http://127.0.0.1:8181");
assert_eq!(laya.served_name().as_deref(), Some("laya-bf16"));
let (_, aliased) = service("e", "127.0.0.1", 8080, None, Some("embeddinggemma-2"));
assert_eq!(aliased.served_name().as_deref(), Some("embeddinggemma-2"));
}
#[test]
fn test_resolved_remote_endpoint_ladder() {
let mut services: std::collections::BTreeMap<String, ServiceConfig> =
std::collections::BTreeMap::new();
services.insert(
"embed".to_string(),
ServiceConfig {
port: 8123,
..ServiceConfig::default()
},
);
let via_service = EmbeddingConfig {
enable_hybrid: true,
remote_service: Some("embed".into()),
..EmbeddingConfig::default()
};
let resolved = via_service.resolved_remote_with(&services).unwrap();
assert_eq!(resolved.endpoint, "http://127.0.0.1:8123/v1");
let explicit_wins = EmbeddingConfig {
enable_hybrid: true,
remote_service: Some("embed".into()),
remote_endpoint: Some("http://explicit:9/v1".into()),
..EmbeddingConfig::default()
};
assert_eq!(
explicit_wins
.resolved_remote_with(&services)
.unwrap()
.endpoint,
"http://explicit:9/v1"
);
let preset_only = EmbeddingConfig {
enable_hybrid: true,
..EmbeddingConfig::default()
};
assert_eq!(
preset_only
.resolved_remote_with(&services)
.unwrap()
.endpoint,
EMBEDDINGGEMMA2_DEFAULT_ENDPOINT
);
}
#[test]
fn test_resolved_remote_model_falls_back_to_service() {
let mut services: std::collections::BTreeMap<String, ServiceConfig> =
std::collections::BTreeMap::new();
services.insert(
"embed".to_string(),
ServiceConfig {
model: Some("/models/embeddinggemma-2-Q8_0.gguf".into()),
..ServiceConfig::default()
},
);
let cfg = EmbeddingConfig {
enable_hybrid: true,
remote_service: Some("embed".into()),
remote_provider: "custom".into(),
..EmbeddingConfig::default()
};
assert_eq!(
cfg.resolved_remote_with(&services)
.unwrap()
.model
.as_deref(),
Some("embeddinggemma-2-Q8_0")
);
}
#[test]
fn test_system_one_preset_overlay_swaps_provider() {
let mut cfg = SystemOneConfig {
provider: "laya".into(),
mode: "enforce".into(),
..SystemOneConfig::default()
};
cfg.presets.insert(
"openai-prod".into(),
SystemOnePreset {
provider: Some("openai".into()),
model: Some("gpt-6-luna".into()),
api_key_env: Some("OPENAI_API_KEY".into()),
..SystemOnePreset::default()
},
);
cfg.active_preset = Some("openai-prod".into());
let effective = cfg.effective();
assert_eq!(effective.provider, "openai");
assert_eq!(effective.model.as_deref(), Some("gpt-6-luna"));
assert_eq!(effective.mode, "enforce");
cfg.active_preset = Some("nope".into());
assert_eq!(cfg.effective().provider, "laya");
}
#[test]
fn test_system_one_provider_change_clears_unset_provider_fields() {
let mut cfg = SystemOneConfig {
provider: "laya".into(),
model: Some("local-model".into()),
base_url: Some("http://127.0.0.1:8000".into()),
service: Some("laya".into()),
api_key_env: Some("LOCAL_API_KEY".into()),
mode: "enforce".into(),
active_preset: Some("remote".into()),
..SystemOneConfig::default()
};
cfg.presets.insert(
"remote".into(),
SystemOnePreset {
provider: Some("openai".into()),
..SystemOnePreset::default()
},
);
let effective = cfg.effective();
assert_eq!(effective.provider, "openai");
assert_eq!(effective.model, None);
assert_eq!(effective.base_url, None);
assert_eq!(effective.service, None);
assert_eq!(effective.api_key_env, None);
assert_eq!(effective.mode, "enforce");
let preset = cfg.presets.get_mut("remote").unwrap();
preset.model = Some("remote-model".into());
preset.base_url = Some("https://remote.example".into());
preset.service = Some("remote-service".into());
preset.api_key_env = Some("REMOTE_API_KEY".into());
let effective = cfg.effective();
assert_eq!(effective.model.as_deref(), Some("remote-model"));
assert_eq!(
effective.base_url.as_deref(),
Some("https://remote.example")
);
assert_eq!(effective.service.as_deref(), Some("remote-service"));
assert_eq!(effective.api_key_env.as_deref(), Some("REMOTE_API_KEY"));
}
#[test]
fn test_system_one_same_provider_presets_inherit_unset_fields() {
let mut cfg = SystemOneConfig {
provider: "jev".into(),
model: Some("base-model".into()),
base_url: Some("https://base.example".into()),
service: Some("base-service".into()),
api_key_env: Some("BASE_API_KEY".into()),
active_preset: Some("same".into()),
..SystemOneConfig::default()
};
for provider in [None, Some("jev"), Some(" JEV "), Some("typesafe")] {
cfg.presets.insert(
"same".into(),
SystemOnePreset {
provider: provider.map(str::to_string),
..SystemOnePreset::default()
},
);
let effective = cfg.effective();
assert_eq!(effective.model, cfg.model);
assert_eq!(effective.base_url, cfg.base_url);
assert_eq!(effective.service, cfg.service);
assert_eq!(effective.api_key_env, cfg.api_key_env);
}
}
#[test]
fn test_service_base_url_brackets_ipv6_host() {
let v6 = ServiceConfig {
host: "::1".into(),
port: 8080,
..ServiceConfig::default()
};
assert_eq!(v6.base_url(), "http://[::1]:8080");
let v4 = ServiceConfig {
host: "127.0.0.1".into(),
port: 8080,
..ServiceConfig::default()
};
assert_eq!(v4.base_url(), "http://127.0.0.1:8080");
}
}