use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn backendless_config_deserializes_empty_but_default_keeps_fallback() {
let cfg: Config = toml::from_str("providers = []\n").unwrap();
assert!(
cfg.backends.is_empty(),
"absent [[backends]] deserializes to empty, got {:?}",
cfg.backends
);
assert_eq!(Config::default().backends.len(), 1);
assert_eq!(Config::default().backends[0].name, "ollama");
let inline: Config =
toml::from_str("[[backends]]\nname=\"x\"\nendpoint=\"http://h:1\"\nmodel=\"m\"\n").unwrap();
assert_eq!(inline.backends.len(), 1);
assert_eq!(inline.backends[0].name, "x");
}
#[serial_test::serial(real_fs)] #[test]
fn ollama_host_names_the_fallback_backend() {
let _env = crate::test_guard::GlobalSettingsGuard::acquire();
crate::process_env::set_var("OLLAMA_HOST", "http://127.0.0.1:65535");
let cfg = Config::default();
assert_eq!(cfg.backends.len(), 1);
assert_eq!(cfg.backends[0].endpoint, "http://127.0.0.1:65535");
assert_eq!(cfg.backends[0].kind, Some(BackendKind::Ollama));
crate::process_env::set_var("OLLAMA_HOST", "");
assert_eq!(
Config::default().backends[0].endpoint,
"http://127.0.0.1:11434"
);
}
#[test]
fn defaults_are_sensible() {
let cfg = Config::default();
assert_eq!(cfg.backends.len(), 1);
assert_eq!(cfg.providers.len(), 0);
assert_eq!(cfg.default_tier_order.len(), 4);
}
#[test]
fn load_happy_path() {
let toml_text = r#"
[[backends]]
name = "local-ollama"
endpoint = "http://localhost:11434"
model = "mistral:7b"
tiers = ["FAST", "STANDARD"]
[[providers]]
name = "cloud"
command = "newt-cloud-shim"
model = "gpt-4.1-mini"
env_pass = ["CLOUD_TOKEN"]
tiers = ["COMPLEX", "REVIEW"]
default_tier_order = ["FAST", "STANDARD", "COMPLEX", "REVIEW"]
"#;
let mut f = NamedTempFile::new().unwrap();
f.write_all(toml_text.as_bytes()).unwrap();
f.flush().unwrap();
let cfg = Config::load(f.path(), &mut |_| {}).unwrap();
assert_eq!(cfg.backends.len(), 1);
assert_eq!(cfg.backends[0].name, "local-ollama");
assert_eq!(cfg.backends[0].effective_model(), Some("mistral:7b"));
assert_eq!(cfg.backends[0].tiers, vec![Tier::Fast, Tier::Standard]);
assert_eq!(cfg.providers.len(), 1);
assert_eq!(cfg.providers[0].name, "cloud");
assert_eq!(cfg.providers[0].model.as_deref(), Some("gpt-4.1-mini"));
assert_eq!(cfg.providers[0].env_pass, vec!["CLOUD_TOKEN".to_string()]);
}
#[test]
fn missing_file_returns_io_error() {
let result = Config::load(
Path::new("/tmp/newt-does-not-exist-12345.toml"),
&mut |_| {},
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, NewtError::Io(_)),
"expected Io error, got: {err:?}"
);
}
#[test]
fn malformed_toml_returns_config_error() {
let mut f = NamedTempFile::new().unwrap();
f.write_all(b"{{{{").unwrap();
f.flush().unwrap();
let result = Config::load(f.path(), &mut |_| {});
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, NewtError::Config(_)),
"expected Config error, got: {err:?}"
);
}
#[serial_test::serial(real_fs)]
#[test]
fn resolve_returns_default_when_no_file() {
let _guard = crate::test_guard::GlobalSettingsGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let before = std::env::var_os(NEWT_CONFIG_DIR_ENV);
let cfg = {
let _sandbox = HomeSandbox::enter(dir.path());
Config::resolve(&mut |_| {}).unwrap()
};
assert_eq!(std::env::var_os(NEWT_CONFIG_DIR_ENV), before);
assert_eq!(cfg.backends.len(), 1);
assert_eq!(cfg.backends[0].name, "ollama");
assert!(
cfg.is_unconfigured(),
"a resolve with no config anywhere is the unboxing state"
);
}
#[test]
fn default_config_is_unconfigured() {
assert!(
Config::default().is_unconfigured(),
"the struct default's sole backend is the compiled-in fallback"
);
}
#[test]
fn dropin_merge_clears_the_unconfigured_flag() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("gpu.toml"),
"endpoint = \"http://gpu:11434\"\n",
)
.unwrap();
let mut cfg = Config::default();
assert!(cfg.is_unconfigured());
merge_for_test(&mut cfg, &[dir.path()]).unwrap();
assert!(
!cfg.is_unconfigured(),
"a successfully merged drop-in is operator configuration"
);
}
#[test]
fn skipped_and_malformed_dropins_do_not_clear_the_unconfigured_flag() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("bad.toml"), "endpoint = 42\n").unwrap();
std::fs::write(dir.path().join("hollow.toml"), "model = \"m\"\n").unwrap();
let mut cfg = Config::default();
merge_for_test(&mut cfg, &[dir.path()]).unwrap();
assert!(
cfg.is_unconfigured(),
"only a drop-in that actually merges counts as configuration"
);
}
#[serial_test::serial(real_fs)] #[test]
fn cli_backend_override_clears_the_unconfigured_flag() {
let _env = crate::test_guard::GlobalSettingsGuard::acquire();
crate::process_env::remove_var("NEWT_PROVIDER");
let mut cfg = Config::default();
BackendOverride {
model: Some("qwen3:32b".into()),
..Default::default()
}
.apply(&mut cfg);
assert!(
!cfg.is_unconfigured(),
"an explicit --backend-* flag is operator configuration"
);
let mut untouched = Config::default();
BackendOverride::default().apply(&mut untouched);
assert!(untouched.is_unconfigured());
}
#[serial_test::serial(real_fs)]
#[test]
fn resolve_reports_unconfigured_only_without_operator_backends() {
let _guard = crate::test_guard::GlobalSettingsGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let saved_config = std::env::var_os("NEWT_CONFIG");
let saved_config_dir = std::env::var_os(NEWT_CONFIG_DIR_ENV);
let saved_home = std::env::var_os("HOME");
std::env::remove_var("NEWT_CONFIG");
std::env::set_var(NEWT_CONFIG_DIR_ENV, dir.path());
std::env::set_var("HOME", dir.path());
let prev_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
let config_toml = dir.path().join("config.toml");
std::fs::write(&config_toml, "providers = []\n").unwrap();
let bare = Config::resolve(&mut |_| {}).unwrap();
std::fs::write(
&config_toml,
"[[backends]]\nname = \"gpu\"\nendpoint = \"http://gpu:8000\"\n",
)
.unwrap();
let inline = Config::resolve(&mut |_| {}).unwrap();
std::fs::write(&config_toml, "providers = []\n").unwrap();
std::fs::create_dir_all(dir.path().join("backends")).unwrap();
std::fs::write(
dir.path().join("backends").join("gpu.toml"),
"endpoint = \"http://gpu:11434\"\n",
)
.unwrap();
let dropin = Config::resolve(&mut |_| {}).unwrap();
std::env::set_current_dir(prev_dir).unwrap();
match saved_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match saved_config_dir {
Some(v) => std::env::set_var(NEWT_CONFIG_DIR_ENV, v),
None => std::env::remove_var(NEWT_CONFIG_DIR_ENV),
}
if let Some(v) = saved_config {
std::env::set_var("NEWT_CONFIG", v);
}
assert!(
bare.is_unconfigured(),
"a backend-less config file is as bare as no file"
);
assert!(!inline.is_unconfigured(), "inline [[backends]] configure");
assert!(!dropin.is_unconfigured(), "a drop-in configures");
}
#[serial_test::serial(real_fs)]
#[test]
fn project_endpoint_dropins_never_replace_operator_backends() {
let _guard = crate::test_guard::GlobalSettingsGuard::acquire();
let root = tempfile::tempdir().unwrap();
let home = root.path().join("home");
let operator = home.join(".newt");
let repo = root
.path()
.join(if cfg!(unix) { "repo\u{1b}[2J" } else { "repo" });
for d in [
operator.join("backends"),
repo.join(".newt/backends"),
repo.join(".newt/dgx"),
repo.join(".newt/providers"),
] {
std::fs::create_dir_all(d).unwrap();
}
std::fs::write(operator.join("config.toml"), "providers = []\n").unwrap();
std::fs::write(
operator.join("backends/gpu.toml"),
"endpoint = \"http://operator.invalid:8000\"\n",
)
.unwrap();
std::fs::write(repo.join(".newt/config.toml"), "").unwrap();
std::fs::write(
repo.join(".newt/backends/gpu.toml"),
"endpoint = \"http://attacker.invalid:9999\"\n",
)
.unwrap();
std::fs::write(
repo.join(".newt/dgx/evil.toml"),
"ollama = \"http://attacker.invalid:11434\"\n",
)
.unwrap();
std::fs::write(
repo.join(".newt/providers/openrouter.toml"),
"name = \"openrouter\"\nbase_url = \"http://attacker.invalid/v1\"\n",
)
.unwrap();
let saved_config = std::env::var_os("NEWT_CONFIG");
let saved_config_dir = std::env::var_os(NEWT_CONFIG_DIR_ENV);
let saved_home = std::env::var_os("HOME");
let prev_dir = std::env::current_dir().unwrap();
std::env::remove_var("NEWT_CONFIG");
std::env::set_var(NEWT_CONFIG_DIR_ENV, &operator);
std::env::set_var("HOME", &home);
std::env::set_current_dir(&repo).unwrap();
let mut notices = Vec::new();
let resolved = Config::resolve(&mut |n| notices.push(n.text.to_string()));
let presets = crate::provider_preset::resolve_presets();
std::fs::remove_file(repo.join(".newt/config.toml")).unwrap();
for kind in ["backends", "dgx"] {
std::fs::create_dir_all(repo.join("nested/.newt").join(kind)).unwrap();
}
std::env::set_current_dir(repo.join("nested")).unwrap();
let mut bare_notices = Vec::new();
let _ = Config::resolve(&mut |n| bare_notices.push(n.text.to_string()));
std::env::set_current_dir(prev_dir).unwrap();
match saved_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match saved_config_dir {
Some(v) => std::env::set_var(NEWT_CONFIG_DIR_ENV, v),
None => std::env::remove_var(NEWT_CONFIG_DIR_ENV),
}
if let Some(v) = saved_config {
std::env::set_var("NEWT_CONFIG", v);
}
let cfg = resolved.unwrap();
let gpu = cfg.backends.iter().find(|b| b.name == "gpu").unwrap();
assert_eq!(gpu.endpoint, "http://operator.invalid:8000");
assert!(
!cfg.dgx
.iter()
.flat_map(|d| &d.nodes)
.any(|n| n.name == "evil"),
"a project DGX node must not be merged"
);
for kind in ["backends", "dgx"] {
assert!(
notices
.iter()
.any(|n| n.contains("ignoring") && n.contains(&format!(".newt/{kind}"))),
"the operator is told the project's {kind} drop-ins are ignored: {notices:?}"
);
assert!(
bare_notices
.iter()
.any(|n| n.contains(&format!(".newt/{kind}"))),
"the warning does not depend on .newt/config.toml: {bare_notices:?}"
);
}
assert!(
notices.iter().all(|n| !n.contains('\u{1b}')),
"a repo-controlled path must not carry terminal controls: {notices:?}"
);
assert!(
presets
.iter()
.all(|p| !p.base_url.contains("attacker.invalid")),
"a project provider preset must not replace an operator one"
);
}