@@ -1,2075 +0,0 @@
1−package config
2−
3−import (
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−
15−func 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−
61−func 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−
75−func 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−
86−func 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−
99−func TestGet_BeforeInitialization(t *testing.T) {
100− reset()
101−
102− assert.Panics(t, func() {
103− Get()
104− })
105−}
106−
107−func 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−
119−func 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−
131−func 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−
184−func 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−
235−func 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−
336−func 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−
349−func 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−
361−func 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−
370−func 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−
387−func 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−
398−func 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−
436−func 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−
453−func 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−
473−func 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−
487−func 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−
506−func 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−
523−func 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−
537−func 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−
554−func 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−
570−func 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−
581−func 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−
598−func 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−
628−func 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−
637−func TestHasAWSCredentials_Profile(t *testing.T) {
638− reset()
639−
640− os.Setenv("AWS_PROFILE", "test-profile")
641−
642− assert.True(t, hasAWSCredentials())
643−}
644−
645−func TestHasAWSCredentials_DefaultProfile(t *testing.T) {
646− reset()
647−
648− os.Setenv("AWS_DEFAULT_PROFILE", "default")
649−
650− assert.True(t, hasAWSCredentials())
651−}
652−
653−func TestHasAWSCredentials_Region(t *testing.T) {
654− reset()
655−
656− os.Setenv("AWS_REGION", "us-east-1")
657−
658− assert.True(t, hasAWSCredentials())
659−}
660−
661−func 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−
669−func TestHasAWSCredentials_NoCredentials(t *testing.T) {
670− reset()
671−
672− assert.False(t, hasAWSCredentials())
673−}
674−
675−func 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−
724−func 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−
776−func 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−
845−func 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−
939−func 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−
968−func 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−
997−func 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−
1027−func 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−
1070−func 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−
1139−func 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−
1205−func 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−
1282−func 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−
1352−func 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−
1374−func 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−
1401−func 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−
1465−func 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−
1519−func 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−
1574−func 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−
1638−func 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−
1679−func 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−
1725−func 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−
1763−func 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−
1804−func 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−
1846−func 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−
1883−func 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−
1902−func 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−
1929−func 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−
1991−func 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−
2015−func 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−
2050−func 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−}