use super::support::write_config;
use crate::config::*;
#[test]
fn a_config_with_no_llm_provider_is_rejected() {
let temp = tempfile::tempdir().expect("tempdir");
for body in ["", "# only a comment\n"] {
let path = write_config(&temp, body);
let err = load(&path).expect_err("a provider-less config must fail");
match err {
ConfigError::NoProviders(reported) => assert_eq!(reported, path),
other => panic!("expected NoProviders, got {other:?}"),
}
}
}
#[test]
fn the_single_table_llm_shape_is_a_parse_error() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(&temp, "[llm]\nmodel = \"x\"\n");
let err = load(&path).expect_err("`[llm]` is not `[[llm]]`");
assert!(
matches!(err, ConfigError::Parse(_, _)),
"expected a Parse error for the single-table shape, got {err:?}"
);
}
#[test]
fn providers_are_kept_in_file_order_and_primary_is_the_first() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
r#"
[[llm]]
model = "first"
endpoint = "http://a/v1"
[[llm]]
model = "second"
endpoint = "http://b/v1"
[[llm]]
model = "third"
endpoint = "http://c/v1"
"#,
);
let config = load(&path).expect("load");
let models: Vec<&str> = config
.llm
.iter()
.map(|p| p.model.as_deref().expect("model"))
.collect();
assert_eq!(models, vec!["first", "second", "third"]);
assert_eq!(
config.providers().first().and_then(|p| p.model.as_deref()),
Some("first"),
"the chain leads with the head of the list, not an arbitrary entry"
);
}
#[test]
fn temperature_error_carries_the_offending_provider_index() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
r#"
[[llm]]
model = "fine"
temperature = 0.2
[[llm]]
model = "too-hot"
temperature = 2.5
"#,
);
let err = load(&path).expect_err("out-of-range temperature must fail");
match err {
ConfigError::Temperature { index, temperature } => {
assert_eq!(index, 1, "the second provider is the offender");
assert!((temperature - 2.5).abs() < f32::EPSILON);
let rendered = ConfigError::Temperature { index, temperature }.to_string();
assert!(rendered.contains("#2 in file order"), "got {rendered:?}");
}
other => panic!("expected Temperature, got {other:?}"),
}
}
#[test]
fn providers_skips_disabled_entries_and_keeps_file_order() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
r#"
[[llm]]
model = "first"
[[llm]]
enabled = false
model = "parked"
[[llm]]
model = "third"
"#,
);
let config = load(&path).expect("load");
let models: Vec<&str> = config
.providers()
.iter()
.map(|p| p.model.as_deref().expect("model"))
.collect();
assert_eq!(
models,
vec!["first", "third"],
"a disabled entry is skipped, and the survivors keep their order"
);
}
#[test]
fn a_disabled_head_falls_through_to_the_next_enabled_entry() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
r#"
[[llm]]
enabled = false
model = "local"
[[llm]]
model = "cloud"
"#,
);
let config = load(&path).expect("load");
assert_eq!(
config.providers().first().and_then(|p| p.model.as_deref()),
Some("cloud"),
"the chain leads with the first ENABLED entry, not the first entry"
);
}
#[test]
fn a_config_whose_every_provider_is_disabled_is_rejected() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
r#"
[[llm]]
enabled = false
model = "a"
[[llm]]
enabled = false
model = "b"
"#,
);
let err = load(&path).expect_err("an all-disabled config must fail");
match err {
ConfigError::NoEnabledProviders(reported) => assert_eq!(reported, path),
other => panic!("expected NoEnabledProviders, got {other:?}"),
}
}
#[test]
fn an_entry_without_an_enabled_key_participates() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(&temp, "[[llm]]\nmodel = \"x\"\n");
let config = load(&path).expect("load");
assert!(config.llm[0].enabled, "`enabled` defaults to true");
assert_eq!(config.providers().len(), 1);
}
#[test]
fn providers_of_an_empty_config_is_empty() {
assert!(Config::default().providers().is_empty());
}
#[test]
fn a_disabled_provider_with_an_unset_env_var_still_loads() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
r#"
[[llm]]
model = "local"
endpoint = "http://localhost:1234/v1"
[[llm]]
enabled = false
model = "cloud"
endpoint = "https://api.example/v1"
api_key = "${DREP_TEST_VAR_THAT_IS_NOT_SET}"
"#,
);
let config = load(&path).expect("a parked provider's env var is not required");
assert_eq!(config.providers().len(), 1);
assert_eq!(config.providers()[0].model.as_deref(), Some("local"));
}
#[test]
fn an_enabled_provider_with_an_unset_env_var_is_still_rejected() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
r#"
[[llm]]
model = "cloud"
endpoint = "https://api.example/v1"
api_key = "${DREP_TEST_VAR_THAT_IS_NOT_SET}"
"#,
);
let err = load(&path).expect_err("an enabled provider needs its variable");
match err {
ConfigError::EnvVarUnset(name, _) => {
assert_eq!(name, "DREP_TEST_VAR_THAT_IS_NOT_SET");
}
other => panic!("expected EnvVarUnset, got {other:?}"),
}
}
#[test]
fn a_disabled_provider_is_not_validated() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
r#"
[[llm]]
model = "live"
[[llm]]
enabled = false
model = "parked"
temperature = 9.0
max_concurrent = 0
"#,
);
load(&path).expect("a parked provider's fields are not validated");
}
#[test]
fn zero_max_concurrent_is_rejected() {
let temp = tempfile::tempdir().expect("tempdir");
let path = write_config(
&temp,
"[[llm]]\nmodel = \"a\"\n\n[[llm]]\nmodel = \"b\"\nmax_concurrent = 0\n",
);
let err = load(&path).expect_err("zero concurrency must not load");
match err {
ConfigError::ZeroConcurrency { index } => {
assert_eq!(index, 1, "the second provider is the offender");
let rendered = ConfigError::ZeroConcurrency { index }.to_string();
assert!(rendered.contains("#2 in file order"), "got {rendered:?}");
}
other => panic!("expected ZeroConcurrency, got {other:?}"),
}
}