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
1059 configPath := filepath.Join(testConfigDir, "crush.json")
1060 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1061 data, err := json.Marshal(globalConfig)
1062 require.NoError(t, err)
1063 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1064
1065 cfg, err := Init(cwdDir, false)
1066
1067 require.NoError(t, err)
1068 assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1069}
1070
1071func TestProviderValidation_DisabledProvider(t *testing.T) {
1072 reset()
1073 testConfigDir = t.TempDir()
1074 cwdDir := t.TempDir()
1075
1076 globalConfig := Config{
1077 Providers: map[provider.InferenceProvider]ProviderConfig{
1078 provider.InferenceProviderOpenAI: {
1079 ID: provider.InferenceProviderOpenAI,
1080 APIKey: "openai-key",
1081 ProviderType: provider.TypeOpenAI,
1082 Disabled: true,
1083 DefaultLargeModel: "gpt-4",
1084 DefaultSmallModel: "gpt-3.5-turbo",
1085 Models: []Model{
1086 {
1087 ID: "gpt-4",
1088 Name: "GPT-4",
1089 ContextWindow: 8192,
1090 DefaultMaxTokens: 4096,
1091 },
1092 {
1093 ID: "gpt-3.5-turbo",
1094 Name: "GPT-3.5 Turbo",
1095 ContextWindow: 4096,
1096 DefaultMaxTokens: 2048,
1097 },
1098 },
1099 },
1100 provider.InferenceProviderAnthropic: {
1101 ID: provider.InferenceProviderAnthropic,
1102 APIKey: "anthropic-key",
1103 ProviderType: provider.TypeAnthropic,
1104 Disabled: false, // This one is enabled
1105 DefaultLargeModel: "claude-3-opus",
1106 DefaultSmallModel: "claude-3-haiku",
1107 Models: []Model{
1108 {
1109 ID: "claude-3-opus",
1110 Name: "Claude 3 Opus",
1111 ContextWindow: 200000,
1112 DefaultMaxTokens: 4096,
1113 },
1114 {
1115 ID: "claude-3-haiku",
1116 Name: "Claude 3 Haiku",
1117 ContextWindow: 200000,
1118 DefaultMaxTokens: 4096,
1119 },
1120 },
1121 },
1122 },
1123 }
1124
1125 configPath := filepath.Join(testConfigDir, "crush.json")
1126 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1127 data, err := json.Marshal(globalConfig)
1128 require.NoError(t, err)
1129 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1130
1131 cfg, err := Init(cwdDir, false)
1132
1133 require.NoError(t, err)
1134 assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1135 assert.True(t, cfg.Providers[provider.InferenceProviderOpenAI].Disabled)
1136 assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
1137 assert.False(t, cfg.Providers[provider.InferenceProviderAnthropic].Disabled)
1138}
1139
1140func TestProviderModels_AddingNewModels(t *testing.T) {
1141 reset()
1142 testConfigDir = t.TempDir()
1143 cwdDir := t.TempDir()
1144
1145 globalConfig := Config{
1146 Providers: map[provider.InferenceProvider]ProviderConfig{
1147 provider.InferenceProviderOpenAI: {
1148 ID: provider.InferenceProviderOpenAI,
1149 APIKey: "openai-key",
1150 ProviderType: provider.TypeOpenAI,
1151 DefaultLargeModel: "gpt-4",
1152 DefaultSmallModel: "gpt-4-turbo",
1153 Models: []Model{
1154 {
1155 ID: "gpt-4",
1156 Name: "GPT-4",
1157 ContextWindow: 8192,
1158 DefaultMaxTokens: 4096,
1159 },
1160 },
1161 },
1162 },
1163 }
1164
1165 localConfig := Config{
1166 Providers: map[provider.InferenceProvider]ProviderConfig{
1167 provider.InferenceProviderOpenAI: {
1168 Models: []Model{
1169 {
1170 ID: "gpt-4-turbo",
1171 Name: "GPT-4 Turbo",
1172 ContextWindow: 128000,
1173 DefaultMaxTokens: 4096,
1174 },
1175 },
1176 },
1177 },
1178 }
1179
1180 configPath := filepath.Join(testConfigDir, "crush.json")
1181 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1182 data, err := json.Marshal(globalConfig)
1183 require.NoError(t, err)
1184 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1185
1186 localConfigPath := filepath.Join(cwdDir, "crush.json")
1187 data, err = json.Marshal(localConfig)
1188 require.NoError(t, err)
1189 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1190
1191 cfg, err := Init(cwdDir, false)
1192
1193 require.NoError(t, err)
1194
1195 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1196 assert.Len(t, openaiProvider.Models, 2)
1197
1198 modelIDs := make([]string, len(openaiProvider.Models))
1199 for i, model := range openaiProvider.Models {
1200 modelIDs[i] = model.ID
1201 }
1202 assert.Contains(t, modelIDs, "gpt-4")
1203 assert.Contains(t, modelIDs, "gpt-4-turbo")
1204}
1205
1206func TestProviderModels_DuplicateModelHandling(t *testing.T) {
1207 reset()
1208 testConfigDir = t.TempDir()
1209 cwdDir := t.TempDir()
1210
1211 globalConfig := Config{
1212 Providers: map[provider.InferenceProvider]ProviderConfig{
1213 provider.InferenceProviderOpenAI: {
1214 ID: provider.InferenceProviderOpenAI,
1215 APIKey: "openai-key",
1216 ProviderType: provider.TypeOpenAI,
1217 DefaultLargeModel: "gpt-4",
1218 DefaultSmallModel: "gpt-4",
1219 Models: []Model{
1220 {
1221 ID: "gpt-4",
1222 Name: "GPT-4",
1223 ContextWindow: 8192,
1224 DefaultMaxTokens: 4096,
1225 },
1226 },
1227 },
1228 },
1229 }
1230
1231 localConfig := Config{
1232 Providers: map[provider.InferenceProvider]ProviderConfig{
1233 provider.InferenceProviderOpenAI: {
1234 Models: []Model{
1235 {
1236 ID: "gpt-4",
1237 Name: "GPT-4 Updated",
1238 ContextWindow: 16384,
1239 DefaultMaxTokens: 8192,
1240 },
1241 },
1242 },
1243 },
1244 }
1245
1246 configPath := filepath.Join(testConfigDir, "crush.json")
1247 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1248 data, err := json.Marshal(globalConfig)
1249 require.NoError(t, err)
1250 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1251
1252 localConfigPath := filepath.Join(cwdDir, "crush.json")
1253 data, err = json.Marshal(localConfig)
1254 require.NoError(t, err)
1255 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1256
1257 cfg, err := Init(cwdDir, false)
1258
1259 require.NoError(t, err)
1260
1261 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1262 assert.Len(t, openaiProvider.Models, 1)
1263
1264 model := openaiProvider.Models[0]
1265 assert.Equal(t, "gpt-4", model.ID)
1266 assert.Equal(t, "GPT-4", model.Name)
1267 assert.Equal(t, int64(8192), model.ContextWindow)
1268}
1269
1270func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
1271 reset()
1272 testConfigDir = t.TempDir()
1273 cwdDir := t.TempDir()
1274
1275 globalConfig := Config{
1276 Providers: map[provider.InferenceProvider]ProviderConfig{
1277 provider.InferenceProviderOpenAI: {
1278 ID: provider.InferenceProviderOpenAI,
1279 APIKey: "openai-key",
1280 ProviderType: provider.TypeOpenAI,
1281 DefaultLargeModel: "gpt-4",
1282 DefaultSmallModel: "gpt-4",
1283 Models: []Model{
1284 {
1285 ID: "gpt-4",
1286 Name: "GPT-4",
1287 CostPer1MIn: 30.0,
1288 CostPer1MOut: 60.0,
1289 CostPer1MInCached: 15.0,
1290 CostPer1MOutCached: 30.0,
1291 ContextWindow: 8192,
1292 DefaultMaxTokens: 4096,
1293 CanReason: true,
1294 ReasoningEffort: "medium",
1295 SupportsImages: true,
1296 },
1297 },
1298 },
1299 },
1300 }
1301
1302 configPath := filepath.Join(testConfigDir, "crush.json")
1303 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1304 data, err := json.Marshal(globalConfig)
1305 require.NoError(t, err)
1306 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1307
1308 cfg, err := Init(cwdDir, false)
1309
1310 require.NoError(t, err)
1311
1312 openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1313 require.Len(t, openaiProvider.Models, 1)
1314
1315 model := openaiProvider.Models[0]
1316 assert.Equal(t, 30.0, model.CostPer1MIn)
1317 assert.Equal(t, 60.0, model.CostPer1MOut)
1318 assert.Equal(t, 15.0, model.CostPer1MInCached)
1319 assert.Equal(t, 30.0, model.CostPer1MOutCached)
1320 assert.True(t, model.CanReason)
1321 assert.Equal(t, "medium", model.ReasoningEffort)
1322 assert.True(t, model.SupportsImages)
1323}
1324
1325func TestDefaultAgents_CoderAgent(t *testing.T) {
1326 reset()
1327 testConfigDir = t.TempDir()
1328 cwdDir := t.TempDir()
1329
1330 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1331
1332 cfg, err := Init(cwdDir, false)
1333
1334 require.NoError(t, err)
1335 assert.Contains(t, cfg.Agents, AgentCoder)
1336
1337 coderAgent := cfg.Agents[AgentCoder]
1338 assert.Equal(t, AgentCoder, coderAgent.ID)
1339 assert.Equal(t, "Coder", coderAgent.Name)
1340 assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1341 assert.Equal(t, LargeModel, coderAgent.Model)
1342 assert.False(t, coderAgent.Disabled)
1343 assert.Equal(t, cfg.Options.ContextPaths, coderAgent.ContextPaths)
1344 assert.Nil(t, coderAgent.AllowedTools)
1345}
1346
1347func TestDefaultAgents_TaskAgent(t *testing.T) {
1348 reset()
1349 testConfigDir = t.TempDir()
1350 cwdDir := t.TempDir()
1351
1352 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1353
1354 cfg, err := Init(cwdDir, false)
1355
1356 require.NoError(t, err)
1357 assert.Contains(t, cfg.Agents, AgentTask)
1358
1359 taskAgent := cfg.Agents[AgentTask]
1360 assert.Equal(t, AgentTask, taskAgent.ID)
1361 assert.Equal(t, "Task", taskAgent.Name)
1362 assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1363 assert.Equal(t, LargeModel, taskAgent.Model)
1364 assert.False(t, taskAgent.Disabled)
1365 assert.Equal(t, cfg.Options.ContextPaths, taskAgent.ContextPaths)
1366
1367 expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
1368 assert.Equal(t, expectedTools, taskAgent.AllowedTools)
1369
1370 assert.Equal(t, map[string][]string{}, taskAgent.AllowedMCP)
1371 assert.Equal(t, []string{}, taskAgent.AllowedLSP)
1372}
1373
1374func TestAgentMerging_CustomAgent(t *testing.T) {
1375 reset()
1376 testConfigDir = t.TempDir()
1377 cwdDir := t.TempDir()
1378
1379 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1380
1381 globalConfig := Config{
1382 Agents: map[AgentID]Agent{
1383 AgentID("custom-agent"): {
1384 ID: AgentID("custom-agent"),
1385 Name: "Custom Agent",
1386 Description: "A custom agent for testing",
1387 Model: SmallModel,
1388 AllowedTools: []string{"glob", "grep"},
1389 AllowedMCP: map[string][]string{"mcp1": {"tool1", "tool2"}},
1390 AllowedLSP: []string{"typescript", "go"},
1391 ContextPaths: []string{"custom-context.md"},
1392 },
1393 },
1394 MCP: map[string]MCP{
1395 "mcp1": {
1396 Type: MCPStdio,
1397 Command: "test-mcp-command",
1398 Args: []string{"--test"},
1399 },
1400 },
1401 LSP: map[string]LSPConfig{
1402 "typescript": {
1403 Command: "typescript-language-server",
1404 Args: []string{"--stdio"},
1405 },
1406 "go": {
1407 Command: "gopls",
1408 Args: []string{},
1409 },
1410 },
1411 }
1412
1413 configPath := filepath.Join(testConfigDir, "crush.json")
1414 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1415 data, err := json.Marshal(globalConfig)
1416 require.NoError(t, err)
1417 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1418
1419 cfg, err := Init(cwdDir, false)
1420
1421 require.NoError(t, err)
1422
1423 assert.Contains(t, cfg.Agents, AgentCoder)
1424 assert.Contains(t, cfg.Agents, AgentTask)
1425 assert.Contains(t, cfg.Agents, AgentID("custom-agent"))
1426
1427 customAgent := cfg.Agents[AgentID("custom-agent")]
1428 assert.Equal(t, "Custom Agent", customAgent.Name)
1429 assert.Equal(t, "A custom agent for testing", customAgent.Description)
1430 assert.Equal(t, SmallModel, customAgent.Model)
1431 assert.Equal(t, []string{"glob", "grep"}, customAgent.AllowedTools)
1432 assert.Equal(t, map[string][]string{"mcp1": {"tool1", "tool2"}}, customAgent.AllowedMCP)
1433 assert.Equal(t, []string{"typescript", "go"}, customAgent.AllowedLSP)
1434 expectedContextPaths := append(defaultContextPaths, "custom-context.md")
1435 assert.Equal(t, expectedContextPaths, customAgent.ContextPaths)
1436}
1437
1438func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
1439 reset()
1440 testConfigDir = t.TempDir()
1441 cwdDir := t.TempDir()
1442
1443 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1444
1445 globalConfig := Config{
1446 Agents: map[AgentID]Agent{
1447 AgentCoder: {
1448 Model: SmallModel,
1449 AllowedMCP: map[string][]string{"mcp1": {"tool1"}},
1450 AllowedLSP: []string{"typescript"},
1451 ContextPaths: []string{"coder-specific.md"},
1452 },
1453 },
1454 MCP: map[string]MCP{
1455 "mcp1": {
1456 Type: MCPStdio,
1457 Command: "test-mcp-command",
1458 Args: []string{"--test"},
1459 },
1460 },
1461 LSP: map[string]LSPConfig{
1462 "typescript": {
1463 Command: "typescript-language-server",
1464 Args: []string{"--stdio"},
1465 },
1466 },
1467 }
1468
1469 configPath := filepath.Join(testConfigDir, "crush.json")
1470 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1471 data, err := json.Marshal(globalConfig)
1472 require.NoError(t, err)
1473 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1474
1475 cfg, err := Init(cwdDir, false)
1476
1477 require.NoError(t, err)
1478
1479 coderAgent := cfg.Agents[AgentCoder]
1480 assert.Equal(t, AgentCoder, coderAgent.ID)
1481 assert.Equal(t, "Coder", coderAgent.Name)
1482 assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1483
1484 expectedContextPaths := append(cfg.Options.ContextPaths, "coder-specific.md")
1485 assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1486
1487 assert.Equal(t, SmallModel, coderAgent.Model)
1488 assert.Equal(t, map[string][]string{"mcp1": {"tool1"}}, coderAgent.AllowedMCP)
1489 assert.Equal(t, []string{"typescript"}, coderAgent.AllowedLSP)
1490}
1491
1492func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
1493 reset()
1494 testConfigDir = t.TempDir()
1495 cwdDir := t.TempDir()
1496
1497 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1498
1499 globalConfig := Config{
1500 Agents: map[AgentID]Agent{
1501 AgentTask: {
1502 Model: SmallModel,
1503 AllowedMCP: map[string][]string{"search-mcp": nil},
1504 AllowedLSP: []string{"python"},
1505 Name: "Search Agent",
1506 Description: "Custom search agent",
1507 Disabled: true,
1508 AllowedTools: []string{"glob", "grep", "view"},
1509 },
1510 },
1511 MCP: map[string]MCP{
1512 "search-mcp": {
1513 Type: MCPStdio,
1514 Command: "search-mcp-command",
1515 Args: []string{"--search"},
1516 },
1517 },
1518 LSP: map[string]LSPConfig{
1519 "python": {
1520 Command: "pylsp",
1521 Args: []string{},
1522 },
1523 },
1524 }
1525
1526 configPath := filepath.Join(testConfigDir, "crush.json")
1527 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1528 data, err := json.Marshal(globalConfig)
1529 require.NoError(t, err)
1530 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1531
1532 cfg, err := Init(cwdDir, false)
1533
1534 require.NoError(t, err)
1535
1536 taskAgent := cfg.Agents[AgentTask]
1537 assert.Equal(t, "Task", taskAgent.Name)
1538 assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1539 assert.False(t, taskAgent.Disabled)
1540 assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools)
1541
1542 assert.Equal(t, SmallModel, taskAgent.Model)
1543 assert.Equal(t, map[string][]string{"search-mcp": nil}, taskAgent.AllowedMCP)
1544 assert.Equal(t, []string{"python"}, taskAgent.AllowedLSP)
1545}
1546
1547func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
1548 reset()
1549 testConfigDir = t.TempDir()
1550 cwdDir := t.TempDir()
1551
1552 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1553
1554 globalConfig := Config{
1555 Agents: map[AgentID]Agent{
1556 AgentID("test-agent"): {
1557 ID: AgentID("test-agent"),
1558 Name: "Global Agent",
1559 Description: "Global description",
1560 Model: LargeModel,
1561 AllowedTools: []string{"glob"},
1562 },
1563 },
1564 }
1565
1566 configPath := filepath.Join(testConfigDir, "crush.json")
1567 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1568 data, err := json.Marshal(globalConfig)
1569 require.NoError(t, err)
1570 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1571
1572 // Create local config that overrides
1573 localConfig := Config{
1574 Agents: map[AgentID]Agent{
1575 AgentID("test-agent"): {
1576 Name: "Local Agent",
1577 Description: "Local description",
1578 Model: SmallModel,
1579 Disabled: true,
1580 AllowedTools: []string{"grep", "view"},
1581 AllowedMCP: map[string][]string{"local-mcp": {"tool1"}},
1582 },
1583 },
1584 MCP: map[string]MCP{
1585 "local-mcp": {
1586 Type: MCPStdio,
1587 Command: "local-mcp-command",
1588 Args: []string{"--local"},
1589 },
1590 },
1591 }
1592
1593 localConfigPath := filepath.Join(cwdDir, "crush.json")
1594 data, err = json.Marshal(localConfig)
1595 require.NoError(t, err)
1596 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1597
1598 cfg, err := Init(cwdDir, false)
1599
1600 require.NoError(t, err)
1601
1602 testAgent := cfg.Agents[AgentID("test-agent")]
1603 assert.Equal(t, "Local Agent", testAgent.Name)
1604 assert.Equal(t, "Local description", testAgent.Description)
1605 assert.Equal(t, SmallModel, testAgent.Model)
1606 assert.True(t, testAgent.Disabled)
1607 assert.Equal(t, []string{"grep", "view"}, testAgent.AllowedTools)
1608 assert.Equal(t, map[string][]string{"local-mcp": {"tool1"}}, testAgent.AllowedMCP)
1609}
1610
1611func TestAgentModelTypeAssignment(t *testing.T) {
1612 reset()
1613 testConfigDir = t.TempDir()
1614 cwdDir := t.TempDir()
1615
1616 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1617
1618 globalConfig := Config{
1619 Agents: map[AgentID]Agent{
1620 AgentID("large-agent"): {
1621 ID: AgentID("large-agent"),
1622 Name: "Large Model Agent",
1623 Model: LargeModel,
1624 },
1625 AgentID("small-agent"): {
1626 ID: AgentID("small-agent"),
1627 Name: "Small Model Agent",
1628 Model: SmallModel,
1629 },
1630 AgentID("default-agent"): {
1631 ID: AgentID("default-agent"),
1632 Name: "Default Model Agent",
1633 },
1634 },
1635 }
1636
1637 configPath := filepath.Join(testConfigDir, "crush.json")
1638 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1639 data, err := json.Marshal(globalConfig)
1640 require.NoError(t, err)
1641 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1642
1643 cfg, err := Init(cwdDir, false)
1644
1645 require.NoError(t, err)
1646
1647 assert.Equal(t, LargeModel, cfg.Agents[AgentID("large-agent")].Model)
1648 assert.Equal(t, SmallModel, cfg.Agents[AgentID("small-agent")].Model)
1649 assert.Equal(t, LargeModel, cfg.Agents[AgentID("default-agent")].Model)
1650}
1651
1652func TestAgentContextPathOverrides(t *testing.T) {
1653 reset()
1654 testConfigDir = t.TempDir()
1655 cwdDir := t.TempDir()
1656
1657 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1658
1659 globalConfig := Config{
1660 Options: Options{
1661 ContextPaths: []string{"global-context.md", "shared-context.md"},
1662 },
1663 Agents: map[AgentID]Agent{
1664 AgentID("custom-context-agent"): {
1665 ID: AgentID("custom-context-agent"),
1666 Name: "Custom Context Agent",
1667 ContextPaths: []string{"agent-specific.md", "custom.md"},
1668 },
1669 AgentID("default-context-agent"): {
1670 ID: AgentID("default-context-agent"),
1671 Name: "Default Context Agent",
1672 },
1673 },
1674 }
1675
1676 configPath := filepath.Join(testConfigDir, "crush.json")
1677 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1678 data, err := json.Marshal(globalConfig)
1679 require.NoError(t, err)
1680 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1681
1682 cfg, err := Init(cwdDir, false)
1683
1684 require.NoError(t, err)
1685
1686 customAgent := cfg.Agents[AgentID("custom-context-agent")]
1687 expectedCustomPaths := append(defaultContextPaths, "global-context.md", "shared-context.md", "agent-specific.md", "custom.md")
1688 assert.Equal(t, expectedCustomPaths, customAgent.ContextPaths)
1689
1690 defaultAgent := cfg.Agents[AgentID("default-context-agent")]
1691 expectedContextPaths := append(defaultContextPaths, "global-context.md", "shared-context.md")
1692 assert.Equal(t, expectedContextPaths, defaultAgent.ContextPaths)
1693
1694 coderAgent := cfg.Agents[AgentCoder]
1695 assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1696}
1697
1698func TestOptionsMerging_ContextPaths(t *testing.T) {
1699 reset()
1700 testConfigDir = t.TempDir()
1701 cwdDir := t.TempDir()
1702
1703 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1704
1705 globalConfig := Config{
1706 Options: Options{
1707 ContextPaths: []string{"global1.md", "global2.md"},
1708 },
1709 }
1710
1711 configPath := filepath.Join(testConfigDir, "crush.json")
1712 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1713 data, err := json.Marshal(globalConfig)
1714 require.NoError(t, err)
1715 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1716
1717 localConfig := Config{
1718 Options: Options{
1719 ContextPaths: []string{"local1.md", "local2.md"},
1720 },
1721 }
1722
1723 localConfigPath := filepath.Join(cwdDir, "crush.json")
1724 data, err = json.Marshal(localConfig)
1725 require.NoError(t, err)
1726 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1727
1728 cfg, err := Init(cwdDir, false)
1729
1730 require.NoError(t, err)
1731
1732 expectedContextPaths := append(defaultContextPaths, "global1.md", "global2.md", "local1.md", "local2.md")
1733 assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1734}
1735
1736func TestOptionsMerging_TUIOptions(t *testing.T) {
1737 reset()
1738 testConfigDir = t.TempDir()
1739 cwdDir := t.TempDir()
1740
1741 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1742
1743 globalConfig := Config{
1744 Options: Options{
1745 TUI: TUIOptions{
1746 CompactMode: false,
1747 },
1748 },
1749 }
1750
1751 configPath := filepath.Join(testConfigDir, "crush.json")
1752 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1753 data, err := json.Marshal(globalConfig)
1754 require.NoError(t, err)
1755 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1756
1757 localConfig := Config{
1758 Options: Options{
1759 TUI: TUIOptions{
1760 CompactMode: true,
1761 },
1762 },
1763 }
1764
1765 localConfigPath := filepath.Join(cwdDir, "crush.json")
1766 data, err = json.Marshal(localConfig)
1767 require.NoError(t, err)
1768 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1769
1770 cfg, err := Init(cwdDir, false)
1771
1772 require.NoError(t, err)
1773
1774 assert.True(t, cfg.Options.TUI.CompactMode)
1775}
1776
1777func TestOptionsMerging_DebugFlags(t *testing.T) {
1778 reset()
1779 testConfigDir = t.TempDir()
1780 cwdDir := t.TempDir()
1781
1782 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1783
1784 globalConfig := Config{
1785 Options: Options{
1786 Debug: false,
1787 DebugLSP: false,
1788 DisableAutoSummarize: false,
1789 },
1790 }
1791
1792 configPath := filepath.Join(testConfigDir, "crush.json")
1793 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1794 data, err := json.Marshal(globalConfig)
1795 require.NoError(t, err)
1796 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1797
1798 localConfig := Config{
1799 Options: Options{
1800 DebugLSP: true,
1801 DisableAutoSummarize: true,
1802 },
1803 }
1804
1805 localConfigPath := filepath.Join(cwdDir, "crush.json")
1806 data, err = json.Marshal(localConfig)
1807 require.NoError(t, err)
1808 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1809
1810 cfg, err := Init(cwdDir, false)
1811
1812 require.NoError(t, err)
1813
1814 assert.False(t, cfg.Options.Debug)
1815 assert.True(t, cfg.Options.DebugLSP)
1816 assert.True(t, cfg.Options.DisableAutoSummarize)
1817}
1818
1819func TestOptionsMerging_DataDirectory(t *testing.T) {
1820 reset()
1821 testConfigDir = t.TempDir()
1822 cwdDir := t.TempDir()
1823
1824 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1825
1826 globalConfig := Config{
1827 Options: Options{
1828 DataDirectory: "global-data",
1829 },
1830 }
1831
1832 configPath := filepath.Join(testConfigDir, "crush.json")
1833 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1834 data, err := json.Marshal(globalConfig)
1835 require.NoError(t, err)
1836 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1837
1838 localConfig := Config{
1839 Options: Options{
1840 DataDirectory: "local-data",
1841 },
1842 }
1843
1844 localConfigPath := filepath.Join(cwdDir, "crush.json")
1845 data, err = json.Marshal(localConfig)
1846 require.NoError(t, err)
1847 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1848
1849 cfg, err := Init(cwdDir, false)
1850
1851 require.NoError(t, err)
1852
1853 assert.Equal(t, "local-data", cfg.Options.DataDirectory)
1854}
1855
1856func TestOptionsMerging_DefaultValues(t *testing.T) {
1857 reset()
1858 testConfigDir = t.TempDir()
1859 cwdDir := t.TempDir()
1860
1861 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1862
1863 cfg, err := Init(cwdDir, false)
1864
1865 require.NoError(t, err)
1866
1867 assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
1868 assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
1869 assert.False(t, cfg.Options.TUI.CompactMode)
1870 assert.False(t, cfg.Options.Debug)
1871 assert.False(t, cfg.Options.DebugLSP)
1872 assert.False(t, cfg.Options.DisableAutoSummarize)
1873}
1874
1875func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
1876 reset()
1877 testConfigDir = t.TempDir()
1878 cwdDir := t.TempDir()
1879
1880 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1881
1882 globalConfig := Config{
1883 Options: Options{
1884 Debug: false,
1885 },
1886 }
1887
1888 configPath := filepath.Join(testConfigDir, "crush.json")
1889 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1890 data, err := json.Marshal(globalConfig)
1891 require.NoError(t, err)
1892 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1893
1894 cfg, err := Init(cwdDir, true)
1895
1896 require.NoError(t, err)
1897
1898 // Debug flag from Init should take precedence
1899 assert.True(t, cfg.Options.Debug)
1900}
1901
1902func TestOptionsMerging_ComplexScenario(t *testing.T) {
1903 reset()
1904 testConfigDir = t.TempDir()
1905 cwdDir := t.TempDir()
1906
1907 // Set up a provider
1908 os.Setenv("ANTHROPIC_API_KEY", "test-key")
1909
1910 // Create global config with various options
1911 globalConfig := Config{
1912 Options: Options{
1913 ContextPaths: []string{"global-context.md"},
1914 DataDirectory: "global-data",
1915 Debug: false,
1916 DebugLSP: false,
1917 DisableAutoSummarize: false,
1918 TUI: TUIOptions{
1919 CompactMode: false,
1920 },
1921 },
1922 }
1923
1924 configPath := filepath.Join(testConfigDir, "crush.json")
1925 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1926 data, err := json.Marshal(globalConfig)
1927 require.NoError(t, err)
1928 require.NoError(t, os.WriteFile(configPath, data, 0o644))
1929
1930 // Create local config that partially overrides
1931 localConfig := Config{
1932 Options: Options{
1933 ContextPaths: []string{"local-context.md"},
1934 DebugLSP: true, // Override
1935 DisableAutoSummarize: true, // Override
1936 TUI: TUIOptions{
1937 CompactMode: true, // Override
1938 },
1939 // DataDirectory and Debug not specified - should keep global values
1940 },
1941 }
1942
1943 localConfigPath := filepath.Join(cwdDir, "crush.json")
1944 data, err = json.Marshal(localConfig)
1945 require.NoError(t, err)
1946 require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1947
1948 cfg, err := Init(cwdDir, false)
1949
1950 require.NoError(t, err)
1951
1952 // Check merged results
1953 expectedContextPaths := append(defaultContextPaths, "global-context.md", "local-context.md")
1954 assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1955 assert.Equal(t, "global-data", cfg.Options.DataDirectory) // From global
1956 assert.False(t, cfg.Options.Debug) // From global
1957 assert.True(t, cfg.Options.DebugLSP) // From local
1958 assert.True(t, cfg.Options.DisableAutoSummarize) // From local
1959 assert.True(t, cfg.Options.TUI.CompactMode) // From local
1960}
1961
1962// Model Selection Tests
1963
1964func TestModelSelection_PreferredModelSelection(t *testing.T) {
1965 reset()
1966 testConfigDir = t.TempDir()
1967 cwdDir := t.TempDir()
1968
1969 // Set up multiple providers to test selection logic
1970 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
1971 os.Setenv("OPENAI_API_KEY", "test-openai-key")
1972
1973 cfg, err := Init(cwdDir, false)
1974
1975 require.NoError(t, err)
1976 require.Len(t, cfg.Providers, 2)
1977
1978 // Should have preferred models set
1979 assert.NotEmpty(t, cfg.Models.Large.ModelID)
1980 assert.NotEmpty(t, cfg.Models.Large.Provider)
1981 assert.NotEmpty(t, cfg.Models.Small.ModelID)
1982 assert.NotEmpty(t, cfg.Models.Small.Provider)
1983
1984 // Both should use the same provider (first available)
1985 assert.Equal(t, cfg.Models.Large.Provider, cfg.Models.Small.Provider)
1986}
1987
1988func TestValidation_InvalidModelReference(t *testing.T) {
1989 reset()
1990 testConfigDir = t.TempDir()
1991 cwdDir := t.TempDir()
1992
1993 globalConfig := Config{
1994 Providers: map[provider.InferenceProvider]ProviderConfig{
1995 provider.InferenceProviderOpenAI: {
1996 ID: provider.InferenceProviderOpenAI,
1997 APIKey: "test-key",
1998 ProviderType: provider.TypeOpenAI,
1999 DefaultLargeModel: "non-existent-model",
2000 DefaultSmallModel: "gpt-3.5-turbo",
2001 Models: []Model{
2002 {
2003 ID: "gpt-3.5-turbo",
2004 Name: "GPT-3.5 Turbo",
2005 ContextWindow: 4096,
2006 DefaultMaxTokens: 2048,
2007 },
2008 },
2009 },
2010 },
2011 }
2012
2013 configPath := filepath.Join(testConfigDir, "crush.json")
2014 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2015 data, err := json.Marshal(globalConfig)
2016 require.NoError(t, err)
2017 require.NoError(t, os.WriteFile(configPath, data, 0o644))
2018
2019 _, err = Init(cwdDir, false)
2020 assert.Error(t, err)
2021}
2022
2023func TestValidation_EmptyAPIKey(t *testing.T) {
2024 reset()
2025 testConfigDir = t.TempDir()
2026 cwdDir := t.TempDir()
2027
2028 globalConfig := Config{
2029 Providers: map[provider.InferenceProvider]ProviderConfig{
2030 provider.InferenceProviderOpenAI: {
2031 ID: provider.InferenceProviderOpenAI,
2032 ProviderType: provider.TypeOpenAI,
2033 Models: []Model{
2034 {
2035 ID: "gpt-4",
2036 Name: "GPT-4",
2037 ContextWindow: 8192,
2038 DefaultMaxTokens: 4096,
2039 },
2040 },
2041 },
2042 },
2043 }
2044
2045 configPath := filepath.Join(testConfigDir, "crush.json")
2046 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2047 data, err := json.Marshal(globalConfig)
2048 require.NoError(t, err)
2049 require.NoError(t, os.WriteFile(configPath, data, 0o644))
2050
2051 _, err = Init(cwdDir, false)
2052 assert.Error(t, err)
2053}
2054
2055func TestValidation_InvalidAgentModelType(t *testing.T) {
2056 reset()
2057 testConfigDir = t.TempDir()
2058 cwdDir := t.TempDir()
2059
2060 os.Setenv("ANTHROPIC_API_KEY", "test-key")
2061
2062 globalConfig := Config{
2063 Agents: map[AgentID]Agent{
2064 AgentID("invalid-agent"): {
2065 ID: AgentID("invalid-agent"),
2066 Name: "Invalid Agent",
2067 Model: ModelType("invalid"),
2068 },
2069 },
2070 }
2071
2072 configPath := filepath.Join(testConfigDir, "crush.json")
2073 require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2074 data, err := json.Marshal(globalConfig)
2075 require.NoError(t, err)
2076 require.NoError(t, os.WriteFile(configPath, data, 0o644))
2077
2078 _, err = Init(cwdDir, false)
2079 assert.Error(t, err)
2080}