use std::{fs, path::Path, process::Command};
use crate::config::ConfigSource;
use crate::test_support::CapturedLogs;
use super::{CliOverrides, HomeSource, ServerConfig, aion_home};
const HOME_PROBE: &str = "AION_HOME_TEST_PROBE";
const HOME_EXPECTED: &str = "AION_HOME_TEST_EXPECTED";
fn write(path: &Path, contents: &str) -> Result<(), Box<dyn std::error::Error>> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, contents)?;
Ok(())
}
#[test]
fn discovery_precedence_covers_explicit_local_home_and_defaults()
-> Result<(), Box<dyn std::error::Error>> {
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("home");
let working_dir = scratch.path().join("project");
fs::create_dir_all(&working_dir)?;
let home_config = home.join("config.toml");
let local_config = working_dir.join("aion.toml");
let explicit_config = scratch.path().join("explicit.toml");
write(&home_config, "[namespaces]\ndefault = \"home\"\n")?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
assert_eq!(loaded.config.namespaces.default, "home");
assert_eq!(
loaded.resolution.source,
ConfigSource::AionHome(home_config)
);
write(&local_config, "[namespaces]\ndefault = \"local\"\n")?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
assert_eq!(loaded.config.namespaces.default, "local");
assert_eq!(
loaded.resolution.source,
ConfigSource::ProjectLocal(local_config.clone())
);
write(&explicit_config, "[namespaces]\ndefault = \"explicit\"\n")?;
let cli = CliOverrides {
config_path: Some(explicit_config.clone()),
..CliOverrides::default()
};
let loaded = ServerConfig::load_for_test(&cli, &home, HomeSource::Derived, &working_dir)?;
assert_eq!(loaded.config.namespaces.default, "explicit");
assert_eq!(
loaded.resolution.source,
ConfigSource::Explicit(explicit_config)
);
fs::remove_file(local_config)?;
fs::remove_file(home.join("config.toml"))?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
assert_eq!(loaded.config.namespaces.default, "default");
assert_eq!(loaded.resolution.source, ConfigSource::BuiltInDefaults);
Ok(())
}
#[test]
fn malformed_home_config_is_a_loud_typed_failure() -> Result<(), Box<dyn std::error::Error>> {
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("home");
let working_dir = scratch.path().join("project");
fs::create_dir_all(&working_dir)?;
let config_path = home.join("config.toml");
write(&config_path, "[store\nbackend = nope")?;
let error = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)
.err()
.ok_or("malformed home config unexpectedly loaded")?;
let message = error.to_string();
assert!(message.contains("failed to parse Aion home file"));
assert!(message.contains(&config_path.display().to_string()));
Ok(())
}
#[test]
fn unconfigured_paths_resolve_under_home_without_eager_creation()
-> Result<(), Box<dyn std::error::Error>> {
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("not-created-yet");
let working_dir = scratch.path().join("project");
fs::create_dir_all(&working_dir)?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
assert_eq!(
loaded.config.store.data_dir.as_deref(),
home.join("data").to_str()
);
assert_eq!(
loaded.config.authoring.workspace_dir.as_deref(),
Some(home.join("authoring").as_path())
);
assert!(
!home.exists(),
"config reads must not eagerly create Aion home"
);
Ok(())
}
#[test]
fn a_derived_home_adopts_legacy_directories_and_guards_only_unconfigured_defaults()
-> Result<(), Box<dyn std::error::Error>> {
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("home");
let working_dir = scratch.path().join("project");
let legacy_data = working_dir.join("aion-data");
let legacy_authoring = working_dir.join("aion-authoring");
fs::create_dir_all(&legacy_data)?;
fs::create_dir_all(&legacy_authoring)?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
assert_eq!(
loaded.config.store.data_dir.as_deref(),
legacy_data.to_str()
);
assert_eq!(
loaded.config.authoring.workspace_dir.as_deref(),
Some(legacy_authoring.as_path())
);
assert_eq!(loaded.resolution.legacy_notices.len(), 2);
for notice in &loaded.resolution.legacy_notices {
let line = notice.to_string();
assert!(line.contains("AION HOME MIGRATION REQUIRED"));
assert!(line.contains("stop the server, move"));
assert!(line.contains(&home.display().to_string()));
assert!(line.contains(&working_dir.display().to_string()));
}
let (captured, ()) = CapturedLogs::capture(|| loaded.resolution.log_startup());
let logs = captured.text()?;
assert!(logs.contains("Aion home legacy-directory migration guard active"));
assert!(logs.contains("AION HOME MIGRATION REQUIRED"));
assert!(logs.contains(&legacy_data.display().to_string()));
assert!(logs.contains(&home.join("data").display().to_string()));
assert!(logs.contains("aion-server configuration resolved"));
assert!(logs.contains("built-in defaults"));
assert!(logs.contains("aion-server data root resolved"));
assert!(logs.contains("aion-server authoring root resolved"));
write(
&working_dir.join("aion.toml"),
"[store]\ndata_dir = \"configured-data\"\n\n[authoring]\nworkspace_dir = \"configured-authoring\"\n",
)?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
assert_eq!(
loaded.config.store.data_dir.as_deref(),
Some("configured-data")
);
assert_eq!(
loaded.config.authoring.workspace_dir.as_deref(),
Some(Path::new("configured-authoring"))
);
assert!(loaded.resolution.legacy_notices.is_empty());
Ok(())
}
#[cfg(unix)]
#[test]
fn legacy_symlink_directories_are_never_selected() -> Result<(), Box<dyn std::error::Error>> {
use std::os::unix::fs::symlink;
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("home");
let working_dir = scratch.path().join("project");
let outside = scratch.path().join("outside");
fs::create_dir(&working_dir)?;
fs::create_dir(&outside)?;
symlink(&outside, working_dir.join("aion-data"))?;
symlink(&outside, working_dir.join("aion-authoring"))?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
assert_eq!(
loaded.config.store.data_dir.as_deref(),
home.join("data").to_str()
);
assert_eq!(
loaded.config.authoring.workspace_dir.as_deref(),
Some(home.join("authoring").as_path())
);
assert!(loaded.resolution.legacy_notices.is_empty());
Ok(())
}
#[test]
fn an_explicit_home_is_never_overridden_by_a_legacy_working_directory()
-> Result<(), Box<dyn std::error::Error>> {
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("isolated-home");
let working_dir = scratch.path().join("project");
let legacy_data = working_dir.join("aion-data");
let legacy_authoring = working_dir.join("aion-authoring");
fs::create_dir_all(&legacy_data)?;
fs::create_dir_all(&legacy_authoring)?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Explicit,
&working_dir,
)?;
assert_eq!(
loaded.config.store.data_dir.as_deref(),
home.join("data").to_str(),
"an explicitly named home lost the data root to a legacy working directory"
);
assert_eq!(
loaded.config.authoring.workspace_dir.as_deref(),
Some(home.join("authoring").as_path()),
"an explicitly named home lost the authoring root to a legacy working directory"
);
assert_eq!(loaded.resolution.legacy_notices.len(), 2);
for notice in &loaded.resolution.legacy_notices {
let line = notice.to_string();
assert!(
line.contains("AION HOME IS SET, SO THE LEGACY DIRECTORY WAS IGNORED"),
"an ignored legacy directory was reported as a migration chore: {line}"
);
assert!(!line.contains("MIGRATION REQUIRED"));
assert!(line.contains(&working_dir.display().to_string()));
assert!(line.contains(&home.display().to_string()));
}
let (captured, ()) = CapturedLogs::capture(|| loaded.resolution.log_startup());
let logs = captured.text()?;
assert!(logs.contains("Aion home legacy directory ignored because AION_HOME is set"));
assert!(!logs.contains("migration guard active"));
assert!(logs.contains(&home.join("data").display().to_string()));
Ok(())
}
#[test]
fn the_grpc_bind_override_reaches_the_server_config() -> Result<(), Box<dyn std::error::Error>> {
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("home");
let working_dir = scratch.path().join("project");
fs::create_dir_all(&working_dir)?;
let baseline = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
let cli = CliOverrides {
grpc_address: Some("127.0.0.1:18051".parse()?),
..CliOverrides::default()
};
let overridden = ServerConfig::load_for_test(&cli, &home, HomeSource::Derived, &working_dir)?;
assert_ne!(
baseline.config.server.grpc_address, overridden.config.server.grpc_address,
"the override changed nothing; comparing against the default would have been vacuous"
);
assert_eq!(
overridden.config.server.grpc_address.port(),
18051,
"--grpc-address parsed and converted but never reached the bind"
);
Ok(())
}
#[test]
fn an_explicit_home_with_no_legacy_directory_reports_nothing()
-> Result<(), Box<dyn std::error::Error>> {
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("isolated-home");
let working_dir = scratch.path().join("project");
fs::create_dir_all(&working_dir)?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Explicit,
&working_dir,
)?;
assert_eq!(
loaded.config.store.data_dir.as_deref(),
home.join("data").to_str()
);
assert!(loaded.resolution.legacy_notices.is_empty());
Ok(())
}
#[test]
fn aion_home_environment_override_is_respected_everywhere() -> Result<(), Box<dyn std::error::Error>>
{
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("overridden-home");
let working_dir = scratch.path().join("project");
fs::create_dir_all(&working_dir)?;
fs::create_dir_all(working_dir.join("aion-data"))?;
fs::create_dir_all(working_dir.join("aion-authoring"))?;
write(
&home.join("config.toml"),
"[namespaces]\ndefault = \"from-home-config\"\n",
)?;
let executable = std::env::current_exe()?;
let status = Command::new(executable)
.arg("--exact")
.arg("config::load::home_tests::aion_home_env_probe_child")
.arg("--nocapture")
.current_dir(&working_dir)
.env("AION_HOME", &home)
.env(HOME_PROBE, "1")
.env(HOME_EXPECTED, &home)
.env_remove("AION_STORE_DATA_DIR")
.env_remove("AION_AUTHORING_WORKSPACE_DIR")
.status()?;
assert!(status.success(), "AION_HOME probe child failed");
Ok(())
}
#[test]
fn missing_home_is_a_typed_error() -> Result<(), Box<dyn std::error::Error>> {
let executable = std::env::current_exe()?;
let status = Command::new(executable)
.arg("--exact")
.arg("config::load::home_tests::aion_home_env_probe_child")
.arg("--nocapture")
.env_remove("AION_HOME")
.env_remove("HOME")
.env(HOME_PROBE, "missing")
.status()?;
assert!(status.success(), "missing-home probe child failed");
Ok(())
}
#[test]
fn aion_home_env_probe_child() -> Result<(), Box<dyn std::error::Error>> {
let Some(mode) = std::env::var_os(HOME_PROBE) else {
return Ok(());
};
if mode == "missing" {
let error = aion_home()
.err()
.ok_or("missing AION_HOME and HOME unexpectedly resolved")?;
assert!(error.to_string().contains("set AION_HOME or HOME"));
return Ok(());
}
let expected = std::env::var_os(HOME_EXPECTED).ok_or("probe omitted expected home")?;
let expected = std::path::PathBuf::from(expected);
let resolved = aion_home()?;
assert_eq!(resolved.path, expected);
assert_eq!(resolved.source, HomeSource::Explicit);
let config = ServerConfig::load(&CliOverrides::default())?;
assert_eq!(config.namespaces.default, "from-home-config");
assert_eq!(
config.store.data_dir.as_deref(),
expected.join("data").to_str()
);
assert_eq!(
config.authoring.workspace_dir.as_deref(),
Some(expected.join("authoring").as_path())
);
Ok(())
}
#[test]
fn operator_path_values_expand_a_leading_tilde() -> Result<(), Box<dyn std::error::Error>> {
let Some(real_home) = std::env::var_os("HOME").filter(|value| !value.is_empty()) else {
tracing::info!("HOME is not set; skipping tilde-value expansion test");
return Ok(());
};
let scratch = crate::test_support::private_tempdir()?;
let home = scratch.path().join("home");
let working_dir = scratch.path().join("project");
fs::create_dir_all(&working_dir)?;
write(
&home.join("config.toml"),
"[store]\nbackend = \"haematite\"\ndata_dir = \"~/relocated-store\"\n\
[authoring]\nworkspace_dir = \"~/relocated-workspace\"\n",
)?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
let expected_data = Path::new(&real_home).join("relocated-store");
assert_eq!(
loaded.config.store.data_dir.as_deref(),
expected_data.to_str(),
"store.data_dir must expand `~/` against $HOME"
);
assert_eq!(
loaded.config.authoring.workspace_dir.as_deref(),
Some(Path::new(&real_home).join("relocated-workspace").as_path()),
"authoring.workspace_dir must expand `~/` against $HOME"
);
write(
&home.join("config.toml"),
"[store]\nbackend = \"haematite\"\ndata_dir = \"~someone/store\"\n",
)?;
let Err(error) = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
) else {
return Err("~user in store.data_dir must refuse".into());
};
assert!(
error.to_string().contains("~user"),
"the refusal must name the unsupported `~user` form, got: {error}"
);
write(
&home.join("config.toml"),
"[store]\nbackend = \"haematite\"\ndata_dir = \"plain-data\"\n",
)?;
let loaded = ServerConfig::load_for_test(
&CliOverrides::default(),
&home,
HomeSource::Derived,
&working_dir,
)?;
assert_eq!(
loaded.config.store.data_dir.as_deref(),
Some("plain-data"),
"a tilde-free data_dir must pass through byte-for-byte"
);
Ok(())
}