use super::*;
fn exporting(vars: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let vars: Vec<(String, String)> = vars
.iter()
.map(|(var, value)| ((*var).to_string(), (*value).to_string()))
.collect();
move |name: &str| {
vars.iter()
.find(|(var, _)| var == name)
.map(|(_, value)| value.clone())
}
}
fn nothing_exported() -> impl Fn(&str) -> Option<String> {
exporting(&[])
}
fn write(path: &std::path::Path, body: &str) {
std::fs::create_dir_all(path.parent().expect("a parent")).expect("create dir");
std::fs::write(path, body).expect("write file");
}
fn workspace_file(root: &std::path::Path, body: &str) {
write(&root.join(DEFAULT_WORKSPACE_CONFIG_FILE), body);
}
fn global_file(dir: &std::path::Path, body: &str) {
write(&dir.join(DEFAULT_GLOBAL_CONFIG_FILE), body);
}
fn discover(
root: &std::path::Path,
global: Option<&std::path::Path>,
) -> Result<Config, ConfigError> {
Config::discover_with(root, global, ¬hing_exported())
}
#[test]
fn nothing_on_disk_says_nothing() {
let tmp = tempfile::tempdir().expect("tempdir");
let config = discover(tmp.path(), None).expect("a missing file is not an error");
assert!(config.is_empty());
assert!(config.files.is_empty());
}
#[test]
fn a_workspace_file_pins_the_model_and_names_itself() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(
tmp.path(),
r#"{"schema": 1, "model": "claude-sonnet-4-5-20250929"}"#,
);
let config = discover(tmp.path(), None).expect("a well-formed file");
let model = config.model.as_ref().expect("the file set a model");
assert_eq!(model.value, "claude-sonnet-4-5-20250929");
assert_eq!(model.scope, ContextScope::Workspace);
assert_eq!(model.path, tmp.path().join(DEFAULT_WORKSPACE_CONFIG_FILE));
assert_eq!(
config.model_selector(),
Some(ModelSelector::Id("claude-sonnet-4-5-20250929".to_string()))
);
assert_eq!(config.files.len(), 1, "the file that decided is reported");
assert_eq!(config.files[0].scope, "workspace");
}
#[test]
fn every_key_is_optional() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1}"#);
let config = discover(tmp.path(), None).expect("an empty file is a choice");
assert!(
config.is_empty(),
"a file that says nothing settles nothing"
);
assert_eq!(config.files.len(), 1, "it was still read");
}
#[test]
fn the_workspace_file_outranks_the_global_one_key_by_key() {
let tmp = tempfile::tempdir().expect("tempdir");
let global = tmp.path().join("global");
workspace_file(tmp.path(), r#"{"schema": 1, "model": "workspace-model"}"#);
global_file(
&global,
r#"{"schema": 1, "model": "global-model", "effort": "high", "provider": "openai"}"#,
);
let config = discover(tmp.path(), Some(&global)).expect("both files parse");
let model = config.model.expect("a model");
assert_eq!(model.value, "workspace-model");
assert_eq!(model.scope, ContextScope::Workspace);
let effort = config.effort.expect("the global effort survives");
assert_eq!(effort.value, Effort::High);
assert_eq!(effort.scope, ContextScope::Global);
assert_eq!(effort.path, global.join(DEFAULT_GLOBAL_CONFIG_FILE));
let provider = config.provider.expect("the global provider survives");
assert_eq!(provider.value, BuiltinProvider::OpenAI);
assert_eq!(provider.scope, ContextScope::Global);
assert_eq!(config.files.len(), 2, "both files are reported");
assert_eq!(
config
.files
.iter()
.map(|file| file.scope.as_str())
.collect::<Vec<_>>(),
vec!["workspace", "global"],
"most specific first"
);
}
#[test]
fn a_base_url_in_the_workspace_file_is_refused_and_the_file_is_named() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(
tmp.path(),
r#"{"schema": 1, "base_url": "https://gateway.example.com/v1"}"#,
);
let error = discover(tmp.path(), None).expect_err("refused, not ignored");
assert!(
matches!(error, ConfigError::WorkspaceBaseUrl { .. }),
"{error}"
);
let rendered = error.to_string();
assert!(
rendered.contains(DEFAULT_WORKSPACE_CONFIG_FILE),
"{rendered}"
);
assert!(rendered.contains("base_url"), "{rendered}");
}
#[test]
fn a_base_url_in_the_global_file_is_honored() {
let tmp = tempfile::tempdir().expect("tempdir");
let global = tmp.path().join("global");
global_file(
&global,
r#"{"schema": 1, "base_url": "https://gateway.example.com/v1"}"#,
);
let config = discover(tmp.path(), Some(&global)).expect("the user's own file may say it");
let base_url = config.base_url.expect("a base URL");
assert_eq!(
base_url.value, "https://gateway.example.com/",
"normalized where a typo can still name this file"
);
assert_eq!(base_url.scope, ContextScope::Global);
}
#[test]
fn a_workspace_file_may_still_choose_a_provider() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "provider": "anthropic"}"#);
let config = discover(tmp.path(), None).expect("a well-formed file");
assert_eq!(
config.provider.expect("a provider").value,
BuiltinProvider::Anthropic
);
}
#[test]
fn there_is_no_api_key_and_saying_one_is_an_error() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "api_key": "sk-live-nope"}"#);
let error = discover(tmp.path(), None).expect_err("there is no such key");
assert!(matches!(error, ConfigError::Parse { .. }), "{error}");
assert!(
!error.to_string().contains("sk-live-nope"),
"no error repeats a value it read: {error}"
);
}
#[test]
fn a_misspelled_key_is_an_error_not_a_silent_default() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "modle": "gpt-5"}"#);
let error = discover(tmp.path(), None).expect_err("a typo is caught");
assert!(matches!(error, ConfigError::Parse { .. }), "{error}");
}
#[test]
fn a_malformed_file_is_an_error_naming_it() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), "{not json");
let error = discover(tmp.path(), None).expect_err("malformed is an error");
match error {
ConfigError::Parse { ref path, .. } => {
assert_eq!(path, &tmp.path().join(DEFAULT_WORKSPACE_CONFIG_FILE));
}
other => panic!("expected a parse error, got {other}"),
}
}
#[test]
fn a_file_that_states_no_schema_is_an_error() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"model": "gpt-5"}"#);
let error = discover(tmp.path(), None).expect_err("basis will not guess a schema");
assert!(matches!(error, ConfigError::NoSchema { .. }), "{error}");
}
#[test]
fn a_schema_from_the_future_is_an_error() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 99, "model": "gpt-5"}"#);
let error = discover(tmp.path(), None).expect_err("an unknown schema is refused");
assert!(
matches!(error, ConfigError::UnsupportedSchema { schema: 99, .. }),
"{error}"
);
}
#[test]
fn environment_placeholders_are_expanded() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(
tmp.path(),
r#"{"schema": 1, "model": "${TEAM_MODEL}", "provider": "${TEAM_PROVIDER}"}"#,
);
let config = Config::discover_with(
tmp.path(),
None,
&exporting(&[("TEAM_MODEL", "gpt-5"), ("TEAM_PROVIDER", "openai")]),
)
.expect("both are set");
assert_eq!(config.model.expect("a model").value, "gpt-5");
assert_eq!(
config.provider.expect("a provider").value,
BuiltinProvider::OpenAI
);
}
#[test]
fn an_unset_placeholder_names_the_key_and_the_variable() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "model": "${TEAM_MODEL}"}"#);
let error = discover(tmp.path(), None).expect_err("an unset variable is an error");
let rendered = error.to_string();
assert!(rendered.contains("model"), "{rendered}");
assert!(rendered.contains("TEAM_MODEL"), "{rendered}");
}
#[test]
fn a_placeholder_may_carry_a_default() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(
tmp.path(),
r#"{"schema": 1, "model": "${TEAM_MODEL:-gpt-5}"}"#,
);
let config = discover(tmp.path(), None).expect("the fallback answers");
assert_eq!(config.model.expect("a model").value, "gpt-5");
}
#[test]
fn the_five_effort_spellings_are_the_flags_own() {
for (text, expected) in [
("low", Effort::Low),
("medium", Effort::Medium),
("high", Effort::High),
("xhigh", Effort::XHigh),
("max", Effort::Max),
] {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(
tmp.path(),
&format!(r#"{{"schema": 1, "effort": "{text}"}}"#),
);
let config = discover(tmp.path(), None).expect("a known spelling");
assert_eq!(config.effort.expect("an effort").value, expected);
}
}
#[test]
fn an_unknown_effort_is_an_error() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "effort": "hardest"}"#);
let error = discover(tmp.path(), None).expect_err("only five words are efforts");
assert!(matches!(error, ConfigError::Parse { .. }), "{error}");
}
#[test]
fn an_unknown_provider_names_the_key_and_the_alternatives() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "provider": "hal9000"}"#);
let error = discover(tmp.path(), None).expect_err("rejected");
let rendered = error.to_string();
assert!(rendered.contains("provider"), "{rendered}");
assert!(rendered.contains("anthropic"), "{rendered}");
}
#[test]
fn an_empty_model_is_an_error_rather_than_a_request_for_nothing() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "model": " "}"#);
let error = discover(tmp.path(), None).expect_err("rejected");
assert!(
matches!(error, ConfigError::Invalid { key: "model", .. }),
"{error}"
);
}
#[test]
fn a_directory_where_the_file_should_be_is_ignored() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(tmp.path().join(DEFAULT_WORKSPACE_CONFIG_FILE))
.expect("create a directory with the file's name");
let config = discover(tmp.path(), None).expect("discovery succeeds");
assert!(config.is_empty());
}
#[test]
fn the_same_file_reached_twice_is_read_once() {
let tmp = tempfile::tempdir().expect("tempdir");
let global = tmp.path().join(".basis");
workspace_file(tmp.path(), r#"{"schema": 1, "model": "gpt-5"}"#);
let config = discover(tmp.path(), Some(&global)).expect("discovery succeeds");
assert_eq!(config.files.len(), 1);
assert_eq!(config.files[0].scope, "workspace");
}
#[test]
fn an_empty_config_is_the_off_switch() {
let config = Config::default();
assert!(config.is_empty());
assert_eq!(config.model_selector(), None);
}
#[test]
fn a_ring_in_the_global_file_names_its_members_in_order_and_where_their_keys_live() {
let tmp = tempfile::tempdir().expect("tempdir");
let global = tmp.path().join("global");
global_file(
&global,
r#"{"schema": 1, "gateways": [
{"base_url": "${GATEWAY_HOST}/v1", "api_key_file": "/run/secrets/local"},
{"base_url": "http://relay/v1", "api_key_env": "RELAY_KEY",
"strip_model_prefix": "openai/"}
]}"#,
);
let config = Config::discover_with(
tmp.path(),
Some(&global),
&exporting(&[("GATEWAY_HOST", "http://local:2455")]),
)
.expect("the user's own file may say it");
assert!(!config.is_empty());
let ring = config.gateways.expect("a ring");
assert_eq!(ring.scope, ContextScope::Global);
assert_eq!(
ring.value,
GatewayRingConfig {
members: vec![
crate::runtime::GatewayMember::new("http://local:2455/v1")
.with_api_key_file("/run/secrets/local"),
crate::runtime::GatewayMember::new("http://relay/v1")
.with_api_key_env("RELAY_KEY")
.with_model_prefix_stripped("openai/"),
],
policy: None,
}
);
}
#[test]
fn a_ring_object_carries_its_policy() {
use crate::runtime::GatewayRingPolicy;
use std::time::Duration;
let policy = |body: &str| {
let tmp = tempfile::tempdir().expect("tempdir");
let global = tmp.path().join("global");
global_file(&global, body);
discover(tmp.path(), Some(&global))
.expect("parses")
.gateways
.expect("a ring")
.value
.policy
};
let members = r#""members": [{"base_url": "http://a/v1"}]"#;
assert_eq!(
policy(&format!(
r#"{{"schema": 1, "gateways": {{{members}, "failure_threshold": 2, "cooldown_secs": 30}}}}"#
)),
Some(GatewayRingPolicy {
failure_threshold: 2,
cooldown: Some(Duration::from_secs(30)),
})
);
assert_eq!(
policy(&format!(
r#"{{"schema": 1, "gateways": {{{members}, "sticky": true}}}}"#
)),
Some(GatewayRingPolicy::sticky())
);
assert_eq!(
policy(&format!(r#"{{"schema": 1, "gateways": {{{members}}}}}"#)),
None,
"an object that says nothing about policy leaves the host's standing"
);
}
#[test]
fn a_ring_in_the_workspace_file_is_refused_like_a_base_url() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(
tmp.path(),
r#"{"schema": 1, "gateways": [{"base_url": "http://a/v1"}]}"#,
);
let error = discover(tmp.path(), None).expect_err("refused, not ignored");
assert!(
matches!(error, ConfigError::WorkspaceGateways { .. }),
"{error}"
);
}
#[test]
fn a_ring_that_cannot_be_meant_is_refused_by_key() {
for (gateways, says) in [
(r#"[]"#, "lists no gateways"),
(
r#"[{"base_url": "http://a/v1", "api_key_env": "A", "api_key_file": "/a"}]"#,
"gateway #0 names both",
),
(r#"[{"base_url": "not a url"}]"#, "gateway #0"),
(
r#"{"members": [{"base_url": "http://a/v1"}], "sticky": true, "cooldown_secs": 5}"#,
"both `sticky` and `cooldown_secs`",
),
(
r#"{"members": [{"base_url": "http://a/v1"}], "failure_threshold": 0}"#,
"`failure_threshold` to 0",
),
] {
let tmp = tempfile::tempdir().expect("tempdir");
let global = tmp.path().join("global");
global_file(
&global,
&format!(r#"{{"schema": 1, "gateways": {gateways}}}"#),
);
let error = discover(tmp.path(), Some(&global)).expect_err(gateways);
let rendered = error.to_string();
assert!(
matches!(
error,
ConfigError::Invalid {
key: "gateways",
..
}
),
"{rendered}"
);
assert!(rendered.contains(says), "{rendered}");
}
}
#[test]
fn a_ring_beside_a_base_url_is_refused() {
let tmp = tempfile::tempdir().expect("tempdir");
let global = tmp.path().join("global");
global_file(
&global,
r#"{"schema": 1, "base_url": "http://a/v1", "gateways": [{"base_url": "http://b/v1"}]}"#,
);
let error = discover(tmp.path(), Some(&global)).expect_err("refused");
assert!(error.to_string().contains("beside `base_url`"), "{error}");
}
#[test]
fn the_variable_reads_the_files_schema_and_names_no_value_when_it_cannot() {
let ring =
parse_gateways_var(r#"[{"base_url": "http://a/v1", "api_key_env": "A"}]"#).expect("parses");
assert_eq!(ring.members.len(), 1);
let reason = parse_gateways_var(r#"[{"base_url": "http://a/v1", "api_kee": "sk-secret"}]"#)
.expect_err("an unknown key");
assert!(!reason.contains("sk-secret"), "{reason}");
assert!(parse_gateways_var("not json").is_err());
}
#[test]
fn a_named_file_is_read_as_the_hosts_own_and_nothing_else_is_probed() {
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "model": "from-the-repo"}"#);
let named = tmp.path().join("host.json");
write(
&named,
r#"{"schema": 1, "base_url": "http://gateway/v1", "model": "from-the-host"}"#,
);
let config = Config::read_file_with(&named, ¬hing_exported())
.expect("a file the host named may set base_url");
assert_eq!(
config.model.map(|model| model.value).as_deref(),
Some("from-the-host")
);
assert_eq!(
config.base_url.map(|url| url.value).as_deref(),
Some("http://gateway/")
);
assert_eq!(config.files.len(), 1);
}
#[test]
fn a_lookup_is_recorded_in_three_spellings_and_defaults_to_nothing() {
for (lookup, json) in [
(ConfigLookup::None, r#""none""#),
(ConfigLookup::Discover, r#""discover""#),
(
ConfigLookup::File(PathBuf::from("/etc/basis.json")),
r#"{"file":"/etc/basis.json"}"#,
),
] {
assert_eq!(serde_json::to_string(&lookup).expect("serializes"), json);
assert_eq!(
serde_json::from_str::<ConfigLookup>(json).expect("parses"),
lookup
);
}
assert_eq!(ConfigLookup::default(), ConfigLookup::None);
let tmp = tempfile::tempdir().expect("tempdir");
workspace_file(tmp.path(), r#"{"schema": 1, "model": "from-the-repo"}"#);
assert!(
ConfigLookup::None
.load(tmp.path(), None)
.expect("reads nothing")
.is_empty()
);
}