load_test.go

  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}