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