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	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}