use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tempfile::tempdir;
use engram_core::Config;
use engram_core::config;
use engram_core::error::CoreError;
const PROVIDER_ENV_VARS: [&str; 7] = [
"ENGRAM_EMBEDDING_MODEL",
"ENGRAM_LLM_MODEL",
"ENGRAM_TRAINER_BINARY",
"ENGRAM_TRAINER_TIMEOUT",
"ENGRAM_MODELS_PATH",
"ENGRAM_VOYAGE_API_KEY",
"ENGRAM_OPENAI_API_KEY",
];
struct ConfigLoadEnvironment {
home: Option<String>,
current_directory: PathBuf,
project_directory: Option<String>,
provider_overrides: Vec<(&'static str, Option<String>)>,
}
impl ConfigLoadEnvironment {
fn capture() -> Self {
let provider_overrides = PROVIDER_ENV_VARS
.iter()
.map(|name| (*name, std::env::var(name).ok()))
.collect();
Self {
home: std::env::var("HOME").ok(),
current_directory: std::env::current_dir().expect("current dir"),
project_directory: std::env::var("ENGRAM_PROJECT_DIR").ok(),
provider_overrides,
}
}
fn redirect(home: &Path, current_directory: &Path) {
unsafe {
std::env::set_var("HOME", home);
std::env::remove_var("ENGRAM_PROJECT_DIR");
for name in PROVIDER_ENV_VARS {
std::env::remove_var(name);
}
}
std::env::set_current_dir(current_directory).expect("set cwd");
}
}
impl Drop for ConfigLoadEnvironment {
fn drop(&mut self) {
std::env::set_current_dir(&self.current_directory).expect("restore cwd");
unsafe {
match &self.home {
Some(value) => std::env::set_var("HOME", value),
None => std::env::remove_var("HOME"),
}
match &self.project_directory {
Some(value) => std::env::set_var("ENGRAM_PROJECT_DIR", value),
None => std::env::remove_var("ENGRAM_PROJECT_DIR"),
}
for (name, value) in &self.provider_overrides {
match value {
Some(value) => std::env::set_var(name, value),
None => std::env::remove_var(name),
}
}
}
}
}
fn write_global_config(home: &Path, contents: &str) {
let engram_dir = home.join(".engram");
std::fs::create_dir_all(&engram_dir).expect("create global .engram");
std::fs::write(engram_dir.join("engram.toml"), contents).expect("write global config");
}
fn write_project_local_config(project_dir: &Path, contents: &str) {
let engram_dir = project_dir.join(".engram");
std::fs::create_dir_all(&engram_dir).expect("create project .engram");
std::fs::write(engram_dir.join("engram.toml"), contents).expect("write project config");
}
const FULL_GLOBAL_CONFIG: &str = r#"
[database]
[embedding]
provider = "voyage"
api_key = "global-voyage-key"
model = "voyage-4"
dimension = 1024
hyde_threshold = 5
[llm]
provider = "openai"
api_key = "global-openai-key"
model = "gpt-4o-mini"
[server]
reindex_interval_secs = 3600
[hnsw]
max_connections = 16
ef_construction = 200
ef_search = 40
dimension = 1024
[consolidation]
stale_days = 90
min_score = 0.3
"#;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn lock_env() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn resolve_project_dir_finds_engram_in_cwd() {
let _lock = lock_env();
let temp = tempdir().expect("temp dir");
let engram_dir = temp.path().join(".engram");
std::fs::create_dir_all(&engram_dir).expect("create .engram");
let resolved = config::resolve_project_dir(temp.path(), None).expect("should resolve");
assert_eq!(resolved, temp.path());
}
#[test]
fn resolve_project_dir_walks_up() {
let _lock = lock_env();
let temp = tempdir().expect("temp dir");
let engram_dir = temp.path().join(".engram");
std::fs::create_dir_all(&engram_dir).expect("create .engram");
let nested = temp.path().join("a").join("b").join("c");
std::fs::create_dir_all(&nested).expect("create nested");
let resolved = config::resolve_project_dir(&nested, None).expect("should walk up");
assert_eq!(resolved, temp.path());
}
#[test]
fn resolve_project_dir_respects_explicit_override() {
let _lock = lock_env();
let temp = tempdir().expect("temp dir");
let other = tempdir().expect("other");
let engram_dir = other.path().join(".engram");
std::fs::create_dir_all(&engram_dir).expect("create .engram");
let resolved =
config::resolve_project_dir(temp.path(), Some(other.path())).expect("override wins");
assert_eq!(resolved, other.path());
}
#[test]
fn resolve_project_dir_not_found() {
let _lock = lock_env();
let temp = tempdir().expect("temp dir");
let nested = temp.path().join("sub");
std::fs::create_dir_all(&nested).expect("create nested");
let result = config::resolve_project_dir(&nested, None);
let error = result.expect_err("should fail without .engram");
assert!(matches!(error, CoreError::ProjectDirNotFound));
assert!(error.to_string().contains("[6016]"));
}
#[test]
fn resolve_project_dir_respects_env_override() {
let _lock = lock_env();
let env_project = tempdir().expect("env project");
std::fs::create_dir_all(env_project.path().join(".engram")).expect("create .engram");
let other_start = tempdir().expect("other start");
let original = std::env::var("ENGRAM_PROJECT_DIR").ok();
unsafe {
std::env::set_var("ENGRAM_PROJECT_DIR", env_project.path());
}
let result = config::resolve_project_dir(other_start.path(), None);
unsafe {
match &original {
Some(value) => std::env::set_var("ENGRAM_PROJECT_DIR", value),
None => std::env::remove_var("ENGRAM_PROJECT_DIR"),
}
}
let resolved = result.expect("env override should win");
assert_eq!(resolved, env_project.path());
}
#[test]
fn resolve_project_dir_at_filesystem_root() {
let _lock = lock_env();
let result = config::resolve_project_dir(Path::new("/"), None);
let error = result.expect_err("filesystem root lacks .engram/");
assert!(matches!(error, CoreError::ProjectDirNotFound));
}
#[test]
fn resolve_project_dir_with_nonexistent_start() {
let _lock = lock_env();
let nonexistent: PathBuf = PathBuf::from("/this/path/does/not/exist/nowhere");
let result = config::resolve_project_dir(&nonexistent, None);
let error = result.expect_err("nonexistent start has no .engram/ ancestor");
assert!(matches!(error, CoreError::ProjectDirNotFound));
}
#[test]
fn load_uses_global_only_when_no_project_local() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
std::fs::create_dir_all(project.path().join(".engram")).expect("create marker");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("global-only load succeeds");
assert_eq!(config.embedding.hyde_threshold, 5);
assert_eq!(config.embedding.model.as_deref(), Some("voyage-4"));
assert_eq!(
config.embedding.api_key.as_deref(),
Some("global-voyage-key")
);
}
#[test]
fn load_merges_project_local_over_global() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
write_project_local_config(
project.path(),
r#"
[embedding]
model = "voyage-code-3"
hyde_threshold = 25
[hnsw]
ef_search = 80
"#,
);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("layered load succeeds");
assert_eq!(config.embedding.model.as_deref(), Some("voyage-code-3"));
assert_eq!(config.embedding.hyde_threshold, 25);
assert_eq!(config.hnsw.ef_search, 80);
assert_eq!(config.hnsw.max_connections, 16);
assert_eq!(config.embedding.dimension, Some(1024));
}
#[test]
fn load_project_local_partial_inherits_global() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
write_project_local_config(
project.path(),
r#"
[embedding]
hyde_threshold = 12
"#,
);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("partial layered load succeeds");
assert_eq!(config.embedding.hyde_threshold, 12);
assert_eq!(config.embedding.provider, "voyage");
assert_eq!(config.llm.provider, "openai");
assert_eq!(config.server.reindex_interval_secs, 3600);
assert_eq!(config.hnsw.dimension, 1024);
}
#[test]
fn load_project_local_api_key_is_ignored() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
write_project_local_config(
project.path(),
r#"
[embedding]
api_key = "project-local-voyage-key"
[llm]
api_key = "project-local-openai-key"
"#,
);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("layered load succeeds");
assert_eq!(
config.embedding.api_key.as_deref(),
Some("global-voyage-key")
);
assert_eq!(config.llm.api_key.as_deref(), Some("global-openai-key"));
}
#[test]
fn load_project_local_api_key_ignored_when_global_has_none() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(
home.path(),
r#"
[database]
[embedding]
provider = "voyage"
model = "voyage-4"
dimension = 1024
[llm]
provider = "openai"
model = "gpt-4o-mini"
[server]
reindex_interval_secs = 3600
[hnsw]
max_connections = 16
ef_construction = 200
ef_search = 40
dimension = 1024
"#,
);
write_project_local_config(
project.path(),
r#"
[embedding]
api_key = "project-local-voyage-key"
[llm]
api_key = "project-local-openai-key"
"#,
);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("layered load succeeds");
assert_eq!(config.embedding.api_key, None);
assert_eq!(config.llm.api_key, None);
}
#[test]
fn load_returns_default_when_no_config_files() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("default load succeeds");
let default_config = Config::default();
assert_eq!(config.embedding.provider, default_config.embedding.provider);
assert_eq!(
config.embedding.hyde_threshold,
default_config.embedding.hyde_threshold
);
assert_eq!(config.hnsw.dimension, default_config.hnsw.dimension);
assert_eq!(
config.server.reindex_interval_secs,
default_config.server.reindex_interval_secs
);
}
#[test]
fn load_invalid_project_local_returns_parse_error() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
write_project_local_config(project.path(), "this is not valid toml [[[");
ConfigLoadEnvironment::redirect(home.path(), project.path());
let result = Config::load();
assert!(matches!(result, Err(CoreError::ConfigParseError(_))));
}
#[test]
fn load_uses_global_only_when_no_project_marker() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let cwd = tempdir().expect("cwd without marker");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
ConfigLoadEnvironment::redirect(home.path(), cwd.path());
let config = Config::load().expect("global-only load succeeds");
assert_eq!(config.embedding.hyde_threshold, 5);
assert_eq!(
config.embedding.api_key.as_deref(),
Some("global-voyage-key")
);
}
#[test]
fn load_project_only_merges_over_defaults() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_project_local_config(
project.path(),
r#"
[embedding]
hyde_threshold = 7
"#,
);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("project-only load succeeds");
let default_config = Config::default();
assert_eq!(config.embedding.hyde_threshold, 7);
assert_eq!(config.embedding.provider, default_config.embedding.provider);
assert_eq!(config.embedding.model, default_config.embedding.model);
assert_eq!(config.embedding.api_key, None);
assert_eq!(config.hnsw.dimension, default_config.hnsw.dimension);
assert_eq!(
config.server.reindex_interval_secs,
default_config.server.reindex_interval_secs
);
}
#[test]
fn load_global_secret_survives_partial_project_section() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
write_project_local_config(
project.path(),
r#"
[embedding]
model = "voyage-code-3"
[llm]
model = "gpt-4o"
"#,
);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("layered load succeeds");
assert_eq!(config.embedding.model.as_deref(), Some("voyage-code-3"));
assert_eq!(config.llm.model.as_deref(), Some("gpt-4o"));
assert_eq!(
config.embedding.api_key.as_deref(),
Some("global-voyage-key")
);
assert_eq!(config.llm.api_key.as_deref(), Some("global-openai-key"));
}
#[test]
fn load_empty_project_local_inherits_global_fully() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
write_project_local_config(
project.path(),
"# engram project-local config\n# all settings inherited from global\n",
);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("empty project-local load succeeds");
assert_eq!(config.embedding.provider, "voyage");
assert_eq!(config.embedding.hyde_threshold, 5);
assert_eq!(config.hnsw.dimension, 1024);
assert_eq!(
config.embedding.api_key.as_deref(),
Some("global-voyage-key")
);
}
#[test]
fn load_invalid_global_returns_parse_error() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(home.path(), "not toml [[[");
ConfigLoadEnvironment::redirect(home.path(), project.path());
let result = Config::load();
assert!(matches!(result, Err(CoreError::ConfigParseError(_))));
}
#[test]
fn load_deduplication_threshold_round_trips() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(
home.path(),
&format!("{FULL_GLOBAL_CONFIG}\n[deduplication]\nthreshold = 0.88\n"),
);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("config with [deduplication] loads");
assert_eq!(config.deduplication.threshold, 0.88);
}
#[test]
fn load_without_deduplication_section_uses_default() {
let _lock = lock_env();
let _environment = ConfigLoadEnvironment::capture();
let home = tempdir().expect("home");
let project = tempdir().expect("project");
write_global_config(home.path(), FULL_GLOBAL_CONFIG);
ConfigLoadEnvironment::redirect(home.path(), project.path());
let config = Config::load().expect("config without [deduplication] still loads");
assert_eq!(config.deduplication.threshold, 0.95);
}