1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//! Regression tests for `merge_provider_keys`.
//!
//! Background: a key written to `keys.toml` under
//! `[providers.<name>] api_key = "…"` only takes effect at runtime if
//! `merge_provider_keys` has an explicit branch for `<name>`. Adding a
//! new top-level provider field on `ProviderConfigs` without adding a
//! corresponding merge branch causes a silent failure: the key is on
//! disk, the running config never sees it, and the provider factory
//! reports "API key missing" with no obvious cause.
//!
//! These tests pin the contract for the providers we ship today.
use crate::config::{ProviderConfig, ProviderConfigs, 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() {
// Repro for the v0.3.16 bug: /models writes `[providers.opencode]
// api_key = "..."` to keys.toml, but on the next config reload
// merge_provider_keys was missing an opencode branch — runtime
// Config.providers.opencode.api_key stayed None, factory.rs
// reported "API key missing", and the new selection silently
// failed to take effect.
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() {
// If config.toml has `enabled = false` for opencode but keys.toml
// carries an api_key, the user's explicit disabled state wins —
// we only auto-enable when there's no entry at all.
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() {
// /models uses `__EXISTING_KEY__` internally to mean "keep the
// current key". The merge function must never propagate that
// sentinel into the 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() {
// Smoke test that the existing branches still work — protects
// against accidental regressions when adding new branches.
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")
);
}