use crate::config::{
OpenaiCompatibleSttConfig, OpenaiCompatibleTtsConfig, ProviderConfig, ProviderConfigs,
SttProviders, TtsProviders, merge_provider_keys,
};
fn key_only(api_key: &str) -> ProviderConfig {
ProviderConfig {
api_key: Some(api_key.to_string()),
..Default::default()
}
}
#[test]
fn opencode_api_key_from_keys_toml_lands_in_runtime_config() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
opencode: Some(key_only("oc_test_key")),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
let opencode = merged.opencode.expect("opencode entry created");
assert_eq!(opencode.api_key.as_deref(), Some("oc_test_key"));
assert!(
opencode.enabled,
"first-time keys.toml load should auto-enable opencode"
);
}
#[test]
fn opencode_existing_config_disabled_state_is_preserved_on_key_merge() {
let base = ProviderConfigs {
opencode: Some(ProviderConfig {
enabled: false,
..Default::default()
}),
..Default::default()
};
let keys = ProviderConfigs {
opencode: Some(key_only("oc_test_key")),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
let opencode = merged.opencode.expect("opencode entry preserved");
assert_eq!(opencode.api_key.as_deref(), Some("oc_test_key"));
assert!(
!opencode.enabled,
"user's explicit disabled state must not flip on key merge"
);
}
#[test]
fn sentinel_placeholder_does_not_leak_into_runtime_config() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
opencode: Some(key_only("__EXISTING_KEY__")),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
assert!(
merged.opencode.is_none(),
"sentinel must not create an opencode entry"
);
}
#[test]
fn anthropic_openai_qwen_keys_still_merge_after_opencode_addition() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
anthropic: Some(key_only("ant_key")),
openai: Some(key_only("oai_key")),
qwen: Some(key_only("qwen_key")),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
assert_eq!(
merged.anthropic.and_then(|c| c.api_key).as_deref(),
Some("ant_key")
);
assert_eq!(
merged.openai.and_then(|c| c.api_key).as_deref(),
Some("oai_key")
);
assert_eq!(
merged.qwen.and_then(|c| c.api_key).as_deref(),
Some("qwen_key")
);
}
#[test]
fn moonshot_api_key_from_keys_toml_lands_in_runtime_config() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
moonshot: Some(key_only("sk-kimi-test")),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
let moonshot = merged.moonshot.expect("moonshot entry created");
assert_eq!(moonshot.api_key.as_deref(), Some("sk-kimi-test"));
}
#[test]
fn ollama_api_key_from_keys_toml_lands_in_runtime_config() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
ollama: Some(key_only("ollama_cloud_key")),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
let ollama = merged.ollama.expect("ollama entry created");
assert_eq!(ollama.api_key.as_deref(), Some("ollama_cloud_key"));
}
#[test]
fn stt_openai_compatible_api_key_lands_in_runtime_config() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
stt: Some(SttProviders {
openai_compatible: Some(OpenaiCompatibleSttConfig {
api_key: Some("stt_compat_key".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
assert_eq!(
merged
.stt
.and_then(|s| s.openai_compatible)
.and_then(|c| c.api_key)
.as_deref(),
Some("stt_compat_key")
);
}
#[test]
fn tts_openai_compatible_api_key_lands_in_runtime_config() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
tts: Some(TtsProviders {
openai_compatible: Some(OpenaiCompatibleTtsConfig {
api_key: Some("tts_compat_key".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
assert_eq!(
merged
.tts
.and_then(|s| s.openai_compatible)
.and_then(|c| c.api_key)
.as_deref(),
Some("tts_compat_key")
);
}
#[test]
fn both_keyed_voice_providers_survive_one_merge() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
stt: Some(SttProviders {
groq: Some(key_only("groq_key")),
openai_compatible: Some(OpenaiCompatibleSttConfig {
api_key: Some("stt_compat_key".to_string()),
..Default::default()
}),
..Default::default()
}),
tts: Some(TtsProviders {
openai: Some(key_only("tts_openai_key")),
openai_compatible: Some(OpenaiCompatibleTtsConfig {
api_key: Some("tts_compat_key".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
let stt = merged.stt.expect("stt section created");
let tts = merged.tts.expect("tts section created");
assert_eq!(
stt.groq.and_then(|c| c.api_key).as_deref(),
Some("groq_key")
);
assert_eq!(
stt.openai_compatible.and_then(|c| c.api_key).as_deref(),
Some("stt_compat_key")
);
assert_eq!(
tts.openai.and_then(|c| c.api_key).as_deref(),
Some("tts_openai_key")
);
assert_eq!(
tts.openai_compatible.and_then(|c| c.api_key).as_deref(),
Some("tts_compat_key")
);
}
#[test]
fn voice_sentinel_placeholder_is_never_merged() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
stt: Some(SttProviders {
groq: Some(key_only("__EXISTING_KEY__")),
..Default::default()
}),
tts: Some(TtsProviders {
openai: Some(key_only("__EXISTING_KEY__")),
..Default::default()
}),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
assert!(
merged.stt.is_none(),
"sentinel must not create an stt section"
);
assert!(
merged.tts.is_none(),
"sentinel must not create a tts section"
);
}
#[test]
fn a_key_stored_with_the_sentinel_glued_to_it_still_merges() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
anthropic: Some(key_only("__EXISTING_KEY__sk-ant-real")),
tts: Some(TtsProviders {
openai: Some(key_only("__EXISTING_KEY__sk-tts-real")),
..Default::default()
}),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
assert_eq!(
merged.anthropic.and_then(|c| c.api_key).as_deref(),
Some("sk-ant-real"),
"the marker must be stripped, not treated as part of the key"
);
assert_eq!(
merged
.tts
.and_then(|t| t.openai)
.and_then(|c| c.api_key)
.as_deref(),
Some("sk-tts-real")
);
}
#[test]
fn empty_voice_sections_do_not_create_phantom_config() {
let base = ProviderConfigs::default();
let keys = ProviderConfigs {
stt: Some(SttProviders::default()),
tts: Some(TtsProviders::default()),
..Default::default()
};
let merged = merge_provider_keys(base, keys);
assert!(merged.stt.is_none(), "empty stt keys must not allocate");
assert!(merged.tts.is_none(), "empty tts keys must not allocate");
}