1package config
2
3import (
4 "io"
5 "strings"
6 "testing"
7
8 "github.com/charmbracelet/crush/internal/fur/provider"
9 "github.com/charmbracelet/crush/pkg/env"
10 "github.com/stretchr/testify/assert"
11)
12
13func TestConfig_LoadFromReaders(t *testing.T) {
14 data1 := strings.NewReader(`{"providers": {"openai": {"api_key": "key1", "base_url": "https://api.openai.com/v1"}}}`)
15 data2 := strings.NewReader(`{"providers": {"openai": {"api_key": "key2", "base_url": "https://api.openai.com/v2"}}}`)
16 data3 := strings.NewReader(`{"providers": {"openai": {}}}`)
17
18 loadedConfig, err := loadFromReaders([]io.Reader{data1, data2, data3})
19
20 assert.NoError(t, err)
21 assert.NotNil(t, loadedConfig)
22 assert.Len(t, loadedConfig.Providers, 1)
23 assert.Equal(t, "key2", loadedConfig.Providers["openai"].APIKey)
24 assert.Equal(t, "https://api.openai.com/v2", loadedConfig.Providers["openai"].BaseURL)
25}
26
27func TestConfig_setDefaults(t *testing.T) {
28 cfg := &Config{}
29
30 setDefaults("/tmp", cfg)
31
32 assert.NotNil(t, cfg.Options)
33 assert.NotNil(t, cfg.Options.TUI)
34 assert.NotNil(t, cfg.Options.ContextPaths)
35 assert.NotNil(t, cfg.Providers)
36 assert.NotNil(t, cfg.Models)
37 assert.NotNil(t, cfg.LSP)
38 assert.NotNil(t, cfg.MCP)
39 assert.Equal(t, "/tmp/.crush", cfg.Options.DataDirectory)
40 for _, path := range defaultContextPaths {
41 assert.Contains(t, cfg.Options.ContextPaths, path)
42 }
43 assert.Equal(t, "/tmp", cfg.workingDir)
44}
45
46func TestConfig_configureProviders(t *testing.T) {
47 knownProviders := []provider.Provider{
48 {
49 ID: "openai",
50 APIKey: "$OPENAI_API_KEY",
51 APIEndpoint: "https://api.openai.com/v1",
52 Models: []provider.Model{{
53 ID: "test-model",
54 }},
55 },
56 }
57
58 cfg := &Config{}
59 setDefaults("/tmp", cfg)
60 env := env.NewFromMap(map[string]string{
61 "OPENAI_API_KEY": "test-key",
62 })
63 resolver := NewEnvironmentVariableResolver(env)
64 err := configureProviders(cfg, env, resolver, knownProviders)
65 assert.NoError(t, err)
66 assert.Len(t, cfg.Providers, 1)
67
68 // We want to make sure that we keep the configured API key as a placeholder
69 assert.Equal(t, "$OPENAI_API_KEY", cfg.Providers["openai"].APIKey)
70}
71
72func TestConfig_configureProvidersWithOverride(t *testing.T) {
73 knownProviders := []provider.Provider{
74 {
75 ID: "openai",
76 APIKey: "$OPENAI_API_KEY",
77 APIEndpoint: "https://api.openai.com/v1",
78 Models: []provider.Model{{
79 ID: "test-model",
80 }},
81 },
82 }
83
84 cfg := &Config{
85 Providers: map[string]ProviderConfig{
86 "openai": {
87 APIKey: "xyz",
88 BaseURL: "https://api.openai.com/v2",
89 Models: []provider.Model{
90 {
91 ID: "test-model",
92 Name: "Updated",
93 },
94 {
95 ID: "another-model",
96 },
97 },
98 },
99 },
100 }
101 setDefaults("/tmp", cfg)
102 env := env.NewFromMap(map[string]string{
103 "OPENAI_API_KEY": "test-key",
104 })
105 resolver := NewEnvironmentVariableResolver(env)
106 err := configureProviders(cfg, env, resolver, knownProviders)
107 assert.NoError(t, err)
108 assert.Len(t, cfg.Providers, 1)
109
110 // We want to make sure that we keep the configured API key as a placeholder
111 assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
112 assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
113 assert.Len(t, cfg.Providers["openai"].Models, 2)
114 assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Name)
115}
116
117func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
118 knownProviders := []provider.Provider{
119 {
120 ID: "openai",
121 APIKey: "$OPENAI_API_KEY",
122 APIEndpoint: "https://api.openai.com/v1",
123 Models: []provider.Model{{
124 ID: "test-model",
125 }},
126 },
127 }
128
129 cfg := &Config{
130 Providers: map[string]ProviderConfig{
131 "custom": {
132 APIKey: "xyz",
133 BaseURL: "https://api.someendpoint.com/v2",
134 Models: []provider.Model{
135 {
136 ID: "test-model",
137 },
138 },
139 },
140 },
141 }
142 setDefaults("/tmp", cfg)
143 env := env.NewFromMap(map[string]string{
144 "OPENAI_API_KEY": "test-key",
145 })
146 resolver := NewEnvironmentVariableResolver(env)
147 err := configureProviders(cfg, env, resolver, knownProviders)
148 assert.NoError(t, err)
149 // Should be to because of the env variable
150 assert.Len(t, cfg.Providers, 2)
151
152 // We want to make sure that we keep the configured API key as a placeholder
153 assert.Equal(t, "xyz", cfg.Providers["custom"].APIKey)
154 assert.Equal(t, "https://api.someendpoint.com/v2", cfg.Providers["custom"].BaseURL)
155 assert.Len(t, cfg.Providers["custom"].Models, 1)
156
157 _, ok := cfg.Providers["openai"]
158 assert.True(t, ok, "OpenAI provider should still be present")
159}
160
161func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
162 knownProviders := []provider.Provider{
163 {
164 ID: provider.InferenceProviderBedrock,
165 APIKey: "",
166 APIEndpoint: "",
167 Models: []provider.Model{{
168 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
169 }},
170 },
171 }
172
173 cfg := &Config{}
174 setDefaults("/tmp", cfg)
175 env := env.NewFromMap(map[string]string{
176 "AWS_ACCESS_KEY_ID": "test-key-id",
177 "AWS_SECRET_ACCESS_KEY": "test-secret-key",
178 })
179 resolver := NewEnvironmentVariableResolver(env)
180 err := configureProviders(cfg, env, resolver, knownProviders)
181 assert.NoError(t, err)
182 assert.Len(t, cfg.Providers, 1)
183
184 bedrockProvider, ok := cfg.Providers["bedrock"]
185 assert.True(t, ok, "Bedrock provider should be present")
186 assert.Len(t, bedrockProvider.Models, 1)
187 assert.Equal(t, "anthropic.claude-sonnet-4-20250514-v1:0", bedrockProvider.Models[0].ID)
188}
189
190func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
191 knownProviders := []provider.Provider{
192 {
193 ID: provider.InferenceProviderBedrock,
194 APIKey: "",
195 APIEndpoint: "",
196 Models: []provider.Model{{
197 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
198 }},
199 },
200 }
201
202 cfg := &Config{}
203 setDefaults("/tmp", cfg)
204 env := env.NewFromMap(map[string]string{})
205 resolver := NewEnvironmentVariableResolver(env)
206 err := configureProviders(cfg, env, resolver, knownProviders)
207 assert.NoError(t, err)
208 // Provider should not be configured without credentials
209 assert.Len(t, cfg.Providers, 0)
210}
211
212func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
213 knownProviders := []provider.Provider{
214 {
215 ID: provider.InferenceProviderBedrock,
216 APIKey: "",
217 APIEndpoint: "",
218 Models: []provider.Model{{
219 ID: "some-random-model",
220 }},
221 },
222 }
223
224 cfg := &Config{}
225 setDefaults("/tmp", cfg)
226 env := env.NewFromMap(map[string]string{
227 "AWS_ACCESS_KEY_ID": "test-key-id",
228 "AWS_SECRET_ACCESS_KEY": "test-secret-key",
229 })
230 resolver := NewEnvironmentVariableResolver(env)
231 err := configureProviders(cfg, env, resolver, knownProviders)
232 assert.Error(t, err)
233}
234
235func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
236 knownProviders := []provider.Provider{
237 {
238 ID: provider.InferenceProviderVertexAI,
239 APIKey: "",
240 APIEndpoint: "",
241 Models: []provider.Model{{
242 ID: "gemini-pro",
243 }},
244 },
245 }
246
247 cfg := &Config{}
248 setDefaults("/tmp", cfg)
249 env := env.NewFromMap(map[string]string{
250 "GOOGLE_GENAI_USE_VERTEXAI": "true",
251 "GOOGLE_CLOUD_PROJECT": "test-project",
252 "GOOGLE_CLOUD_LOCATION": "us-central1",
253 })
254 resolver := NewEnvironmentVariableResolver(env)
255 err := configureProviders(cfg, env, resolver, knownProviders)
256 assert.NoError(t, err)
257 assert.Len(t, cfg.Providers, 1)
258
259 vertexProvider, ok := cfg.Providers["vertexai"]
260 assert.True(t, ok, "VertexAI provider should be present")
261 assert.Len(t, vertexProvider.Models, 1)
262 assert.Equal(t, "gemini-pro", vertexProvider.Models[0].ID)
263 assert.Equal(t, "test-project", vertexProvider.ExtraParams["project"])
264 assert.Equal(t, "us-central1", vertexProvider.ExtraParams["location"])
265}
266
267func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
268 knownProviders := []provider.Provider{
269 {
270 ID: provider.InferenceProviderVertexAI,
271 APIKey: "",
272 APIEndpoint: "",
273 Models: []provider.Model{{
274 ID: "gemini-pro",
275 }},
276 },
277 }
278
279 cfg := &Config{}
280 setDefaults("/tmp", cfg)
281 env := env.NewFromMap(map[string]string{
282 "GOOGLE_GENAI_USE_VERTEXAI": "false",
283 "GOOGLE_CLOUD_PROJECT": "test-project",
284 "GOOGLE_CLOUD_LOCATION": "us-central1",
285 })
286 resolver := NewEnvironmentVariableResolver(env)
287 err := configureProviders(cfg, env, resolver, knownProviders)
288 assert.NoError(t, err)
289 // Provider should not be configured without proper credentials
290 assert.Len(t, cfg.Providers, 0)
291}
292
293func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
294 knownProviders := []provider.Provider{
295 {
296 ID: provider.InferenceProviderVertexAI,
297 APIKey: "",
298 APIEndpoint: "",
299 Models: []provider.Model{{
300 ID: "gemini-pro",
301 }},
302 },
303 }
304
305 cfg := &Config{}
306 setDefaults("/tmp", cfg)
307 env := env.NewFromMap(map[string]string{
308 "GOOGLE_GENAI_USE_VERTEXAI": "true",
309 "GOOGLE_CLOUD_LOCATION": "us-central1",
310 })
311 resolver := NewEnvironmentVariableResolver(env)
312 err := configureProviders(cfg, env, resolver, knownProviders)
313 assert.NoError(t, err)
314 // Provider should not be configured without project
315 assert.Len(t, cfg.Providers, 0)
316}