@@ -0,0 +1,316 @@
1+package config
2+
3+import (
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+
13+func 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+
27+func 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+
46+func 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+
72+func 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+
117+func 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+
161+func 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+
190+func 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+
212+func 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+
235+func 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+
267+func 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+
293+func 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+}