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