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 cfg.setDefaults("/tmp")
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 cfg.setDefaults("/tmp")
60 env := env.NewFromMap(map[string]string{
61 "OPENAI_API_KEY": "test-key",
62 })
63 resolver := NewEnvironmentVariableResolver(env)
64 err := cfg.configureProviders(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 cfg.setDefaults("/tmp")
102
103 env := env.NewFromMap(map[string]string{
104 "OPENAI_API_KEY": "test-key",
105 })
106 resolver := NewEnvironmentVariableResolver(env)
107 err := cfg.configureProviders(env, resolver, knownProviders)
108 assert.NoError(t, err)
109 assert.Len(t, cfg.Providers, 1)
110
111 // We want to make sure that we keep the configured API key as a placeholder
112 assert.Equal(t, "xyz", cfg.Providers["openai"].APIKey)
113 assert.Equal(t, "https://api.openai.com/v2", cfg.Providers["openai"].BaseURL)
114 assert.Len(t, cfg.Providers["openai"].Models, 2)
115 assert.Equal(t, "Updated", cfg.Providers["openai"].Models[0].Name)
116}
117
118func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
119 knownProviders := []provider.Provider{
120 {
121 ID: "openai",
122 APIKey: "$OPENAI_API_KEY",
123 APIEndpoint: "https://api.openai.com/v1",
124 Models: []provider.Model{{
125 ID: "test-model",
126 }},
127 },
128 }
129
130 cfg := &Config{
131 Providers: map[string]ProviderConfig{
132 "custom": {
133 APIKey: "xyz",
134 BaseURL: "https://api.someendpoint.com/v2",
135 Models: []provider.Model{
136 {
137 ID: "test-model",
138 },
139 },
140 },
141 },
142 }
143 cfg.setDefaults("/tmp")
144 env := env.NewFromMap(map[string]string{
145 "OPENAI_API_KEY": "test-key",
146 })
147 resolver := NewEnvironmentVariableResolver(env)
148 err := cfg.configureProviders(env, resolver, knownProviders)
149 assert.NoError(t, err)
150 // Should be to because of the env variable
151 assert.Len(t, cfg.Providers, 2)
152
153 // We want to make sure that we keep the configured API key as a placeholder
154 assert.Equal(t, "xyz", cfg.Providers["custom"].APIKey)
155 assert.Equal(t, "https://api.someendpoint.com/v2", cfg.Providers["custom"].BaseURL)
156 assert.Len(t, cfg.Providers["custom"].Models, 1)
157
158 _, ok := cfg.Providers["openai"]
159 assert.True(t, ok, "OpenAI provider should still be present")
160}
161
162func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
163 knownProviders := []provider.Provider{
164 {
165 ID: provider.InferenceProviderBedrock,
166 APIKey: "",
167 APIEndpoint: "",
168 Models: []provider.Model{{
169 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
170 }},
171 },
172 }
173
174 cfg := &Config{}
175 cfg.setDefaults("/tmp")
176 env := env.NewFromMap(map[string]string{
177 "AWS_ACCESS_KEY_ID": "test-key-id",
178 "AWS_SECRET_ACCESS_KEY": "test-secret-key",
179 })
180 resolver := NewEnvironmentVariableResolver(env)
181 err := cfg.configureProviders(env, resolver, knownProviders)
182 assert.NoError(t, err)
183 assert.Len(t, cfg.Providers, 1)
184
185 bedrockProvider, ok := cfg.Providers["bedrock"]
186 assert.True(t, ok, "Bedrock provider should be present")
187 assert.Len(t, bedrockProvider.Models, 1)
188 assert.Equal(t, "anthropic.claude-sonnet-4-20250514-v1:0", bedrockProvider.Models[0].ID)
189}
190
191func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
192 knownProviders := []provider.Provider{
193 {
194 ID: provider.InferenceProviderBedrock,
195 APIKey: "",
196 APIEndpoint: "",
197 Models: []provider.Model{{
198 ID: "anthropic.claude-sonnet-4-20250514-v1:0",
199 }},
200 },
201 }
202
203 cfg := &Config{}
204 cfg.setDefaults("/tmp")
205 env := env.NewFromMap(map[string]string{})
206 resolver := NewEnvironmentVariableResolver(env)
207 err := cfg.configureProviders(env, resolver, knownProviders)
208 assert.NoError(t, err)
209 // Provider should not be configured without credentials
210 assert.Len(t, cfg.Providers, 0)
211}
212
213func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
214 knownProviders := []provider.Provider{
215 {
216 ID: provider.InferenceProviderBedrock,
217 APIKey: "",
218 APIEndpoint: "",
219 Models: []provider.Model{{
220 ID: "some-random-model",
221 }},
222 },
223 }
224
225 cfg := &Config{}
226 cfg.setDefaults("/tmp")
227 env := env.NewFromMap(map[string]string{
228 "AWS_ACCESS_KEY_ID": "test-key-id",
229 "AWS_SECRET_ACCESS_KEY": "test-secret-key",
230 })
231 resolver := NewEnvironmentVariableResolver(env)
232 err := cfg.configureProviders(env, resolver, knownProviders)
233 assert.Error(t, err)
234}
235
236func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
237 knownProviders := []provider.Provider{
238 {
239 ID: provider.InferenceProviderVertexAI,
240 APIKey: "",
241 APIEndpoint: "",
242 Models: []provider.Model{{
243 ID: "gemini-pro",
244 }},
245 },
246 }
247
248 cfg := &Config{}
249 cfg.setDefaults("/tmp")
250 env := env.NewFromMap(map[string]string{
251 "GOOGLE_GENAI_USE_VERTEXAI": "true",
252 "GOOGLE_CLOUD_PROJECT": "test-project",
253 "GOOGLE_CLOUD_LOCATION": "us-central1",
254 })
255 resolver := NewEnvironmentVariableResolver(env)
256 err := cfg.configureProviders(env, resolver, knownProviders)
257 assert.NoError(t, err)
258 assert.Len(t, cfg.Providers, 1)
259
260 vertexProvider, ok := cfg.Providers["vertexai"]
261 assert.True(t, ok, "VertexAI provider should be present")
262 assert.Len(t, vertexProvider.Models, 1)
263 assert.Equal(t, "gemini-pro", vertexProvider.Models[0].ID)
264 assert.Equal(t, "test-project", vertexProvider.ExtraParams["project"])
265 assert.Equal(t, "us-central1", vertexProvider.ExtraParams["location"])
266}
267
268func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
269 knownProviders := []provider.Provider{
270 {
271 ID: provider.InferenceProviderVertexAI,
272 APIKey: "",
273 APIEndpoint: "",
274 Models: []provider.Model{{
275 ID: "gemini-pro",
276 }},
277 },
278 }
279
280 cfg := &Config{}
281 cfg.setDefaults("/tmp")
282 env := env.NewFromMap(map[string]string{
283 "GOOGLE_GENAI_USE_VERTEXAI": "false",
284 "GOOGLE_CLOUD_PROJECT": "test-project",
285 "GOOGLE_CLOUD_LOCATION": "us-central1",
286 })
287 resolver := NewEnvironmentVariableResolver(env)
288 err := cfg.configureProviders(env, resolver, knownProviders)
289 assert.NoError(t, err)
290 // Provider should not be configured without proper credentials
291 assert.Len(t, cfg.Providers, 0)
292}
293
294func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
295 knownProviders := []provider.Provider{
296 {
297 ID: provider.InferenceProviderVertexAI,
298 APIKey: "",
299 APIEndpoint: "",
300 Models: []provider.Model{{
301 ID: "gemini-pro",
302 }},
303 },
304 }
305
306 cfg := &Config{}
307 cfg.setDefaults("/tmp")
308 env := env.NewFromMap(map[string]string{
309 "GOOGLE_GENAI_USE_VERTEXAI": "true",
310 "GOOGLE_CLOUD_LOCATION": "us-central1",
311 })
312 resolver := NewEnvironmentVariableResolver(env)
313 err := cfg.configureProviders(env, resolver, knownProviders)
314 assert.NoError(t, err)
315 // Provider should not be configured without project
316 assert.Len(t, cfg.Providers, 0)
317}