1package config
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "sync"
8 "testing"
9
10 "github.com/charmbracelet/crush/internal/fur/provider"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13)
14
15func reset() {
16 // Clear all environment variables that could affect config
17 envVarsToUnset := []string{
18 // API Keys
19 "ANTHROPIC_API_KEY",
20 "OPENAI_API_KEY",
21 "GEMINI_API_KEY",
22 "XAI_API_KEY",
23 "OPENROUTER_API_KEY",
24 "LLAMA_API_KEY",
25
26 // Google Cloud / VertexAI
27 "GOOGLE_GENAI_USE_VERTEXAI",
28 "GOOGLE_CLOUD_PROJECT",
29 "GOOGLE_CLOUD_LOCATION",
30
31 // AWS Credentials
32 "AWS_ACCESS_KEY_ID",
33 "AWS_SECRET_ACCESS_KEY",
34 "AWS_REGION",
35 "AWS_DEFAULT_REGION",
36 "AWS_PROFILE",
37 "AWS_DEFAULT_PROFILE",
38 "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
39 "AWS_CONTAINER_CREDENTIALS_FULL_URI",
40
41 // Other
42 "CRUSH_DEV_DEBUG",
43 }
44
45 for _, envVar := range envVarsToUnset {
46 os.Unsetenv(envVar)
47 }
48
49 // Reset singleton
50 once = sync.Once{}
51 instance = nil
52 cwd = ""
53 testConfigDir = ""
54
55 // Enable mock providers for all tests to avoid API calls
56 UseMockProviders = true
57 ResetProviders()
58}
59
60// Core Configuration Loading Tests
61
62func TestInit_ValidWorkingDirectory(t *testing.T) {
63 reset()
64 testConfigDir = t.TempDir()
65 cwdDir := t.TempDir()
66
67 cfg, err := Init(cwdDir, false)
68
69 require.NoError(t, err)
70 assert.NotNil(t, cfg)
71 assert.Equal(t, cwdDir, WorkingDirectory())
72 assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
73 assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
74}
75
76func TestInit_WithDebugFlag(t *testing.T) {
77 reset()
78 testConfigDir = t.TempDir()
79 cwdDir := t.TempDir()
80
81 cfg, err := Init(cwdDir, true)
82
83 require.NoError(t, err)
84 assert.True(t, cfg.Options.Debug)
85}
86
87func TestInit_SingletonBehavior(t *testing.T) {
88 reset()
89 testConfigDir = t.TempDir()
90 cwdDir := t.TempDir()
91
92 cfg1, err1 := Init(cwdDir, false)
93 cfg2, err2 := Init(cwdDir, false)
94
95 require.NoError(t, err1)
96 require.NoError(t, err2)
97 assert.Same(t, cfg1, cfg2)
98}
99
100func TestGet_BeforeInitialization(t *testing.T) {
101 reset()
102
103 assert.Panics(t, func() {
104 Get()
105 })
106}
107
108func TestGet_AfterInitialization(t *testing.T) {
109 reset()
110 testConfigDir = t.TempDir()
111 cwdDir := t.TempDir()
112
113 cfg1, err := Init(cwdDir, false)
114 require.NoError(t, err)
115
116 cfg2 := Get()
117 assert.Same(t, cfg1, cfg2)
118}
119
120func TestLoadConfig_NoConfigFiles(t *testing.T) {
121 reset()
122 testConfigDir = t.TempDir()
123 cwdDir := t.TempDir()
124
125 cfg, err := Init(cwdDir, false)
126
127 require.NoError(t, err)
128 assert.Len(t, cfg.Providers, 0)
129 assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
130}
131
132func TestLoadConfig_OnlyGlobalConfig(t *testing.T) {
133 reset()
134 testConfigDir = t.TempDir()
135 cwdDir := t.TempDir()
136
137 globalConfig := Config{
138 Providers: map[provider.InferenceProvider]ProviderConfig{
139 provider.InferenceProviderOpenAI: {
140 ID: provider.InferenceProviderOpenAI,
141 APIKey: "test-key",
142 ProviderType: provider.TypeOpenAI,
143 DefaultLargeModel: "gpt-4",
144 DefaultSmallModel: "gpt-3.5-turbo",
145 Models: []Model{
146 {
147 ID: "gpt-4",
148 Name: "GPT-4",
149 CostPer1MIn: 30.0,
150 CostPer1MOut: 60.0,
151 ContextWindow: 8192,
152 DefaultMaxTokens: 4096,
153 },
154 {
155 ID: "gpt-3.5-turbo",
156 Name: "GPT-3.5 Turbo",
157 CostPer1MIn: 1.0,
158 CostPer1MOut: 2.0,
159 ContextWindow: 4096,
160 DefaultMaxTokens: 4096,
161 },
162 },
163 },
164 },
165 Options: Options{
166 ContextPaths: []string{"custom-context.md"},
167 },
168 }
169
170 configPath := filepath.Join(testConfigDir, "crush.json")
171 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
172
173 data, err := json.Marshal(globalConfig)
174 require.NoError(t, err)
175 require.NoError(t, os.WriteFile(configPath, data, 0o644))
176
177 cfg, err := Init(cwdDir, false)
178
179 require.NoError(t, err)
180 assert.Len(t, cfg.Providers, 1)
181 assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
182 assert.Contains(t, cfg.Options.ContextPaths, "custom-context.md")
183}
184
185func TestLoadConfig_OnlyLocalConfig(t *testing.T) {
186 reset()
187 testConfigDir = t.TempDir()
188 cwdDir := t.TempDir()
189
190 localConfig := Config{
191 Providers: map[provider.InferenceProvider]ProviderConfig{
192 provider.InferenceProviderAnthropic: {
193 ID: provider.InferenceProviderAnthropic,
194 APIKey: "local-key",
195 ProviderType: provider.TypeAnthropic,
196 DefaultLargeModel: "claude-3-opus",
197 DefaultSmallModel: "claude-3-haiku",
198 Models: []Model{
199 {
200 ID: "claude-3-opus",
201 Name: "Claude 3 Opus",
202 CostPer1MIn: 15.0,
203 CostPer1MOut: 75.0,
204 ContextWindow: 200000,
205 DefaultMaxTokens: 4096,
206 },
207 {
208 ID: "claude-3-haiku",
209 Name: "Claude 3 Haiku",
210 CostPer1MIn: 0.25,
211 CostPer1MOut: 1.25,
212 ContextWindow: 200000,
213 DefaultMaxTokens: 4096,
214 },
215 },
216 },
217 },
218 Options: Options{
219 TUI: TUIOptions{CompactMode: true},
220 },
221 }
222
223 localConfigPath := filepath.Join(cwdDir, "crush.json")
224 data, err := json.Marshal(localConfig)
225 require.NoError(t, err)
226 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
227
228 cfg, err := Init(cwdDir, false)
229
230 require.NoError(t, err)
231 assert.Len(t, cfg.Providers, 1)
232 assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
233 assert.True(t, cfg.Options.TUI.CompactMode)
234}
235
236func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
237 reset()
238 testConfigDir = t.TempDir()
239 cwdDir := t.TempDir()
240
241 globalConfig := Config{
242 Providers: map[provider.InferenceProvider]ProviderConfig{
243 provider.InferenceProviderOpenAI: {
244 ID: provider.InferenceProviderOpenAI,
245 APIKey: "global-key",
246 ProviderType: provider.TypeOpenAI,
247 DefaultLargeModel: "gpt-4",
248 DefaultSmallModel: "gpt-3.5-turbo",
249 Models: []Model{
250 {
251 ID: "gpt-4",
252 Name: "GPT-4",
253 CostPer1MIn: 30.0,
254 CostPer1MOut: 60.0,
255 ContextWindow: 8192,
256 DefaultMaxTokens: 4096,
257 },
258 {
259 ID: "gpt-3.5-turbo",
260 Name: "GPT-3.5 Turbo",
261 CostPer1MIn: 1.0,
262 CostPer1MOut: 2.0,
263 ContextWindow: 4096,
264 DefaultMaxTokens: 4096,
265 },
266 },
267 },
268 },
269 Options: Options{
270 ContextPaths: []string{"global-context.md"},
271 },
272 }
273
274 configPath := filepath.Join(testConfigDir, "crush.json")
275 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
276 data, err := json.Marshal(globalConfig)
277 require.NoError(t, err)
278 require.NoError(t, os.WriteFile(configPath, data, 0o644))
279
280 localConfig := Config{
281 Providers: map[provider.InferenceProvider]ProviderConfig{
282 provider.InferenceProviderOpenAI: {
283 APIKey: "local-key", // Override global
284 },
285 provider.InferenceProviderAnthropic: {
286 ID: provider.InferenceProviderAnthropic,
287 APIKey: "anthropic-key",
288 ProviderType: provider.TypeAnthropic,
289 DefaultLargeModel: "claude-3-opus",
290 DefaultSmallModel: "claude-3-haiku",
291 Models: []Model{
292 {
293 ID: "claude-3-opus",
294 Name: "Claude 3 Opus",
295 CostPer1MIn: 15.0,
296 CostPer1MOut: 75.0,
297 ContextWindow: 200000,
298 DefaultMaxTokens: 4096,
299 },
300 {
301 ID: "claude-3-haiku",
302 Name: "Claude 3 Haiku",
303 CostPer1MIn: 0.25,
304 CostPer1MOut: 1.25,
305 ContextWindow: 200000,
306 DefaultMaxTokens: 4096,
307 },
308 },
309 },
310 },
311 Options: Options{
312 ContextPaths: []string{"local-context.md"},
313 TUI: TUIOptions{CompactMode: true},
314 },
315 }
316
317 localConfigPath := filepath.Join(cwdDir, "crush.json")
318 data, err = json.Marshal(localConfig)
319 require.NoError(t, err)
320 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
321
322 cfg, err := Init(cwdDir, false)
323
324 require.NoError(t, err)
325 assert.Len(t, cfg.Providers, 2)
326
327 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
328 assert.Equal(t, "local-key", openaiProvider.APIKey)
329
330 assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
331
332 assert.Contains(t, cfg.Options.ContextPaths, "global-context.md")
333 assert.Contains(t, cfg.Options.ContextPaths, "local-context.md")
334 assert.True(t, cfg.Options.TUI.CompactMode)
335}
336
337func TestLoadConfig_MalformedGlobalJSON(t *testing.T) {
338 reset()
339 testConfigDir = t.TempDir()
340 cwdDir := t.TempDir()
341
342 configPath := filepath.Join(testConfigDir, "crush.json")
343 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
344 require.NoError(t, os.WriteFile(configPath, []byte(`{invalid json`), 0o644))
345
346 _, err := Init(cwdDir, false)
347 assert.Error(t, err)
348}
349
350func TestLoadConfig_MalformedLocalJSON(t *testing.T) {
351 reset()
352 testConfigDir = t.TempDir()
353 cwdDir := t.TempDir()
354
355 localConfigPath := filepath.Join(cwdDir, "crush.json")
356 require.NoError(t, os.WriteFile(localConfigPath, []byte(`{invalid json`), 0o644))
357
358 _, err := Init(cwdDir, false)
359 assert.Error(t, err)
360}
361
362func TestConfigWithoutEnv(t *testing.T) {
363 reset()
364 testConfigDir = t.TempDir()
365 cwdDir := t.TempDir()
366
367 cfg, _ := Init(cwdDir, false)
368 assert.Len(t, cfg.Providers, 0)
369}
370
371func TestConfigWithEnv(t *testing.T) {
372 reset()
373 testConfigDir = t.TempDir()
374 cwdDir := t.TempDir()
375
376 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
377 os.Setenv("OPENAI_API_KEY", "test-openai-key")
378 os.Setenv("GEMINI_API_KEY", "test-gemini-key")
379 os.Setenv("XAI_API_KEY", "test-xai-key")
380 os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
381
382 cfg, _ := Init(cwdDir, false)
383 assert.Len(t, cfg.Providers, 5)
384}
385
386// Environment Variable Tests
387
388func TestEnvVars_NoEnvironmentVariables(t *testing.T) {
389 reset()
390 testConfigDir = t.TempDir()
391 cwdDir := t.TempDir()
392
393 cfg, err := Init(cwdDir, false)
394
395 require.NoError(t, err)
396 assert.Len(t, cfg.Providers, 0)
397}
398
399func TestEnvVars_AllSupportedAPIKeys(t *testing.T) {
400 reset()
401 testConfigDir = t.TempDir()
402 cwdDir := t.TempDir()
403
404 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
405 os.Setenv("OPENAI_API_KEY", "test-openai-key")
406 os.Setenv("GEMINI_API_KEY", "test-gemini-key")
407 os.Setenv("XAI_API_KEY", "test-xai-key")
408 os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
409 os.Setenv("LLAMA_API_KEY", "test-llama-key")
410
411 cfg, err := Init(cwdDir, false)
412
413 require.NoError(t, err)
414 assert.Len(t, cfg.Providers, 6)
415
416 anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
417 assert.Equal(t, "test-anthropic-key", anthropicProvider.APIKey)
418 assert.Equal(t, provider.TypeAnthropic, anthropicProvider.ProviderType)
419
420 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
421 assert.Equal(t, "test-openai-key", openaiProvider.APIKey)
422 assert.Equal(t, provider.TypeOpenAI, openaiProvider.ProviderType)
423
424 geminiProvider := cfg.Providers[provider.InferenceProviderGemini]
425 assert.Equal(t, "test-gemini-key", geminiProvider.APIKey)
426 assert.Equal(t, provider.TypeGemini, geminiProvider.ProviderType)
427
428 xaiProvider := cfg.Providers[provider.InferenceProviderXAI]
429 assert.Equal(t, "test-xai-key", xaiProvider.APIKey)
430 assert.Equal(t, provider.TypeXAI, xaiProvider.ProviderType)
431
432 openrouterProvider := cfg.Providers[provider.InferenceProviderOpenRouter]
433 assert.Equal(t, "test-openrouter-key", openrouterProvider.APIKey)
434 assert.Equal(t, provider.TypeOpenAI, openrouterProvider.ProviderType)
435 assert.Equal(t, "https://openrouter.ai/api/v1", openrouterProvider.BaseURL)
436
437 llamaProvider := cfg.Providers[provider.InferenceProviderLlama]
438 assert.Equal(t, "test-llama-key", llamaProvider.APIKey)
439 assert.Equal(t, provider.TypeLlama, llamaProvider.ProviderType)
440 assert.Equal(t, "https://api.llama.com/compat/v1", llamaProvider.BaseURL)
441}
442
443func TestEnvVars_PartialEnvironmentVariables(t *testing.T) {
444 reset()
445 testConfigDir = t.TempDir()
446 cwdDir := t.TempDir()
447
448 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
449 os.Setenv("OPENAI_API_KEY", "test-openai-key")
450
451 cfg, err := Init(cwdDir, false)
452
453 require.NoError(t, err)
454 assert.Len(t, cfg.Providers, 2)
455 assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
456 assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
457 assert.NotContains(t, cfg.Providers, provider.InferenceProviderGemini)
458}
459
460func TestEnvVars_VertexAIConfiguration(t *testing.T) {
461 reset()
462 testConfigDir = t.TempDir()
463 cwdDir := t.TempDir()
464
465 os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
466 os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
467 os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
468
469 cfg, err := Init(cwdDir, false)
470
471 require.NoError(t, err)
472 assert.Contains(t, cfg.Providers, provider.InferenceProviderVertexAI)
473
474 vertexProvider := cfg.Providers[provider.InferenceProviderVertexAI]
475 assert.Equal(t, provider.TypeVertexAI, vertexProvider.ProviderType)
476 assert.Equal(t, "test-project", vertexProvider.ExtraParams["project"])
477 assert.Equal(t, "us-central1", vertexProvider.ExtraParams["location"])
478}
479
480func TestEnvVars_VertexAIWithoutUseFlag(t *testing.T) {
481 reset()
482 testConfigDir = t.TempDir()
483 cwdDir := t.TempDir()
484
485 os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
486 os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
487
488 cfg, err := Init(cwdDir, false)
489
490 require.NoError(t, err)
491 assert.NotContains(t, cfg.Providers, provider.InferenceProviderVertexAI)
492}
493
494func TestEnvVars_AWSBedrockWithAccessKeys(t *testing.T) {
495 reset()
496 testConfigDir = t.TempDir()
497 cwdDir := t.TempDir()
498
499 os.Setenv("AWS_ACCESS_KEY_ID", "test-access-key")
500 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
501 os.Setenv("AWS_DEFAULT_REGION", "us-east-1")
502
503 cfg, err := Init(cwdDir, false)
504
505 require.NoError(t, err)
506 assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
507
508 bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
509 assert.Equal(t, provider.TypeBedrock, bedrockProvider.ProviderType)
510 assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
511}
512
513func TestEnvVars_AWSBedrockWithProfile(t *testing.T) {
514 reset()
515 testConfigDir = t.TempDir()
516 cwdDir := t.TempDir()
517
518 os.Setenv("AWS_PROFILE", "test-profile")
519 os.Setenv("AWS_REGION", "eu-west-1")
520
521 cfg, err := Init(cwdDir, false)
522
523 require.NoError(t, err)
524 assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
525
526 bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
527 assert.Equal(t, "eu-west-1", bedrockProvider.ExtraParams["region"])
528}
529
530func TestEnvVars_AWSBedrockWithContainerCredentials(t *testing.T) {
531 reset()
532 testConfigDir = t.TempDir()
533 cwdDir := t.TempDir()
534
535 os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
536 os.Setenv("AWS_DEFAULT_REGION", "ap-southeast-1")
537
538 cfg, err := Init(cwdDir, false)
539
540 require.NoError(t, err)
541 assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
542}
543
544func TestEnvVars_AWSBedrockRegionPriority(t *testing.T) {
545 reset()
546 testConfigDir = t.TempDir()
547 cwdDir := t.TempDir()
548
549 os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
550 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
551 os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
552 os.Setenv("AWS_REGION", "us-east-1")
553
554 cfg, err := Init(cwdDir, false)
555
556 require.NoError(t, err)
557 bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
558 assert.Equal(t, "us-west-2", bedrockProvider.ExtraParams["region"])
559}
560
561func TestEnvVars_AWSBedrockFallbackRegion(t *testing.T) {
562 reset()
563 testConfigDir = t.TempDir()
564 cwdDir := t.TempDir()
565
566 os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
567 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
568 os.Setenv("AWS_REGION", "us-east-1")
569
570 cfg, err := Init(cwdDir, false)
571
572 require.NoError(t, err)
573 bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
574 assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
575}
576
577func TestEnvVars_NoAWSCredentials(t *testing.T) {
578 reset()
579 testConfigDir = t.TempDir()
580 cwdDir := t.TempDir()
581
582 cfg, err := Init(cwdDir, false)
583
584 require.NoError(t, err)
585 assert.NotContains(t, cfg.Providers, provider.InferenceProviderBedrock)
586}
587
588func TestEnvVars_CustomEnvironmentVariables(t *testing.T) {
589 reset()
590 testConfigDir = t.TempDir()
591 cwdDir := t.TempDir()
592
593 os.Setenv("ANTHROPIC_API_KEY", "resolved-anthropic-key")
594
595 cfg, err := Init(cwdDir, false)
596
597 require.NoError(t, err)
598 if len(cfg.Providers) > 0 {
599 if anthropicProvider, exists := cfg.Providers[provider.InferenceProviderAnthropic]; exists {
600 assert.Equal(t, "resolved-anthropic-key", anthropicProvider.APIKey)
601 }
602 }
603}
604
605func TestEnvVars_CombinedEnvironmentVariables(t *testing.T) {
606 reset()
607 testConfigDir = t.TempDir()
608 cwdDir := t.TempDir()
609
610 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic")
611 os.Setenv("OPENAI_API_KEY", "test-openai")
612 os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
613 os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
614 os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
615 os.Setenv("AWS_ACCESS_KEY_ID", "test-aws-key")
616 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-aws-secret")
617 os.Setenv("AWS_DEFAULT_REGION", "us-west-1")
618
619 cfg, err := Init(cwdDir, false)
620
621 require.NoError(t, err)
622
623 expectedProviders := []provider.InferenceProvider{
624 provider.InferenceProviderAnthropic,
625 provider.InferenceProviderOpenAI,
626 provider.InferenceProviderVertexAI,
627 provider.InferenceProviderBedrock,
628 }
629
630 for _, expectedProvider := range expectedProviders {
631 assert.Contains(t, cfg.Providers, expectedProvider)
632 }
633}
634
635func TestHasAWSCredentials_AccessKeys(t *testing.T) {
636 reset()
637
638 os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
639 os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
640
641 assert.True(t, hasAWSCredentials())
642}
643
644func TestHasAWSCredentials_Profile(t *testing.T) {
645 reset()
646
647 os.Setenv("AWS_PROFILE", "test-profile")
648
649 assert.True(t, hasAWSCredentials())
650}
651
652func TestHasAWSCredentials_DefaultProfile(t *testing.T) {
653 reset()
654
655 os.Setenv("AWS_DEFAULT_PROFILE", "default")
656
657 assert.True(t, hasAWSCredentials())
658}
659
660func TestHasAWSCredentials_Region(t *testing.T) {
661 reset()
662
663 os.Setenv("AWS_REGION", "us-east-1")
664
665 assert.True(t, hasAWSCredentials())
666}
667
668func TestHasAWSCredentials_ContainerCredentials(t *testing.T) {
669 reset()
670
671 os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
672
673 assert.True(t, hasAWSCredentials())
674}
675
676func TestHasAWSCredentials_NoCredentials(t *testing.T) {
677 reset()
678
679 assert.False(t, hasAWSCredentials())
680}
681
682func TestProviderMerging_GlobalToBase(t *testing.T) {
683 reset()
684 testConfigDir = t.TempDir()
685 cwdDir := t.TempDir()
686
687 globalConfig := Config{
688 Providers: map[provider.InferenceProvider]ProviderConfig{
689 provider.InferenceProviderOpenAI: {
690 ID: provider.InferenceProviderOpenAI,
691 APIKey: "global-openai-key",
692 ProviderType: provider.TypeOpenAI,
693 DefaultLargeModel: "gpt-4",
694 DefaultSmallModel: "gpt-3.5-turbo",
695 Models: []Model{
696 {
697 ID: "gpt-4",
698 Name: "GPT-4",
699 ContextWindow: 8192,
700 DefaultMaxTokens: 4096,
701 },
702 {
703 ID: "gpt-3.5-turbo",
704 Name: "GPT-3.5 Turbo",
705 ContextWindow: 4096,
706 DefaultMaxTokens: 2048,
707 },
708 },
709 },
710 },
711 }
712
713 configPath := filepath.Join(testConfigDir, "crush.json")
714 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
715 data, err := json.Marshal(globalConfig)
716 require.NoError(t, err)
717 require.NoError(t, os.WriteFile(configPath, data, 0o644))
718
719 cfg, err := Init(cwdDir, false)
720
721 require.NoError(t, err)
722 assert.Len(t, cfg.Providers, 1)
723
724 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
725 assert.Equal(t, "global-openai-key", openaiProvider.APIKey)
726 assert.Equal(t, "gpt-4", openaiProvider.DefaultLargeModel)
727 assert.Equal(t, "gpt-4o", openaiProvider.DefaultSmallModel)
728 assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
729}
730
731func TestProviderMerging_LocalToBase(t *testing.T) {
732 reset()
733 testConfigDir = t.TempDir()
734 cwdDir := t.TempDir()
735
736 localConfig := Config{
737 Providers: map[provider.InferenceProvider]ProviderConfig{
738 provider.InferenceProviderAnthropic: {
739 ID: provider.InferenceProviderAnthropic,
740 APIKey: "local-anthropic-key",
741 ProviderType: provider.TypeAnthropic,
742 DefaultLargeModel: "claude-3-opus",
743 DefaultSmallModel: "claude-3-haiku",
744 Models: []Model{
745 {
746 ID: "claude-3-opus",
747 Name: "Claude 3 Opus",
748 ContextWindow: 200000,
749 DefaultMaxTokens: 4096,
750 CostPer1MIn: 15.0,
751 CostPer1MOut: 75.0,
752 },
753 {
754 ID: "claude-3-haiku",
755 Name: "Claude 3 Haiku",
756 ContextWindow: 200000,
757 DefaultMaxTokens: 4096,
758 CostPer1MIn: 0.25,
759 CostPer1MOut: 1.25,
760 },
761 },
762 },
763 },
764 }
765
766 localConfigPath := filepath.Join(cwdDir, "crush.json")
767 data, err := json.Marshal(localConfig)
768 require.NoError(t, err)
769 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
770
771 cfg, err := Init(cwdDir, false)
772
773 require.NoError(t, err)
774 assert.Len(t, cfg.Providers, 1)
775
776 anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
777 assert.Equal(t, "local-anthropic-key", anthropicProvider.APIKey)
778 assert.Equal(t, "claude-3-opus", anthropicProvider.DefaultLargeModel)
779 assert.Equal(t, "claude-3-5-haiku-20241022", anthropicProvider.DefaultSmallModel)
780 assert.GreaterOrEqual(t, len(anthropicProvider.Models), 2)
781}
782
783func TestProviderMerging_ConflictingSettings(t *testing.T) {
784 reset()
785 testConfigDir = t.TempDir()
786 cwdDir := t.TempDir()
787
788 globalConfig := Config{
789 Providers: map[provider.InferenceProvider]ProviderConfig{
790 provider.InferenceProviderOpenAI: {
791 ID: provider.InferenceProviderOpenAI,
792 APIKey: "global-key",
793 ProviderType: provider.TypeOpenAI,
794 DefaultLargeModel: "gpt-4",
795 DefaultSmallModel: "gpt-3.5-turbo",
796 Models: []Model{
797 {
798 ID: "gpt-4",
799 Name: "GPT-4",
800 ContextWindow: 8192,
801 DefaultMaxTokens: 4096,
802 },
803 {
804 ID: "gpt-3.5-turbo",
805 Name: "GPT-3.5 Turbo",
806 ContextWindow: 4096,
807 DefaultMaxTokens: 2048,
808 },
809 {
810 ID: "gpt-4-turbo",
811 Name: "GPT-4 Turbo",
812 ContextWindow: 128000,
813 DefaultMaxTokens: 4096,
814 },
815 },
816 },
817 },
818 }
819
820 configPath := filepath.Join(testConfigDir, "crush.json")
821 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
822 data, err := json.Marshal(globalConfig)
823 require.NoError(t, err)
824 require.NoError(t, os.WriteFile(configPath, data, 0o644))
825
826 // Create local config that overrides
827 localConfig := Config{
828 Providers: map[provider.InferenceProvider]ProviderConfig{
829 provider.InferenceProviderOpenAI: {
830 APIKey: "local-key",
831 DefaultLargeModel: "gpt-4-turbo",
832 },
833 },
834 }
835
836 localConfigPath := filepath.Join(cwdDir, "crush.json")
837 data, err = json.Marshal(localConfig)
838 require.NoError(t, err)
839 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
840
841 cfg, err := Init(cwdDir, false)
842
843 require.NoError(t, err)
844
845 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
846 assert.Equal(t, "local-key", openaiProvider.APIKey)
847 assert.Equal(t, "gpt-4-turbo", openaiProvider.DefaultLargeModel)
848 assert.False(t, openaiProvider.Disabled)
849 assert.Equal(t, "gpt-4o", openaiProvider.DefaultSmallModel)
850}
851
852func TestProviderMerging_CustomVsKnownProviders(t *testing.T) {
853 reset()
854 testConfigDir = t.TempDir()
855 cwdDir := t.TempDir()
856
857 customProviderID := provider.InferenceProvider("custom-provider")
858
859 globalConfig := Config{
860 Providers: map[provider.InferenceProvider]ProviderConfig{
861 provider.InferenceProviderOpenAI: {
862 ID: provider.InferenceProviderOpenAI,
863 APIKey: "openai-key",
864 BaseURL: "should-not-override",
865 ProviderType: provider.TypeAnthropic,
866 DefaultLargeModel: "gpt-4",
867 DefaultSmallModel: "gpt-3.5-turbo",
868 Models: []Model{
869 {
870 ID: "gpt-4",
871 Name: "GPT-4",
872 ContextWindow: 8192,
873 DefaultMaxTokens: 4096,
874 },
875 {
876 ID: "gpt-3.5-turbo",
877 Name: "GPT-3.5 Turbo",
878 ContextWindow: 4096,
879 DefaultMaxTokens: 2048,
880 },
881 },
882 },
883 customProviderID: {
884 ID: customProviderID,
885 APIKey: "custom-key",
886 BaseURL: "https://custom.api.com",
887 ProviderType: provider.TypeOpenAI,
888 DefaultLargeModel: "custom-large",
889 DefaultSmallModel: "custom-small",
890 Models: []Model{
891 {
892 ID: "custom-large",
893 Name: "Custom Large",
894 ContextWindow: 8192,
895 DefaultMaxTokens: 4096,
896 },
897 {
898 ID: "custom-small",
899 Name: "Custom Small",
900 ContextWindow: 4096,
901 DefaultMaxTokens: 2048,
902 },
903 },
904 },
905 },
906 }
907
908 localConfig := Config{
909 Providers: map[provider.InferenceProvider]ProviderConfig{
910 provider.InferenceProviderOpenAI: {
911 BaseURL: "https://should-not-change.com",
912 ProviderType: provider.TypeGemini, // Should not change
913 },
914 customProviderID: {
915 BaseURL: "https://updated-custom.api.com",
916 ProviderType: provider.TypeOpenAI,
917 },
918 },
919 }
920
921 configPath := filepath.Join(testConfigDir, "crush.json")
922 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
923 data, err := json.Marshal(globalConfig)
924 require.NoError(t, err)
925 require.NoError(t, os.WriteFile(configPath, data, 0o644))
926
927 localConfigPath := filepath.Join(cwdDir, "crush.json")
928 data, err = json.Marshal(localConfig)
929 require.NoError(t, err)
930 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
931
932 cfg, err := Init(cwdDir, false)
933
934 require.NoError(t, err)
935
936 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
937 assert.NotEqual(t, "https://should-not-change.com", openaiProvider.BaseURL)
938 assert.NotEqual(t, provider.TypeGemini, openaiProvider.ProviderType)
939
940 customProvider := cfg.Providers[customProviderID]
941 assert.Equal(t, "custom-key", customProvider.APIKey)
942 assert.Equal(t, "https://updated-custom.api.com", customProvider.BaseURL)
943 assert.Equal(t, provider.TypeOpenAI, customProvider.ProviderType)
944}
945
946func TestProviderValidation_CustomProviderMissingBaseURL(t *testing.T) {
947 reset()
948 testConfigDir = t.TempDir()
949 cwdDir := t.TempDir()
950
951 customProviderID := provider.InferenceProvider("custom-provider")
952
953 globalConfig := Config{
954 Providers: map[provider.InferenceProvider]ProviderConfig{
955 customProviderID: {
956 ID: customProviderID,
957 APIKey: "custom-key",
958 ProviderType: provider.TypeOpenAI,
959 },
960 },
961 }
962
963 configPath := filepath.Join(testConfigDir, "crush.json")
964 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
965 data, err := json.Marshal(globalConfig)
966 require.NoError(t, err)
967 require.NoError(t, os.WriteFile(configPath, data, 0o644))
968
969 cfg, err := Init(cwdDir, false)
970
971 require.NoError(t, err)
972 assert.NotContains(t, cfg.Providers, customProviderID)
973}
974
975func TestProviderValidation_CustomProviderMissingAPIKey(t *testing.T) {
976 reset()
977 testConfigDir = t.TempDir()
978 cwdDir := t.TempDir()
979
980 customProviderID := provider.InferenceProvider("custom-provider")
981
982 globalConfig := Config{
983 Providers: map[provider.InferenceProvider]ProviderConfig{
984 customProviderID: {
985 ID: customProviderID,
986 BaseURL: "https://custom.api.com",
987 ProviderType: provider.TypeOpenAI,
988 },
989 },
990 }
991
992 configPath := filepath.Join(testConfigDir, "crush.json")
993 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
994 data, err := json.Marshal(globalConfig)
995 require.NoError(t, err)
996 require.NoError(t, os.WriteFile(configPath, data, 0o644))
997
998 cfg, err := Init(cwdDir, false)
999
1000 require.NoError(t, err)
1001 assert.NotContains(t, cfg.Providers, customProviderID)
1002}
1003
1004func TestProviderValidation_CustomProviderInvalidType(t *testing.T) {
1005 reset()
1006 testConfigDir = t.TempDir()
1007 cwdDir := t.TempDir()
1008
1009 customProviderID := provider.InferenceProvider("custom-provider")
1010
1011 globalConfig := Config{
1012 Providers: map[provider.InferenceProvider]ProviderConfig{
1013 customProviderID: {
1014 ID: customProviderID,
1015 APIKey: "custom-key",
1016 BaseURL: "https://custom.api.com",
1017 ProviderType: provider.Type("invalid-type"),
1018 },
1019 },
1020 }
1021
1022 configPath := filepath.Join(testConfigDir, "crush.json")
1023 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1024 data, err := json.Marshal(globalConfig)
1025 require.NoError(t, err)
1026 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1027
1028 cfg, err := Init(cwdDir, false)
1029
1030 require.NoError(t, err)
1031 assert.NotContains(t, cfg.Providers, customProviderID)
1032}
1033
1034func TestProviderValidation_KnownProviderValid(t *testing.T) {
1035 reset()
1036 testConfigDir = t.TempDir()
1037 cwdDir := t.TempDir()
1038
1039 globalConfig := Config{
1040 Providers: map[provider.InferenceProvider]ProviderConfig{
1041 provider.InferenceProviderOpenAI: {
1042 ID: provider.InferenceProviderOpenAI,
1043 APIKey: "openai-key",
1044 ProviderType: provider.TypeOpenAI,
1045 DefaultLargeModel: "gpt-4",
1046 DefaultSmallModel: "gpt-3.5-turbo",
1047 Models: []Model{
1048 {
1049 ID: "gpt-4",
1050 Name: "GPT-4",
1051 ContextWindow: 8192,
1052 DefaultMaxTokens: 4096,
1053 },
1054 {
1055 ID: "gpt-3.5-turbo",
1056 Name: "GPT-3.5 Turbo",
1057 ContextWindow: 4096,
1058 DefaultMaxTokens: 2048,
1059 },
1060 },
1061 },
1062 },
1063 }
1064
1065 configPath := filepath.Join(testConfigDir, "crush.json")
1066 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1067 data, err := json.Marshal(globalConfig)
1068 require.NoError(t, err)
1069 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1070
1071 cfg, err := Init(cwdDir, false)
1072
1073 require.NoError(t, err)
1074 assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1075}
1076
1077func TestProviderValidation_DisabledProvider(t *testing.T) {
1078 reset()
1079 testConfigDir = t.TempDir()
1080 cwdDir := t.TempDir()
1081
1082 globalConfig := Config{
1083 Providers: map[provider.InferenceProvider]ProviderConfig{
1084 provider.InferenceProviderOpenAI: {
1085 ID: provider.InferenceProviderOpenAI,
1086 APIKey: "openai-key",
1087 ProviderType: provider.TypeOpenAI,
1088 Disabled: true,
1089 DefaultLargeModel: "gpt-4",
1090 DefaultSmallModel: "gpt-3.5-turbo",
1091 Models: []Model{
1092 {
1093 ID: "gpt-4",
1094 Name: "GPT-4",
1095 ContextWindow: 8192,
1096 DefaultMaxTokens: 4096,
1097 },
1098 {
1099 ID: "gpt-3.5-turbo",
1100 Name: "GPT-3.5 Turbo",
1101 ContextWindow: 4096,
1102 DefaultMaxTokens: 2048,
1103 },
1104 },
1105 },
1106 provider.InferenceProviderAnthropic: {
1107 ID: provider.InferenceProviderAnthropic,
1108 APIKey: "anthropic-key",
1109 ProviderType: provider.TypeAnthropic,
1110 Disabled: false, // This one is enabled
1111 DefaultLargeModel: "claude-3-opus",
1112 DefaultSmallModel: "claude-3-haiku",
1113 Models: []Model{
1114 {
1115 ID: "claude-3-opus",
1116 Name: "Claude 3 Opus",
1117 ContextWindow: 200000,
1118 DefaultMaxTokens: 4096,
1119 },
1120 {
1121 ID: "claude-3-haiku",
1122 Name: "Claude 3 Haiku",
1123 ContextWindow: 200000,
1124 DefaultMaxTokens: 4096,
1125 },
1126 },
1127 },
1128 },
1129 }
1130
1131 configPath := filepath.Join(testConfigDir, "crush.json")
1132 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1133 data, err := json.Marshal(globalConfig)
1134 require.NoError(t, err)
1135 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1136
1137 cfg, err := Init(cwdDir, false)
1138
1139 require.NoError(t, err)
1140 assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1141 assert.True(t, cfg.Providers[provider.InferenceProviderOpenAI].Disabled)
1142 assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
1143 assert.False(t, cfg.Providers[provider.InferenceProviderAnthropic].Disabled)
1144}
1145
1146func TestProviderModels_AddingNewModels(t *testing.T) {
1147 reset()
1148 testConfigDir = t.TempDir()
1149 cwdDir := t.TempDir()
1150
1151 globalConfig := Config{
1152 Providers: map[provider.InferenceProvider]ProviderConfig{
1153 provider.InferenceProviderOpenAI: {
1154 ID: provider.InferenceProviderOpenAI,
1155 APIKey: "openai-key",
1156 ProviderType: provider.TypeOpenAI,
1157 DefaultLargeModel: "gpt-4",
1158 DefaultSmallModel: "gpt-4-turbo",
1159 Models: []Model{
1160 {
1161 ID: "gpt-4",
1162 Name: "GPT-4",
1163 ContextWindow: 8192,
1164 DefaultMaxTokens: 4096,
1165 },
1166 },
1167 },
1168 },
1169 }
1170
1171 localConfig := Config{
1172 Providers: map[provider.InferenceProvider]ProviderConfig{
1173 provider.InferenceProviderOpenAI: {
1174 Models: []Model{
1175 {
1176 ID: "gpt-4-turbo",
1177 Name: "GPT-4 Turbo",
1178 ContextWindow: 128000,
1179 DefaultMaxTokens: 4096,
1180 },
1181 },
1182 },
1183 },
1184 }
1185
1186 configPath := filepath.Join(testConfigDir, "crush.json")
1187 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1188 data, err := json.Marshal(globalConfig)
1189 require.NoError(t, err)
1190 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1191
1192 localConfigPath := filepath.Join(cwdDir, "crush.json")
1193 data, err = json.Marshal(localConfig)
1194 require.NoError(t, err)
1195 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1196
1197 cfg, err := Init(cwdDir, false)
1198
1199 require.NoError(t, err)
1200
1201 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1202 assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
1203
1204 modelIDs := make([]string, len(openaiProvider.Models))
1205 for i, model := range openaiProvider.Models {
1206 modelIDs[i] = model.ID
1207 }
1208 assert.Contains(t, modelIDs, "gpt-4")
1209 assert.Contains(t, modelIDs, "gpt-4-turbo")
1210}
1211
1212func TestProviderModels_DuplicateModelHandling(t *testing.T) {
1213 reset()
1214 testConfigDir = t.TempDir()
1215 cwdDir := t.TempDir()
1216
1217 globalConfig := Config{
1218 Providers: map[provider.InferenceProvider]ProviderConfig{
1219 provider.InferenceProviderOpenAI: {
1220 ID: provider.InferenceProviderOpenAI,
1221 APIKey: "openai-key",
1222 ProviderType: provider.TypeOpenAI,
1223 DefaultLargeModel: "gpt-4",
1224 DefaultSmallModel: "gpt-4",
1225 Models: []Model{
1226 {
1227 ID: "gpt-4",
1228 Name: "GPT-4",
1229 ContextWindow: 8192,
1230 DefaultMaxTokens: 4096,
1231 },
1232 },
1233 },
1234 },
1235 }
1236
1237 localConfig := Config{
1238 Providers: map[provider.InferenceProvider]ProviderConfig{
1239 provider.InferenceProviderOpenAI: {
1240 Models: []Model{
1241 {
1242 ID: "gpt-4",
1243 Name: "GPT-4 Updated",
1244 ContextWindow: 16384,
1245 DefaultMaxTokens: 8192,
1246 },
1247 },
1248 },
1249 },
1250 }
1251
1252 configPath := filepath.Join(testConfigDir, "crush.json")
1253 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1254 data, err := json.Marshal(globalConfig)
1255 require.NoError(t, err)
1256 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1257
1258 localConfigPath := filepath.Join(cwdDir, "crush.json")
1259 data, err = json.Marshal(localConfig)
1260 require.NoError(t, err)
1261 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1262
1263 cfg, err := Init(cwdDir, false)
1264
1265 require.NoError(t, err)
1266
1267 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1268 assert.GreaterOrEqual(t, len(openaiProvider.Models), 1)
1269
1270 // Find the first model that matches our test data
1271 var testModel *Model
1272 for _, model := range openaiProvider.Models {
1273 if model.ID == "gpt-4" {
1274 testModel = &model
1275 break
1276 }
1277 }
1278
1279 // If gpt-4 not found, use the first available model
1280 if testModel == nil {
1281 testModel = &openaiProvider.Models[0]
1282 }
1283
1284 assert.NotEmpty(t, testModel.ID)
1285 assert.NotEmpty(t, testModel.Name)
1286 assert.Greater(t, testModel.ContextWindow, int64(0))
1287}
1288
1289func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
1290 reset()
1291 testConfigDir = t.TempDir()
1292 cwdDir := t.TempDir()
1293
1294 globalConfig := Config{
1295 Providers: map[provider.InferenceProvider]ProviderConfig{
1296 provider.InferenceProviderOpenAI: {
1297 ID: provider.InferenceProviderOpenAI,
1298 APIKey: "openai-key",
1299 ProviderType: provider.TypeOpenAI,
1300 DefaultLargeModel: "gpt-4",
1301 DefaultSmallModel: "gpt-4",
1302 Models: []Model{
1303 {
1304 ID: "gpt-4",
1305 Name: "GPT-4",
1306 CostPer1MIn: 30.0,
1307 CostPer1MOut: 60.0,
1308 CostPer1MInCached: 15.0,
1309 CostPer1MOutCached: 30.0,
1310 ContextWindow: 8192,
1311 DefaultMaxTokens: 4096,
1312 CanReason: true,
1313 ReasoningEffort: "medium",
1314 SupportsImages: true,
1315 },
1316 },
1317 },
1318 },
1319 }
1320
1321 configPath := filepath.Join(testConfigDir, "crush.json")
1322 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1323 data, err := json.Marshal(globalConfig)
1324 require.NoError(t, err)
1325 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1326
1327 cfg, err := Init(cwdDir, false)
1328
1329 require.NoError(t, err)
1330
1331 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1332 require.GreaterOrEqual(t, len(openaiProvider.Models), 1)
1333
1334 // Find the test model or use the first one
1335 var testModel *Model
1336 for _, model := range openaiProvider.Models {
1337 if model.ID == "gpt-4" {
1338 testModel = &model
1339 break
1340 }
1341 }
1342
1343 if testModel == nil {
1344 testModel = &openaiProvider.Models[0]
1345 }
1346
1347 // Only test the custom properties if this is actually our test model
1348 if testModel.ID == "gpt-4" {
1349 assert.Equal(t, 30.0, testModel.CostPer1MIn)
1350 assert.Equal(t, 60.0, testModel.CostPer1MOut)
1351 assert.Equal(t, 15.0, testModel.CostPer1MInCached)
1352 assert.Equal(t, 30.0, testModel.CostPer1MOutCached)
1353 assert.True(t, testModel.CanReason)
1354 assert.Equal(t, "medium", testModel.ReasoningEffort)
1355 assert.True(t, testModel.SupportsImages)
1356 }
1357}
1358
1359func TestDefaultAgents_CoderAgent(t *testing.T) {
1360 reset()
1361 testConfigDir = t.TempDir()
1362 cwdDir := t.TempDir()
1363
1364 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1365
1366 cfg, err := Init(cwdDir, false)
1367
1368 require.NoError(t, err)
1369 assert.Contains(t, cfg.Agents, AgentCoder)
1370
1371 coderAgent := cfg.Agents[AgentCoder]
1372 assert.Equal(t, AgentCoder, coderAgent.ID)
1373 assert.Equal(t, "Coder", coderAgent.Name)
1374 assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1375 assert.Equal(t, LargeModel, coderAgent.Model)
1376 assert.False(t, coderAgent.Disabled)
1377 assert.Equal(t, cfg.Options.ContextPaths, coderAgent.ContextPaths)
1378 assert.Nil(t, coderAgent.AllowedTools)
1379}
1380
1381func TestDefaultAgents_TaskAgent(t *testing.T) {
1382 reset()
1383 testConfigDir = t.TempDir()
1384 cwdDir := t.TempDir()
1385
1386 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1387
1388 cfg, err := Init(cwdDir, false)
1389
1390 require.NoError(t, err)
1391 assert.Contains(t, cfg.Agents, AgentTask)
1392
1393 taskAgent := cfg.Agents[AgentTask]
1394 assert.Equal(t, AgentTask, taskAgent.ID)
1395 assert.Equal(t, "Task", taskAgent.Name)
1396 assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1397 assert.Equal(t, LargeModel, taskAgent.Model)
1398 assert.False(t, taskAgent.Disabled)
1399 assert.Equal(t, cfg.Options.ContextPaths, taskAgent.ContextPaths)
1400
1401 expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
1402 assert.Equal(t, expectedTools, taskAgent.AllowedTools)
1403
1404 assert.Equal(t, map[string][]string{}, taskAgent.AllowedMCP)
1405 assert.Equal(t, []string{}, taskAgent.AllowedLSP)
1406}
1407
1408func TestAgentMerging_CustomAgent(t *testing.T) {
1409 reset()
1410 testConfigDir = t.TempDir()
1411 cwdDir := t.TempDir()
1412
1413 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1414
1415 globalConfig := Config{
1416 Agents: map[AgentID]Agent{
1417 AgentID("custom-agent"): {
1418 ID: AgentID("custom-agent"),
1419 Name: "Custom Agent",
1420 Description: "A custom agent for testing",
1421 Model: SmallModel,
1422 AllowedTools: []string{"glob", "grep"},
1423 AllowedMCP: map[string][]string{"mcp1": {"tool1", "tool2"}},
1424 AllowedLSP: []string{"typescript", "go"},
1425 ContextPaths: []string{"custom-context.md"},
1426 },
1427 },
1428 MCP: map[string]MCP{
1429 "mcp1": {
1430 Type: MCPStdio,
1431 Command: "test-mcp-command",
1432 Args: []string{"--test"},
1433 },
1434 },
1435 LSP: map[string]LSPConfig{
1436 "typescript": {
1437 Command: "typescript-language-server",
1438 Args: []string{"--stdio"},
1439 },
1440 "go": {
1441 Command: "gopls",
1442 Args: []string{},
1443 },
1444 },
1445 }
1446
1447 configPath := filepath.Join(testConfigDir, "crush.json")
1448 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1449 data, err := json.Marshal(globalConfig)
1450 require.NoError(t, err)
1451 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1452
1453 cfg, err := Init(cwdDir, false)
1454
1455 require.NoError(t, err)
1456
1457 assert.Contains(t, cfg.Agents, AgentCoder)
1458 assert.Contains(t, cfg.Agents, AgentTask)
1459 assert.Contains(t, cfg.Agents, AgentID("custom-agent"))
1460
1461 customAgent := cfg.Agents[AgentID("custom-agent")]
1462 assert.Equal(t, "Custom Agent", customAgent.Name)
1463 assert.Equal(t, "A custom agent for testing", customAgent.Description)
1464 assert.Equal(t, SmallModel, customAgent.Model)
1465 assert.Equal(t, []string{"glob", "grep"}, customAgent.AllowedTools)
1466 assert.Equal(t, map[string][]string{"mcp1": {"tool1", "tool2"}}, customAgent.AllowedMCP)
1467 assert.Equal(t, []string{"typescript", "go"}, customAgent.AllowedLSP)
1468 expectedContextPaths := append(defaultContextPaths, "custom-context.md")
1469 assert.Equal(t, expectedContextPaths, customAgent.ContextPaths)
1470}
1471
1472func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
1473 reset()
1474 testConfigDir = t.TempDir()
1475 cwdDir := t.TempDir()
1476
1477 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1478
1479 globalConfig := Config{
1480 Agents: map[AgentID]Agent{
1481 AgentCoder: {
1482 Model: SmallModel,
1483 AllowedMCP: map[string][]string{"mcp1": {"tool1"}},
1484 AllowedLSP: []string{"typescript"},
1485 ContextPaths: []string{"coder-specific.md"},
1486 },
1487 },
1488 MCP: map[string]MCP{
1489 "mcp1": {
1490 Type: MCPStdio,
1491 Command: "test-mcp-command",
1492 Args: []string{"--test"},
1493 },
1494 },
1495 LSP: map[string]LSPConfig{
1496 "typescript": {
1497 Command: "typescript-language-server",
1498 Args: []string{"--stdio"},
1499 },
1500 },
1501 }
1502
1503 configPath := filepath.Join(testConfigDir, "crush.json")
1504 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1505 data, err := json.Marshal(globalConfig)
1506 require.NoError(t, err)
1507 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1508
1509 cfg, err := Init(cwdDir, false)
1510
1511 require.NoError(t, err)
1512
1513 coderAgent := cfg.Agents[AgentCoder]
1514 assert.Equal(t, AgentCoder, coderAgent.ID)
1515 assert.Equal(t, "Coder", coderAgent.Name)
1516 assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1517
1518 expectedContextPaths := append(cfg.Options.ContextPaths, "coder-specific.md")
1519 assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1520
1521 assert.Equal(t, SmallModel, coderAgent.Model)
1522 assert.Equal(t, map[string][]string{"mcp1": {"tool1"}}, coderAgent.AllowedMCP)
1523 assert.Equal(t, []string{"typescript"}, coderAgent.AllowedLSP)
1524}
1525
1526func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
1527 reset()
1528 testConfigDir = t.TempDir()
1529 cwdDir := t.TempDir()
1530
1531 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1532
1533 globalConfig := Config{
1534 Agents: map[AgentID]Agent{
1535 AgentTask: {
1536 Model: SmallModel,
1537 AllowedMCP: map[string][]string{"search-mcp": nil},
1538 AllowedLSP: []string{"python"},
1539 Name: "Search Agent",
1540 Description: "Custom search agent",
1541 Disabled: true,
1542 AllowedTools: []string{"glob", "grep", "view"},
1543 },
1544 },
1545 MCP: map[string]MCP{
1546 "search-mcp": {
1547 Type: MCPStdio,
1548 Command: "search-mcp-command",
1549 Args: []string{"--search"},
1550 },
1551 },
1552 LSP: map[string]LSPConfig{
1553 "python": {
1554 Command: "pylsp",
1555 Args: []string{},
1556 },
1557 },
1558 }
1559
1560 configPath := filepath.Join(testConfigDir, "crush.json")
1561 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1562 data, err := json.Marshal(globalConfig)
1563 require.NoError(t, err)
1564 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1565
1566 cfg, err := Init(cwdDir, false)
1567
1568 require.NoError(t, err)
1569
1570 taskAgent := cfg.Agents[AgentTask]
1571 assert.Equal(t, "Task", taskAgent.Name)
1572 assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1573 assert.False(t, taskAgent.Disabled)
1574 assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools)
1575
1576 assert.Equal(t, SmallModel, taskAgent.Model)
1577 assert.Equal(t, map[string][]string{"search-mcp": nil}, taskAgent.AllowedMCP)
1578 assert.Equal(t, []string{"python"}, taskAgent.AllowedLSP)
1579}
1580
1581func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
1582 reset()
1583 testConfigDir = t.TempDir()
1584 cwdDir := t.TempDir()
1585
1586 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1587
1588 globalConfig := Config{
1589 Agents: map[AgentID]Agent{
1590 AgentID("test-agent"): {
1591 ID: AgentID("test-agent"),
1592 Name: "Global Agent",
1593 Description: "Global description",
1594 Model: LargeModel,
1595 AllowedTools: []string{"glob"},
1596 },
1597 },
1598 }
1599
1600 configPath := filepath.Join(testConfigDir, "crush.json")
1601 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1602 data, err := json.Marshal(globalConfig)
1603 require.NoError(t, err)
1604 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1605
1606 // Create local config that overrides
1607 localConfig := Config{
1608 Agents: map[AgentID]Agent{
1609 AgentID("test-agent"): {
1610 Name: "Local Agent",
1611 Description: "Local description",
1612 Model: SmallModel,
1613 Disabled: true,
1614 AllowedTools: []string{"grep", "view"},
1615 AllowedMCP: map[string][]string{"local-mcp": {"tool1"}},
1616 },
1617 },
1618 MCP: map[string]MCP{
1619 "local-mcp": {
1620 Type: MCPStdio,
1621 Command: "local-mcp-command",
1622 Args: []string{"--local"},
1623 },
1624 },
1625 }
1626
1627 localConfigPath := filepath.Join(cwdDir, "crush.json")
1628 data, err = json.Marshal(localConfig)
1629 require.NoError(t, err)
1630 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1631
1632 cfg, err := Init(cwdDir, false)
1633
1634 require.NoError(t, err)
1635
1636 testAgent := cfg.Agents[AgentID("test-agent")]
1637 assert.Equal(t, "Local Agent", testAgent.Name)
1638 assert.Equal(t, "Local description", testAgent.Description)
1639 assert.Equal(t, SmallModel, testAgent.Model)
1640 assert.True(t, testAgent.Disabled)
1641 assert.Equal(t, []string{"grep", "view"}, testAgent.AllowedTools)
1642 assert.Equal(t, map[string][]string{"local-mcp": {"tool1"}}, testAgent.AllowedMCP)
1643}
1644
1645func TestAgentModelTypeAssignment(t *testing.T) {
1646 reset()
1647 testConfigDir = t.TempDir()
1648 cwdDir := t.TempDir()
1649
1650 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1651
1652 globalConfig := Config{
1653 Agents: map[AgentID]Agent{
1654 AgentID("large-agent"): {
1655 ID: AgentID("large-agent"),
1656 Name: "Large Model Agent",
1657 Model: LargeModel,
1658 },
1659 AgentID("small-agent"): {
1660 ID: AgentID("small-agent"),
1661 Name: "Small Model Agent",
1662 Model: SmallModel,
1663 },
1664 AgentID("default-agent"): {
1665 ID: AgentID("default-agent"),
1666 Name: "Default Model Agent",
1667 },
1668 },
1669 }
1670
1671 configPath := filepath.Join(testConfigDir, "crush.json")
1672 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1673 data, err := json.Marshal(globalConfig)
1674 require.NoError(t, err)
1675 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1676
1677 cfg, err := Init(cwdDir, false)
1678
1679 require.NoError(t, err)
1680
1681 assert.Equal(t, LargeModel, cfg.Agents[AgentID("large-agent")].Model)
1682 assert.Equal(t, SmallModel, cfg.Agents[AgentID("small-agent")].Model)
1683 assert.Equal(t, LargeModel, cfg.Agents[AgentID("default-agent")].Model)
1684}
1685
1686func TestAgentContextPathOverrides(t *testing.T) {
1687 reset()
1688 testConfigDir = t.TempDir()
1689 cwdDir := t.TempDir()
1690
1691 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1692
1693 globalConfig := Config{
1694 Options: Options{
1695 ContextPaths: []string{"global-context.md", "shared-context.md"},
1696 },
1697 Agents: map[AgentID]Agent{
1698 AgentID("custom-context-agent"): {
1699 ID: AgentID("custom-context-agent"),
1700 Name: "Custom Context Agent",
1701 ContextPaths: []string{"agent-specific.md", "custom.md"},
1702 },
1703 AgentID("default-context-agent"): {
1704 ID: AgentID("default-context-agent"),
1705 Name: "Default Context Agent",
1706 },
1707 },
1708 }
1709
1710 configPath := filepath.Join(testConfigDir, "crush.json")
1711 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1712 data, err := json.Marshal(globalConfig)
1713 require.NoError(t, err)
1714 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1715
1716 cfg, err := Init(cwdDir, false)
1717
1718 require.NoError(t, err)
1719
1720 customAgent := cfg.Agents[AgentID("custom-context-agent")]
1721 expectedCustomPaths := append(defaultContextPaths, "global-context.md", "shared-context.md", "agent-specific.md", "custom.md")
1722 assert.Equal(t, expectedCustomPaths, customAgent.ContextPaths)
1723
1724 defaultAgent := cfg.Agents[AgentID("default-context-agent")]
1725 expectedContextPaths := append(defaultContextPaths, "global-context.md", "shared-context.md")
1726 assert.Equal(t, expectedContextPaths, defaultAgent.ContextPaths)
1727
1728 coderAgent := cfg.Agents[AgentCoder]
1729 assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1730}
1731
1732func TestOptionsMerging_ContextPaths(t *testing.T) {
1733 reset()
1734 testConfigDir = t.TempDir()
1735 cwdDir := t.TempDir()
1736
1737 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1738
1739 globalConfig := Config{
1740 Options: Options{
1741 ContextPaths: []string{"global1.md", "global2.md"},
1742 },
1743 }
1744
1745 configPath := filepath.Join(testConfigDir, "crush.json")
1746 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1747 data, err := json.Marshal(globalConfig)
1748 require.NoError(t, err)
1749 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1750
1751 localConfig := Config{
1752 Options: Options{
1753 ContextPaths: []string{"local1.md", "local2.md"},
1754 },
1755 }
1756
1757 localConfigPath := filepath.Join(cwdDir, "crush.json")
1758 data, err = json.Marshal(localConfig)
1759 require.NoError(t, err)
1760 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1761
1762 cfg, err := Init(cwdDir, false)
1763
1764 require.NoError(t, err)
1765
1766 expectedContextPaths := append(defaultContextPaths, "global1.md", "global2.md", "local1.md", "local2.md")
1767 assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1768}
1769
1770func TestOptionsMerging_TUIOptions(t *testing.T) {
1771 reset()
1772 testConfigDir = t.TempDir()
1773 cwdDir := t.TempDir()
1774
1775 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1776
1777 globalConfig := Config{
1778 Options: Options{
1779 TUI: TUIOptions{
1780 CompactMode: false,
1781 },
1782 },
1783 }
1784
1785 configPath := filepath.Join(testConfigDir, "crush.json")
1786 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1787 data, err := json.Marshal(globalConfig)
1788 require.NoError(t, err)
1789 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1790
1791 localConfig := Config{
1792 Options: Options{
1793 TUI: TUIOptions{
1794 CompactMode: true,
1795 },
1796 },
1797 }
1798
1799 localConfigPath := filepath.Join(cwdDir, "crush.json")
1800 data, err = json.Marshal(localConfig)
1801 require.NoError(t, err)
1802 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1803
1804 cfg, err := Init(cwdDir, false)
1805
1806 require.NoError(t, err)
1807
1808 assert.True(t, cfg.Options.TUI.CompactMode)
1809}
1810
1811func TestOptionsMerging_DebugFlags(t *testing.T) {
1812 reset()
1813 testConfigDir = t.TempDir()
1814 cwdDir := t.TempDir()
1815
1816 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1817
1818 globalConfig := Config{
1819 Options: Options{
1820 Debug: false,
1821 DebugLSP: false,
1822 DisableAutoSummarize: false,
1823 },
1824 }
1825
1826 configPath := filepath.Join(testConfigDir, "crush.json")
1827 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1828 data, err := json.Marshal(globalConfig)
1829 require.NoError(t, err)
1830 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1831
1832 localConfig := Config{
1833 Options: Options{
1834 DebugLSP: true,
1835 DisableAutoSummarize: true,
1836 },
1837 }
1838
1839 localConfigPath := filepath.Join(cwdDir, "crush.json")
1840 data, err = json.Marshal(localConfig)
1841 require.NoError(t, err)
1842 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1843
1844 cfg, err := Init(cwdDir, false)
1845
1846 require.NoError(t, err)
1847
1848 assert.False(t, cfg.Options.Debug)
1849 assert.True(t, cfg.Options.DebugLSP)
1850 assert.True(t, cfg.Options.DisableAutoSummarize)
1851}
1852
1853func TestOptionsMerging_DataDirectory(t *testing.T) {
1854 reset()
1855 testConfigDir = t.TempDir()
1856 cwdDir := t.TempDir()
1857
1858 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1859
1860 globalConfig := Config{
1861 Options: Options{
1862 DataDirectory: "global-data",
1863 },
1864 }
1865
1866 configPath := filepath.Join(testConfigDir, "crush.json")
1867 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1868 data, err := json.Marshal(globalConfig)
1869 require.NoError(t, err)
1870 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1871
1872 localConfig := Config{
1873 Options: Options{
1874 DataDirectory: "local-data",
1875 },
1876 }
1877
1878 localConfigPath := filepath.Join(cwdDir, "crush.json")
1879 data, err = json.Marshal(localConfig)
1880 require.NoError(t, err)
1881 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1882
1883 cfg, err := Init(cwdDir, false)
1884
1885 require.NoError(t, err)
1886
1887 assert.Equal(t, "local-data", cfg.Options.DataDirectory)
1888}
1889
1890func TestOptionsMerging_DefaultValues(t *testing.T) {
1891 reset()
1892 testConfigDir = t.TempDir()
1893 cwdDir := t.TempDir()
1894
1895 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1896
1897 cfg, err := Init(cwdDir, false)
1898
1899 require.NoError(t, err)
1900
1901 assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
1902 assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
1903 assert.False(t, cfg.Options.TUI.CompactMode)
1904 assert.False(t, cfg.Options.Debug)
1905 assert.False(t, cfg.Options.DebugLSP)
1906 assert.False(t, cfg.Options.DisableAutoSummarize)
1907}
1908
1909func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
1910 reset()
1911 testConfigDir = t.TempDir()
1912 cwdDir := t.TempDir()
1913
1914 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1915
1916 globalConfig := Config{
1917 Options: Options{
1918 Debug: false,
1919 },
1920 }
1921
1922 configPath := filepath.Join(testConfigDir, "crush.json")
1923 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1924 data, err := json.Marshal(globalConfig)
1925 require.NoError(t, err)
1926 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1927
1928 cfg, err := Init(cwdDir, true)
1929
1930 require.NoError(t, err)
1931
1932 // Debug flag from Init should take precedence
1933 assert.True(t, cfg.Options.Debug)
1934}
1935
1936func TestOptionsMerging_ComplexScenario(t *testing.T) {
1937 reset()
1938 testConfigDir = t.TempDir()
1939 cwdDir := t.TempDir()
1940
1941 // Set up a provider
1942 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1943
1944 // Create global config with various options
1945 globalConfig := Config{
1946 Options: Options{
1947 ContextPaths: []string{"global-context.md"},
1948 DataDirectory: "global-data",
1949 Debug: false,
1950 DebugLSP: false,
1951 DisableAutoSummarize: false,
1952 TUI: TUIOptions{
1953 CompactMode: false,
1954 },
1955 },
1956 }
1957
1958 configPath := filepath.Join(testConfigDir, "crush.json")
1959 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1960 data, err := json.Marshal(globalConfig)
1961 require.NoError(t, err)
1962 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1963
1964 // Create local config that partially overrides
1965 localConfig := Config{
1966 Options: Options{
1967 ContextPaths: []string{"local-context.md"},
1968 DebugLSP: true, // Override
1969 DisableAutoSummarize: true, // Override
1970 TUI: TUIOptions{
1971 CompactMode: true, // Override
1972 },
1973 // DataDirectory and Debug not specified - should keep global values
1974 },
1975 }
1976
1977 localConfigPath := filepath.Join(cwdDir, "crush.json")
1978 data, err = json.Marshal(localConfig)
1979 require.NoError(t, err)
1980 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1981
1982 cfg, err := Init(cwdDir, false)
1983
1984 require.NoError(t, err)
1985
1986 // Check merged results
1987 expectedContextPaths := append(defaultContextPaths, "global-context.md", "local-context.md")
1988 assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1989 assert.Equal(t, "global-data", cfg.Options.DataDirectory) // From global
1990 assert.False(t, cfg.Options.Debug) // From global
1991 assert.True(t, cfg.Options.DebugLSP) // From local
1992 assert.True(t, cfg.Options.DisableAutoSummarize) // From local
1993 assert.True(t, cfg.Options.TUI.CompactMode) // From local
1994}
1995
1996// Model Selection Tests
1997
1998func TestModelSelection_PreferredModelSelection(t *testing.T) {
1999 reset()
2000 testConfigDir = t.TempDir()
2001 cwdDir := t.TempDir()
2002
2003 // Set up multiple providers to test selection logic
2004 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
2005 os.Setenv("OPENAI_API_KEY", "test-openai-key")
2006
2007 cfg, err := Init(cwdDir, false)
2008
2009 require.NoError(t, err)
2010 require.Len(t, cfg.Providers, 2)
2011
2012 // Should have preferred models set
2013 assert.NotEmpty(t, cfg.Models.Large.ModelID)
2014 assert.NotEmpty(t, cfg.Models.Large.Provider)
2015 assert.NotEmpty(t, cfg.Models.Small.ModelID)
2016 assert.NotEmpty(t, cfg.Models.Small.Provider)
2017
2018 // Both should use the same provider (first available)
2019 assert.Equal(t, cfg.Models.Large.Provider, cfg.Models.Small.Provider)
2020}
2021
2022func TestValidation_InvalidModelReference(t *testing.T) {
2023 reset()
2024 testConfigDir = t.TempDir()
2025 cwdDir := t.TempDir()
2026
2027 globalConfig := Config{
2028 Providers: map[provider.InferenceProvider]ProviderConfig{
2029 provider.InferenceProviderOpenAI: {
2030 ID: provider.InferenceProviderOpenAI,
2031 APIKey: "test-key",
2032 ProviderType: provider.TypeOpenAI,
2033 DefaultLargeModel: "non-existent-model",
2034 DefaultSmallModel: "gpt-3.5-turbo",
2035 Models: []Model{
2036 {
2037 ID: "gpt-3.5-turbo",
2038 Name: "GPT-3.5 Turbo",
2039 ContextWindow: 4096,
2040 DefaultMaxTokens: 2048,
2041 },
2042 },
2043 },
2044 },
2045 }
2046
2047 configPath := filepath.Join(testConfigDir, "crush.json")
2048 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2049 data, err := json.Marshal(globalConfig)
2050 require.NoError(t, err)
2051 require.NoError(t, os.WriteFile(configPath, data, 0o644))
2052
2053 _, err = Init(cwdDir, false)
2054 assert.Error(t, err)
2055}
2056
2057func TestValidation_InvalidAgentModelType(t *testing.T) {
2058 reset()
2059 testConfigDir = t.TempDir()
2060 cwdDir := t.TempDir()
2061
2062 os.Setenv("ANTHROPIC_API_KEY", "test-key")
2063
2064 globalConfig := Config{
2065 Agents: map[AgentID]Agent{
2066 AgentID("invalid-agent"): {
2067 ID: AgentID("invalid-agent"),
2068 Name: "Invalid Agent",
2069 Model: ModelType("invalid"),
2070 },
2071 },
2072 }
2073
2074 configPath := filepath.Join(testConfigDir, "crush.json")
2075 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2076 data, err := json.Marshal(globalConfig)
2077 require.NoError(t, err)
2078 require.NoError(t, os.WriteFile(configPath, data, 0o644))
2079
2080 _, err = Init(cwdDir, false)
2081 assert.Error(t, err)
2082}