@@ -2,25 +2,299 @@ package config
4 "encoding/json"
4 "encoding/json"
10+ "github.com/charmbracelet/crush/internal/fur/provider"
9 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/assert"
12+ "github.com/stretchr/testify/require"
12−func resetEnvVars() {
15+func reset() {
16+ // Clear all environment variables that could affect config
17+ envVarsToUnset := []string{
13− os.Setenv("ANTHROPIC_API_KEY", "")
19+ "ANTHROPIC_API_KEY",
14− os.Setenv("OPENAI_API_KEY", "")
20+ "OPENAI_API_KEY",
15− os.Setenv("GEMINI_API_KEY", "")
21+ "GEMINI_API_KEY",
16− os.Setenv("XAI_API_KEY", "")
22+ "XAI_API_KEY",
17− os.Setenv("OPENROUTER_API_KEY", "")
23+ "OPENROUTER_API_KEY",
25+ // Google Cloud / VertexAI
26+ "GOOGLE_GENAI_USE_VERTEXAI",
27+ "GOOGLE_CLOUD_PROJECT",
28+ "GOOGLE_CLOUD_LOCATION",
32+ "AWS_SECRET_ACCESS_KEY",
34+ "AWS_DEFAULT_REGION",
36+ "AWS_DEFAULT_PROFILE",
37+ "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
38+ "AWS_CONTAINER_CREDENTIALS_FULL_URI",
44+ for _, envVar := range envVarsToUnset {
20−func TestConfigWithEnv(t *testing.T) {
55+// Core Configuration Loading Tests
57+func TestInit_ValidWorkingDirectory(t *testing.T) {
59+ testConfigDir = t.TempDir()
60+ cwdDir := t.TempDir()
62+ cfg, err := Init(cwdDir, false)
64+ require.NoError(t, err)
65+ assert.NotNil(t, cfg)
66+ assert.Equal(t, cwdDir, WorkingDirectory())
67+ assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
68+ assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
71+func TestInit_WithDebugFlag(t *testing.T) {
73+ testConfigDir = t.TempDir()
74+ cwdDir := t.TempDir()
76+ cfg, err := Init(cwdDir, true)
78+ require.NoError(t, err)
79+ assert.True(t, cfg.Options.Debug)
82+func TestInit_SingletonBehavior(t *testing.T) {
84+ testConfigDir = t.TempDir()
85+ cwdDir := t.TempDir()
87+ cfg1, err1 := Init(cwdDir, false)
88+ cfg2, err2 := Init(cwdDir, false)
90+ require.NoError(t, err1)
91+ require.NoError(t, err2)
92+ assert.Same(t, cfg1, cfg2) // Should be the same instance
95+func TestGet_BeforeInitialization(t *testing.T) {
98+ assert.Panics(t, func() {
103+func TestGet_AfterInitialization(t *testing.T) {
105+ testConfigDir = t.TempDir()
106+ cwdDir := t.TempDir()
108+ cfg1, err := Init(cwdDir, false)
109+ require.NoError(t, err)
112+ assert.Same(t, cfg1, cfg2)
115+func TestLoadConfig_NoConfigFiles(t *testing.T) {
117+ testConfigDir = t.TempDir()
118+ cwdDir := t.TempDir()
120+ cfg, err := Init(cwdDir, false)
122+ require.NoError(t, err)
123+ assert.Len(t, cfg.Providers, 0) // No providers without env vars or config files
124+ assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
127+func TestLoadConfig_OnlyGlobalConfig(t *testing.T) {
129+ testConfigDir = t.TempDir()
130+ cwdDir := t.TempDir()
132+ // Create global config file
133+ globalConfig := Config{
134+ Providers: map[provider.InferenceProvider]ProviderConfig{
135+ provider.InferenceProviderOpenAI: {
136+ ID: provider.InferenceProviderOpenAI,
138+ ProviderType: provider.TypeOpenAI,
142+ ContextPaths: []string{"custom-context.md"},
146+ configPath := filepath.Join(testConfigDir, "crush.json")
147+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
149+ data, err := json.Marshal(globalConfig)
150+ require.NoError(t, err)
151+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
153+ cfg, err := Init(cwdDir, false)
155+ require.NoError(t, err)
156+ assert.Len(t, cfg.Providers, 1)
157+ assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
158+ assert.Contains(t, cfg.Options.ContextPaths, "custom-context.md")
161+func TestLoadConfig_OnlyLocalConfig(t *testing.T) {
163+ testConfigDir = t.TempDir()
164+ cwdDir := t.TempDir()
166+ // Create local config file
167+ localConfig := Config{
168+ Providers: map[provider.InferenceProvider]ProviderConfig{
169+ provider.InferenceProviderAnthropic: {
170+ ID: provider.InferenceProviderAnthropic,
171+ APIKey: "local-key",
172+ ProviderType: provider.TypeAnthropic,
176+ TUI: TUIOptions{CompactMode: true},
180+ localConfigPath := filepath.Join(cwdDir, "crush.json")
181+ data, err := json.Marshal(localConfig)
182+ require.NoError(t, err)
183+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
185+ cfg, err := Init(cwdDir, false)
187+ require.NoError(t, err)
188+ assert.Len(t, cfg.Providers, 1)
189+ assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
190+ assert.True(t, cfg.Options.TUI.CompactMode)
193+func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
195+ testConfigDir = t.TempDir()
196+ cwdDir := t.TempDir()
198+ // Create global config
199+ globalConfig := Config{
200+ Providers: map[provider.InferenceProvider]ProviderConfig{
201+ provider.InferenceProviderOpenAI: {
202+ ID: provider.InferenceProviderOpenAI,
203+ APIKey: "global-key",
204+ ProviderType: provider.TypeOpenAI,
208+ ContextPaths: []string{"global-context.md"},
212+ configPath := filepath.Join(testConfigDir, "crush.json")
213+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
214+ data, err := json.Marshal(globalConfig)
215+ require.NoError(t, err)
216+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
218+ // Create local config that overrides and adds
219+ localConfig := Config{
220+ Providers: map[provider.InferenceProvider]ProviderConfig{
221+ provider.InferenceProviderOpenAI: {
222+ APIKey: "local-key", // Override global
224+ provider.InferenceProviderAnthropic: {
225+ ID: provider.InferenceProviderAnthropic,
226+ APIKey: "anthropic-key",
227+ ProviderType: provider.TypeAnthropic,
231+ ContextPaths: []string{"local-context.md"},
232+ TUI: TUIOptions{CompactMode: true},
236+ localConfigPath := filepath.Join(cwdDir, "crush.json")
237+ data, err = json.Marshal(localConfig)
238+ require.NoError(t, err)
239+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
241+ cfg, err := Init(cwdDir, false)
243+ require.NoError(t, err)
244+ assert.Len(t, cfg.Providers, 2)
246+ // Check that local config overrode global
247+ openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
248+ assert.Equal(t, "local-key", openaiProvider.APIKey)
250+ // Check that local config added new provider
251+ assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
253+ // Check that context paths were merged
254+ assert.Contains(t, cfg.Options.ContextPaths, "global-context.md")
255+ assert.Contains(t, cfg.Options.ContextPaths, "local-context.md")
256+ assert.True(t, cfg.Options.TUI.CompactMode)
259+func TestLoadConfig_MalformedGlobalJSON(t *testing.T) {
261+ testConfigDir = t.TempDir()
262+ cwdDir := t.TempDir()
264+ // Create malformed global config
265+ configPath := filepath.Join(testConfigDir, "crush.json")
266+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
267+ require.NoError(t, os.WriteFile(configPath, []byte(`{invalid json`), 0o644))
269+ _, err := Init(cwdDir, false)
270+ assert.Error(t, err)
273+func TestLoadConfig_MalformedLocalJSON(t *testing.T) {
275+ testConfigDir = t.TempDir()
276+ cwdDir := t.TempDir()
278+ // Create malformed local config
279+ localConfigPath := filepath.Join(cwdDir, "crush.json")
280+ require.NoError(t, os.WriteFile(localConfigPath, []byte(`{invalid json`), 0o644))
282+ _, err := Init(cwdDir, false)
283+ assert.Error(t, err)
286+func TestConfigWithoutEnv(t *testing.T) {
22 testConfigDir = t.TempDir()
288 testConfigDir = t.TempDir()
289+ cwdDir := t.TempDir()
291+ cfg, _ := Init(cwdDir, false)
292+ assert.Len(t, cfg.Providers, 0)
295+func TestConfigWithEnv(t *testing.T) {
297+ testConfigDir = t.TempDir()
24 cwdDir := t.TempDir()
298 cwdDir := t.TempDir()
26 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
300 os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
@@ -28,8 +302,1914 @@ func TestConfigWithEnv(t *testing.T) {
28 os.Setenv("GEMINI_API_KEY", "test-gemini-key")
302 os.Setenv("GEMINI_API_KEY", "test-gemini-key")
29 os.Setenv("XAI_API_KEY", "test-xai-key")
303 os.Setenv("XAI_API_KEY", "test-xai-key")
30 os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
304 os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
31 cfg, _ := Init(cwdDir, false)
306 cfg, _ := Init(cwdDir, false)
32− data, _ := json.MarshalIndent(cfg, "", " ")
33− fmt.Println(string(data))
34 assert.Len(t, cfg.Providers, 5)
307 assert.Len(t, cfg.Providers, 5)
310+// Environment Variable Tests
312+func TestEnvVars_NoEnvironmentVariables(t *testing.T) {
314+ testConfigDir = t.TempDir()
315+ cwdDir := t.TempDir()
317+ cfg, err := Init(cwdDir, false)
319+ require.NoError(t, err)
320+ assert.Len(t, cfg.Providers, 0)
323+func TestEnvVars_AllSupportedAPIKeys(t *testing.T) {
325+ testConfigDir = t.TempDir()
326+ cwdDir := t.TempDir()
328+ // Set all supported API keys
329+ os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
330+ os.Setenv("OPENAI_API_KEY", "test-openai-key")
331+ os.Setenv("GEMINI_API_KEY", "test-gemini-key")
332+ os.Setenv("XAI_API_KEY", "test-xai-key")
333+ os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
335+ cfg, err := Init(cwdDir, false)
337+ require.NoError(t, err)
338+ assert.Len(t, cfg.Providers, 5)
340+ // Verify each provider is configured correctly
341+ anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
342+ assert.Equal(t, "test-anthropic-key", anthropicProvider.APIKey)
343+ assert.Equal(t, provider.TypeAnthropic, anthropicProvider.ProviderType)
345+ openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
346+ assert.Equal(t, "test-openai-key", openaiProvider.APIKey)
347+ assert.Equal(t, provider.TypeOpenAI, openaiProvider.ProviderType)
349+ geminiProvider := cfg.Providers[provider.InferenceProviderGemini]
350+ assert.Equal(t, "test-gemini-key", geminiProvider.APIKey)
351+ assert.Equal(t, provider.TypeGemini, geminiProvider.ProviderType)
353+ xaiProvider := cfg.Providers[provider.InferenceProviderXAI]
354+ assert.Equal(t, "test-xai-key", xaiProvider.APIKey)
355+ assert.Equal(t, provider.TypeXAI, xaiProvider.ProviderType)
357+ openrouterProvider := cfg.Providers[provider.InferenceProviderOpenRouter]
358+ assert.Equal(t, "test-openrouter-key", openrouterProvider.APIKey)
359+ assert.Equal(t, provider.TypeOpenAI, openrouterProvider.ProviderType)
360+ assert.Equal(t, "https://openrouter.ai/api/v1", openrouterProvider.BaseURL)
363+func TestEnvVars_PartialEnvironmentVariables(t *testing.T) {
365+ testConfigDir = t.TempDir()
366+ cwdDir := t.TempDir()
368+ // Set only some API keys
369+ os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
370+ os.Setenv("OPENAI_API_KEY", "test-openai-key")
372+ cfg, err := Init(cwdDir, false)
374+ require.NoError(t, err)
375+ assert.Len(t, cfg.Providers, 2)
376+ assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
377+ assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
378+ assert.NotContains(t, cfg.Providers, provider.InferenceProviderGemini)
381+func TestEnvVars_VertexAIConfiguration(t *testing.T) {
383+ testConfigDir = t.TempDir()
384+ cwdDir := t.TempDir()
386+ // Set VertexAI environment variables
387+ os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
388+ os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
389+ os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
391+ cfg, err := Init(cwdDir, false)
393+ require.NoError(t, err)
394+ assert.Contains(t, cfg.Providers, provider.InferenceProviderVertexAI)
396+ vertexProvider := cfg.Providers[provider.InferenceProviderVertexAI]
397+ assert.Equal(t, provider.TypeVertexAI, vertexProvider.ProviderType)
398+ assert.Equal(t, "test-project", vertexProvider.ExtraParams["project"])
399+ assert.Equal(t, "us-central1", vertexProvider.ExtraParams["location"])
402+func TestEnvVars_VertexAIWithoutUseFlag(t *testing.T) {
404+ testConfigDir = t.TempDir()
405+ cwdDir := t.TempDir()
407+ // Set Google Cloud vars but not the use flag
408+ os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
409+ os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
411+ cfg, err := Init(cwdDir, false)
413+ require.NoError(t, err)
414+ assert.NotContains(t, cfg.Providers, provider.InferenceProviderVertexAI)
417+func TestEnvVars_AWSBedrockWithAccessKeys(t *testing.T) {
419+ testConfigDir = t.TempDir()
420+ cwdDir := t.TempDir()
422+ // Set AWS credentials
423+ os.Setenv("AWS_ACCESS_KEY_ID", "test-access-key")
424+ os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
425+ os.Setenv("AWS_DEFAULT_REGION", "us-east-1")
427+ cfg, err := Init(cwdDir, false)
429+ require.NoError(t, err)
430+ assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
432+ bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
433+ assert.Equal(t, provider.TypeBedrock, bedrockProvider.ProviderType)
434+ assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
437+func TestEnvVars_AWSBedrockWithProfile(t *testing.T) {
439+ testConfigDir = t.TempDir()
440+ cwdDir := t.TempDir()
443+ os.Setenv("AWS_PROFILE", "test-profile")
444+ os.Setenv("AWS_REGION", "eu-west-1")
446+ cfg, err := Init(cwdDir, false)
448+ require.NoError(t, err)
449+ assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
451+ bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
452+ assert.Equal(t, "eu-west-1", bedrockProvider.ExtraParams["region"])
455+func TestEnvVars_AWSBedrockWithContainerCredentials(t *testing.T) {
457+ testConfigDir = t.TempDir()
458+ cwdDir := t.TempDir()
460+ // Set AWS container credentials
461+ os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
462+ os.Setenv("AWS_DEFAULT_REGION", "ap-southeast-1")
464+ cfg, err := Init(cwdDir, false)
466+ require.NoError(t, err)
467+ assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
470+func TestEnvVars_AWSBedrockRegionPriority(t *testing.T) {
472+ testConfigDir = t.TempDir()
473+ cwdDir := t.TempDir()
475+ // Set both region variables - AWS_DEFAULT_REGION should take priority
476+ os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
477+ os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
478+ os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
479+ os.Setenv("AWS_REGION", "us-east-1")
481+ cfg, err := Init(cwdDir, false)
483+ require.NoError(t, err)
484+ bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
485+ assert.Equal(t, "us-west-2", bedrockProvider.ExtraParams["region"])
488+func TestEnvVars_AWSBedrockFallbackRegion(t *testing.T) {
490+ testConfigDir = t.TempDir()
491+ cwdDir := t.TempDir()
493+ // Set only AWS_REGION (not AWS_DEFAULT_REGION)
494+ os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
495+ os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
496+ os.Setenv("AWS_REGION", "us-east-1")
498+ cfg, err := Init(cwdDir, false)
500+ require.NoError(t, err)
501+ bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
502+ assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
505+func TestEnvVars_NoAWSCredentials(t *testing.T) {
507+ testConfigDir = t.TempDir()
508+ cwdDir := t.TempDir()
510+ // Don't set any AWS credentials
511+ cfg, err := Init(cwdDir, false)
513+ require.NoError(t, err)
514+ assert.NotContains(t, cfg.Providers, provider.InferenceProviderBedrock)
517+func TestEnvVars_CustomEnvironmentVariables(t *testing.T) {
519+ testConfigDir = t.TempDir()
520+ cwdDir := t.TempDir()
522+ // Test that environment variables are properly resolved from provider definitions
523+ // This test assumes the provider system uses $VARIABLE_NAME format
524+ os.Setenv("ANTHROPIC_API_KEY", "resolved-anthropic-key")
526+ cfg, err := Init(cwdDir, false)
528+ require.NoError(t, err)
529+ if len(cfg.Providers) > 0 {
530+ // Verify that the environment variable was resolved
531+ if anthropicProvider, exists := cfg.Providers[provider.InferenceProviderAnthropic]; exists {
532+ assert.Equal(t, "resolved-anthropic-key", anthropicProvider.APIKey)
537+func TestEnvVars_CombinedEnvironmentVariables(t *testing.T) {
539+ testConfigDir = t.TempDir()
540+ cwdDir := t.TempDir()
542+ // Set multiple types of environment variables
543+ os.Setenv("ANTHROPIC_API_KEY", "test-anthropic")
544+ os.Setenv("OPENAI_API_KEY", "test-openai")
545+ os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
546+ os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
547+ os.Setenv("AWS_ACCESS_KEY_ID", "test-aws-key")
548+ os.Setenv("AWS_SECRET_ACCESS_KEY", "test-aws-secret")
549+ os.Setenv("AWS_DEFAULT_REGION", "us-west-1")
551+ cfg, err := Init(cwdDir, false)
553+ require.NoError(t, err)
555+ // Should have API key providers + VertexAI + Bedrock
556+ expectedProviders := []provider.InferenceProvider{
557+ provider.InferenceProviderAnthropic,
558+ provider.InferenceProviderOpenAI,
559+ provider.InferenceProviderVertexAI,
560+ provider.InferenceProviderBedrock,
563+ for _, expectedProvider := range expectedProviders {
564+ assert.Contains(t, cfg.Providers, expectedProvider)
568+func TestHasAWSCredentials_AccessKeys(t *testing.T) {
571+ os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
572+ os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
574+ assert.True(t, hasAWSCredentials())
577+func TestHasAWSCredentials_Profile(t *testing.T) {
580+ os.Setenv("AWS_PROFILE", "test-profile")
582+ assert.True(t, hasAWSCredentials())
585+func TestHasAWSCredentials_DefaultProfile(t *testing.T) {
588+ os.Setenv("AWS_DEFAULT_PROFILE", "default")
590+ assert.True(t, hasAWSCredentials())
593+func TestHasAWSCredentials_Region(t *testing.T) {
596+ os.Setenv("AWS_REGION", "us-east-1")
598+ assert.True(t, hasAWSCredentials())
601+func TestHasAWSCredentials_ContainerCredentials(t *testing.T) {
604+ os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
606+ assert.True(t, hasAWSCredentials())
609+func TestHasAWSCredentials_NoCredentials(t *testing.T) {
612+ assert.False(t, hasAWSCredentials())
615+// Provider Configuration Tests
617+func TestProviderMerging_GlobalToBase(t *testing.T) {
619+ testConfigDir = t.TempDir()
620+ cwdDir := t.TempDir()
622+ // Create global config with provider
623+ globalConfig := Config{
624+ Providers: map[provider.InferenceProvider]ProviderConfig{
625+ provider.InferenceProviderOpenAI: {
626+ ID: provider.InferenceProviderOpenAI,
627+ APIKey: "global-openai-key",
628+ ProviderType: provider.TypeOpenAI,
629+ DefaultLargeModel: "gpt-4",
630+ DefaultSmallModel: "gpt-3.5-turbo",
635+ ContextWindow: 8192,
636+ DefaultMaxTokens: 4096,
643+ configPath := filepath.Join(testConfigDir, "crush.json")
644+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
645+ data, err := json.Marshal(globalConfig)
646+ require.NoError(t, err)
647+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
649+ cfg, err := Init(cwdDir, false)
651+ require.NoError(t, err)
652+ assert.Len(t, cfg.Providers, 1)
654+ openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
655+ assert.Equal(t, "global-openai-key", openaiProvider.APIKey)
656+ assert.Equal(t, "gpt-4", openaiProvider.DefaultLargeModel)
657+ assert.Equal(t, "gpt-3.5-turbo", openaiProvider.DefaultSmallModel)
658+ assert.Len(t, openaiProvider.Models, 1)
661+func TestProviderMerging_LocalToBase(t *testing.T) {
663+ testConfigDir = t.TempDir()
664+ cwdDir := t.TempDir()
666+ // Create local config with provider
667+ localConfig := Config{
668+ Providers: map[provider.InferenceProvider]ProviderConfig{
669+ provider.InferenceProviderAnthropic: {
670+ ID: provider.InferenceProviderAnthropic,
671+ APIKey: "local-anthropic-key",
672+ ProviderType: provider.TypeAnthropic,
673+ DefaultLargeModel: "claude-3-opus",
678+ localConfigPath := filepath.Join(cwdDir, "crush.json")
679+ data, err := json.Marshal(localConfig)
680+ require.NoError(t, err)
681+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
683+ cfg, err := Init(cwdDir, false)
685+ require.NoError(t, err)
686+ assert.Len(t, cfg.Providers, 1)
688+ anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
689+ assert.Equal(t, "local-anthropic-key", anthropicProvider.APIKey)
690+ assert.Equal(t, "claude-3-opus", anthropicProvider.DefaultLargeModel)
693+func TestProviderMerging_ConflictingSettings(t *testing.T) {
695+ testConfigDir = t.TempDir()
696+ cwdDir := t.TempDir()
698+ // Create global config
699+ globalConfig := Config{
700+ Providers: map[provider.InferenceProvider]ProviderConfig{
701+ provider.InferenceProviderOpenAI: {
702+ ID: provider.InferenceProviderOpenAI,
703+ APIKey: "global-key",
704+ ProviderType: provider.TypeOpenAI,
705+ DefaultLargeModel: "gpt-4",
706+ DefaultSmallModel: "gpt-3.5-turbo",
711+ configPath := filepath.Join(testConfigDir, "crush.json")
712+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
713+ data, err := json.Marshal(globalConfig)
714+ require.NoError(t, err)
715+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
717+ // Create local config that overrides
718+ localConfig := Config{
719+ Providers: map[provider.InferenceProvider]ProviderConfig{
720+ provider.InferenceProviderOpenAI: {
721+ APIKey: "local-key",
722+ DefaultLargeModel: "gpt-4-turbo",
723+ // Test disabled separately - don't disable here as it causes nil pointer
728+ localConfigPath := filepath.Join(cwdDir, "crush.json")
729+ data, err = json.Marshal(localConfig)
730+ require.NoError(t, err)
731+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
733+ cfg, err := Init(cwdDir, false)
735+ require.NoError(t, err)
737+ openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
738+ // Local should override global
739+ assert.Equal(t, "local-key", openaiProvider.APIKey)
740+ assert.Equal(t, "gpt-4-turbo", openaiProvider.DefaultLargeModel)
741+ assert.False(t, openaiProvider.Disabled) // Should not be disabled
742+ // Global values should remain where not overridden
743+ assert.Equal(t, "gpt-3.5-turbo", openaiProvider.DefaultSmallModel)
746+func TestProviderMerging_CustomVsKnownProviders(t *testing.T) {
748+ testConfigDir = t.TempDir()
749+ cwdDir := t.TempDir()
751+ customProviderID := provider.InferenceProvider("custom-provider")
753+ // Create config with both known and custom providers
754+ globalConfig := Config{
755+ Providers: map[provider.InferenceProvider]ProviderConfig{
756+ // Known provider - some fields should not be overrideable
757+ provider.InferenceProviderOpenAI: {
758+ ID: provider.InferenceProviderOpenAI,
759+ APIKey: "openai-key",
760+ BaseURL: "should-not-override",
761+ ProviderType: provider.TypeAnthropic, // Should not override
763+ // Custom provider - all fields should be configurable
765+ ID: customProviderID,
766+ APIKey: "custom-key",
767+ BaseURL: "https://custom.api.com",
768+ ProviderType: provider.TypeOpenAI,
773+ localConfig := Config{
774+ Providers: map[provider.InferenceProvider]ProviderConfig{
775+ provider.InferenceProviderOpenAI: {
776+ BaseURL: "https://should-not-change.com",
777+ ProviderType: provider.TypeGemini, // Should not change
780+ BaseURL: "https://updated-custom.api.com",
781+ ProviderType: provider.TypeOpenAI,
786+ configPath := filepath.Join(testConfigDir, "crush.json")
787+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
788+ data, err := json.Marshal(globalConfig)
789+ require.NoError(t, err)
790+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
792+ localConfigPath := filepath.Join(cwdDir, "crush.json")
793+ data, err = json.Marshal(localConfig)
794+ require.NoError(t, err)
795+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
797+ cfg, err := Init(cwdDir, false)
799+ require.NoError(t, err)
801+ // Known provider should not have BaseURL/ProviderType overridden
802+ openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
803+ assert.NotEqual(t, "https://should-not-change.com", openaiProvider.BaseURL)
804+ assert.NotEqual(t, provider.TypeGemini, openaiProvider.ProviderType)
806+ // Custom provider should have all fields configurable
807+ customProvider := cfg.Providers[customProviderID]
808+ assert.Equal(t, "custom-key", customProvider.APIKey) // Should preserve from global
809+ assert.Equal(t, "https://updated-custom.api.com", customProvider.BaseURL)
810+ assert.Equal(t, provider.TypeOpenAI, customProvider.ProviderType)
813+func TestProviderValidation_CustomProviderMissingBaseURL(t *testing.T) {
815+ testConfigDir = t.TempDir()
816+ cwdDir := t.TempDir()
818+ customProviderID := provider.InferenceProvider("custom-provider")
820+ // Create config with custom provider missing BaseURL
821+ globalConfig := Config{
822+ Providers: map[provider.InferenceProvider]ProviderConfig{
824+ ID: customProviderID,
825+ APIKey: "custom-key",
826+ ProviderType: provider.TypeOpenAI,
832+ configPath := filepath.Join(testConfigDir, "crush.json")
833+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
834+ data, err := json.Marshal(globalConfig)
835+ require.NoError(t, err)
836+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
838+ cfg, err := Init(cwdDir, false)
840+ require.NoError(t, err)
841+ // Provider should be filtered out due to validation failure
842+ assert.NotContains(t, cfg.Providers, customProviderID)
845+func TestProviderValidation_CustomProviderMissingAPIKey(t *testing.T) {
847+ testConfigDir = t.TempDir()
848+ cwdDir := t.TempDir()
850+ customProviderID := provider.InferenceProvider("custom-provider")
852+ globalConfig := Config{
853+ Providers: map[provider.InferenceProvider]ProviderConfig{
855+ ID: customProviderID,
856+ BaseURL: "https://custom.api.com",
857+ ProviderType: provider.TypeOpenAI,
863+ configPath := filepath.Join(testConfigDir, "crush.json")
864+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
865+ data, err := json.Marshal(globalConfig)
866+ require.NoError(t, err)
867+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
869+ cfg, err := Init(cwdDir, false)
871+ require.NoError(t, err)
872+ assert.NotContains(t, cfg.Providers, customProviderID)
875+func TestProviderValidation_CustomProviderInvalidType(t *testing.T) {
877+ testConfigDir = t.TempDir()
878+ cwdDir := t.TempDir()
880+ customProviderID := provider.InferenceProvider("custom-provider")
882+ globalConfig := Config{
883+ Providers: map[provider.InferenceProvider]ProviderConfig{
885+ ID: customProviderID,
886+ APIKey: "custom-key",
887+ BaseURL: "https://custom.api.com",
888+ ProviderType: provider.Type("invalid-type"),
893+ configPath := filepath.Join(testConfigDir, "crush.json")
894+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
895+ data, err := json.Marshal(globalConfig)
896+ require.NoError(t, err)
897+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
899+ cfg, err := Init(cwdDir, false)
901+ require.NoError(t, err)
902+ assert.NotContains(t, cfg.Providers, customProviderID)
905+func TestProviderValidation_KnownProviderValid(t *testing.T) {
907+ testConfigDir = t.TempDir()
908+ cwdDir := t.TempDir()
910+ globalConfig := Config{
911+ Providers: map[provider.InferenceProvider]ProviderConfig{
912+ provider.InferenceProviderOpenAI: {
913+ ID: provider.InferenceProviderOpenAI,
914+ APIKey: "openai-key",
915+ ProviderType: provider.TypeOpenAI,
916+ // BaseURL not required for known providers
921+ configPath := filepath.Join(testConfigDir, "crush.json")
922+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
923+ data, err := json.Marshal(globalConfig)
924+ require.NoError(t, err)
925+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
927+ cfg, err := Init(cwdDir, false)
929+ require.NoError(t, err)
930+ assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
933+func TestProviderValidation_DisabledProvider(t *testing.T) {
935+ testConfigDir = t.TempDir()
936+ cwdDir := t.TempDir()
938+ globalConfig := Config{
939+ Providers: map[provider.InferenceProvider]ProviderConfig{
940+ provider.InferenceProviderOpenAI: {
941+ ID: provider.InferenceProviderOpenAI,
942+ APIKey: "openai-key",
943+ ProviderType: provider.TypeOpenAI,
949+ configPath := filepath.Join(testConfigDir, "crush.json")
950+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
951+ data, err := json.Marshal(globalConfig)
952+ require.NoError(t, err)
953+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
955+ cfg, err := Init(cwdDir, false)
957+ require.NoError(t, err)
958+ // Disabled providers should still be in the config but marked as disabled
959+ assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
960+ assert.True(t, cfg.Providers[provider.InferenceProviderOpenAI].Disabled)
963+func TestProviderModels_AddingNewModels(t *testing.T) {
965+ testConfigDir = t.TempDir()
966+ cwdDir := t.TempDir()
968+ globalConfig := Config{
969+ Providers: map[provider.InferenceProvider]ProviderConfig{
970+ provider.InferenceProviderOpenAI: {
971+ ID: provider.InferenceProviderOpenAI,
972+ APIKey: "openai-key",
973+ ProviderType: provider.TypeOpenAI,
978+ ContextWindow: 8192,
979+ DefaultMaxTokens: 4096,
986+ localConfig := Config{
987+ Providers: map[provider.InferenceProvider]ProviderConfig{
988+ provider.InferenceProviderOpenAI: {
992+ Name: "GPT-4 Turbo",
993+ ContextWindow: 128000,
994+ DefaultMaxTokens: 4096,
1001+ configPath := filepath.Join(testConfigDir, "crush.json")
1002+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1003+ data, err := json.Marshal(globalConfig)
1004+ require.NoError(t, err)
1005+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1007+ localConfigPath := filepath.Join(cwdDir, "crush.json")
1008+ data, err = json.Marshal(localConfig)
1009+ require.NoError(t, err)
1010+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1012+ cfg, err := Init(cwdDir, false)
1014+ require.NoError(t, err)
1016+ openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1017+ assert.Len(t, openaiProvider.Models, 2) // Should have both models
1019+ modelIDs := make([]string, len(openaiProvider.Models))
1020+ for i, model := range openaiProvider.Models {
1021+ modelIDs[i] = model.ID
1023+ assert.Contains(t, modelIDs, "gpt-4")
1024+ assert.Contains(t, modelIDs, "gpt-4-turbo")
1027+func TestProviderModels_DuplicateModelHandling(t *testing.T) {
1029+ testConfigDir = t.TempDir()
1030+ cwdDir := t.TempDir()
1032+ globalConfig := Config{
1033+ Providers: map[provider.InferenceProvider]ProviderConfig{
1034+ provider.InferenceProviderOpenAI: {
1035+ ID: provider.InferenceProviderOpenAI,
1036+ APIKey: "openai-key",
1037+ ProviderType: provider.TypeOpenAI,
1042+ ContextWindow: 8192,
1043+ DefaultMaxTokens: 4096,
1050+ localConfig := Config{
1051+ Providers: map[provider.InferenceProvider]ProviderConfig{
1052+ provider.InferenceProviderOpenAI: {
1055+ ID: "gpt-4", // Same ID as global
1056+ Name: "GPT-4 Updated",
1057+ ContextWindow: 16384,
1058+ DefaultMaxTokens: 8192,
1065+ configPath := filepath.Join(testConfigDir, "crush.json")
1066+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1067+ data, err := json.Marshal(globalConfig)
1068+ require.NoError(t, err)
1069+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1071+ localConfigPath := filepath.Join(cwdDir, "crush.json")
1072+ data, err = json.Marshal(localConfig)
1073+ require.NoError(t, err)
1074+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1076+ cfg, err := Init(cwdDir, false)
1078+ require.NoError(t, err)
1080+ openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1081+ assert.Len(t, openaiProvider.Models, 1) // Should not duplicate
1083+ // Should keep the original model (global config)
1084+ model := openaiProvider.Models[0]
1085+ assert.Equal(t, "gpt-4", model.ID)
1086+ assert.Equal(t, "GPT-4", model.Name) // Original name
1087+ assert.Equal(t, int64(8192), model.ContextWindow) // Original context window
1090+func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
1092+ testConfigDir = t.TempDir()
1093+ cwdDir := t.TempDir()
1095+ globalConfig := Config{
1096+ Providers: map[provider.InferenceProvider]ProviderConfig{
1097+ provider.InferenceProviderOpenAI: {
1098+ ID: provider.InferenceProviderOpenAI,
1099+ APIKey: "openai-key",
1100+ ProviderType: provider.TypeOpenAI,
1106+ CostPer1MOut: 60.0,
1107+ CostPer1MInCached: 15.0,
1108+ CostPer1MOutCached: 30.0,
1109+ ContextWindow: 8192,
1110+ DefaultMaxTokens: 4096,
1112+ ReasoningEffort: "medium",
1113+ SupportsImages: true,
1120+ configPath := filepath.Join(testConfigDir, "crush.json")
1121+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1122+ data, err := json.Marshal(globalConfig)
1123+ require.NoError(t, err)
1124+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1126+ cfg, err := Init(cwdDir, false)
1128+ require.NoError(t, err)
1130+ openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1131+ require.Len(t, openaiProvider.Models, 1)
1133+ model := openaiProvider.Models[0]
1134+ assert.Equal(t, 30.0, model.CostPer1MIn)
1135+ assert.Equal(t, 60.0, model.CostPer1MOut)
1136+ assert.Equal(t, 15.0, model.CostPer1MInCached)
1137+ assert.Equal(t, 30.0, model.CostPer1MOutCached)
1138+ assert.True(t, model.CanReason)
1139+ assert.Equal(t, "medium", model.ReasoningEffort)
1140+ assert.True(t, model.SupportsImages)
1143+// Agent Configuration Tests
1145+func TestDefaultAgents_CoderAgent(t *testing.T) {
1147+ testConfigDir = t.TempDir()
1148+ cwdDir := t.TempDir()
1150+ // Set up a provider so we can test agent configuration
1151+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1153+ cfg, err := Init(cwdDir, false)
1155+ require.NoError(t, err)
1156+ assert.Contains(t, cfg.Agents, AgentCoder)
1158+ coderAgent := cfg.Agents[AgentCoder]
1159+ assert.Equal(t, AgentCoder, coderAgent.ID)
1160+ assert.Equal(t, "Coder", coderAgent.Name)
1161+ assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1162+ assert.Equal(t, LargeModel, coderAgent.Model)
1163+ assert.False(t, coderAgent.Disabled)
1164+ assert.Equal(t, cfg.Options.ContextPaths, coderAgent.ContextPaths)
1165+ // Coder agent should have all tools available (nil means all tools)
1166+ assert.Nil(t, coderAgent.AllowedTools)
1169+func TestDefaultAgents_TaskAgent(t *testing.T) {
1171+ testConfigDir = t.TempDir()
1172+ cwdDir := t.TempDir()
1174+ // Set up a provider so we can test agent configuration
1175+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1177+ cfg, err := Init(cwdDir, false)
1179+ require.NoError(t, err)
1180+ assert.Contains(t, cfg.Agents, AgentTask)
1182+ taskAgent := cfg.Agents[AgentTask]
1183+ assert.Equal(t, AgentTask, taskAgent.ID)
1184+ assert.Equal(t, "Task", taskAgent.Name)
1185+ assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1186+ assert.Equal(t, LargeModel, taskAgent.Model)
1187+ assert.False(t, taskAgent.Disabled)
1188+ assert.Equal(t, cfg.Options.ContextPaths, taskAgent.ContextPaths)
1190+ // Task agent should have restricted tools
1191+ expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
1192+ assert.Equal(t, expectedTools, taskAgent.AllowedTools)
1194+ // Task agent should have no MCPs or LSPs by default
1195+ assert.Equal(t, map[string][]string{}, taskAgent.AllowedMCP)
1196+ assert.Equal(t, []string{}, taskAgent.AllowedLSP)
1199+func TestAgentMerging_CustomAgent(t *testing.T) {
1201+ testConfigDir = t.TempDir()
1202+ cwdDir := t.TempDir()
1204+ // Set up a provider
1205+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1207+ // Create config with custom agent
1208+ globalConfig := Config{
1209+ Agents: map[AgentID]Agent{
1210+ AgentID("custom-agent"): {
1211+ ID: AgentID("custom-agent"),
1212+ Name: "Custom Agent",
1213+ Description: "A custom agent for testing",
1215+ AllowedTools: []string{"glob", "grep"},
1216+ AllowedMCP: map[string][]string{"mcp1": {"tool1", "tool2"}},
1217+ AllowedLSP: []string{"typescript", "go"},
1218+ ContextPaths: []string{"custom-context.md"},
1223+ configPath := filepath.Join(testConfigDir, "crush.json")
1224+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1225+ data, err := json.Marshal(globalConfig)
1226+ require.NoError(t, err)
1227+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1229+ cfg, err := Init(cwdDir, false)
1231+ require.NoError(t, err)
1233+ // Should have default agents plus custom agent
1234+ assert.Contains(t, cfg.Agents, AgentCoder)
1235+ assert.Contains(t, cfg.Agents, AgentTask)
1236+ assert.Contains(t, cfg.Agents, AgentID("custom-agent"))
1238+ customAgent := cfg.Agents[AgentID("custom-agent")]
1239+ assert.Equal(t, "Custom Agent", customAgent.Name)
1240+ assert.Equal(t, "A custom agent for testing", customAgent.Description)
1241+ assert.Equal(t, SmallModel, customAgent.Model)
1242+ assert.Equal(t, []string{"glob", "grep"}, customAgent.AllowedTools)
1243+ assert.Equal(t, map[string][]string{"mcp1": {"tool1", "tool2"}}, customAgent.AllowedMCP)
1244+ assert.Equal(t, []string{"typescript", "go"}, customAgent.AllowedLSP)
1245+ // Context paths should be additive (default + custom)
1246+ expectedContextPaths := append(defaultContextPaths, "custom-context.md")
1247+ assert.Equal(t, expectedContextPaths, customAgent.ContextPaths)
1250+func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
1252+ testConfigDir = t.TempDir()
1253+ cwdDir := t.TempDir()
1255+ // Set up a provider
1256+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1258+ // Create config that modifies the default coder agent
1259+ globalConfig := Config{
1260+ Agents: map[AgentID]Agent{
1262+ Model: SmallModel, // Change from default LargeModel
1263+ AllowedMCP: map[string][]string{"mcp1": {"tool1"}},
1264+ AllowedLSP: []string{"typescript"},
1265+ ContextPaths: []string{"coder-specific.md"}, // Should be additive
1270+ configPath := filepath.Join(testConfigDir, "crush.json")
1271+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1272+ data, err := json.Marshal(globalConfig)
1273+ require.NoError(t, err)
1274+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1276+ cfg, err := Init(cwdDir, false)
1278+ require.NoError(t, err)
1280+ coderAgent := cfg.Agents[AgentCoder]
1281+ // Should preserve default values for unspecified fields
1282+ assert.Equal(t, AgentCoder, coderAgent.ID)
1283+ assert.Equal(t, "Coder", coderAgent.Name)
1284+ assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1286+ // Context paths should be additive (default + custom)
1287+ expectedContextPaths := append(cfg.Options.ContextPaths, "coder-specific.md")
1288+ assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1290+ // Should update specified fields
1291+ assert.Equal(t, SmallModel, coderAgent.Model)
1292+ assert.Equal(t, map[string][]string{"mcp1": {"tool1"}}, coderAgent.AllowedMCP)
1293+ assert.Equal(t, []string{"typescript"}, coderAgent.AllowedLSP)
1296+func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
1298+ testConfigDir = t.TempDir()
1299+ cwdDir := t.TempDir()
1301+ // Set up a provider
1302+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1304+ // Create config that modifies the default task agent
1305+ // Note: Only model, MCP, and LSP should be configurable for known agents
1306+ globalConfig := Config{
1307+ Agents: map[AgentID]Agent{
1309+ Model: SmallModel, // Should be updated
1310+ AllowedMCP: map[string][]string{"search-mcp": nil}, // Should be updated
1311+ AllowedLSP: []string{"python"}, // Should be updated
1312+ // These should be ignored for known agents:
1313+ Name: "Search Agent", // Should be ignored
1314+ Description: "Custom search agent", // Should be ignored
1315+ Disabled: true, // Should be ignored
1316+ AllowedTools: []string{"glob", "grep", "view"}, // Should be ignored
1321+ configPath := filepath.Join(testConfigDir, "crush.json")
1322+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1323+ data, err := json.Marshal(globalConfig)
1324+ require.NoError(t, err)
1325+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1327+ cfg, err := Init(cwdDir, false)
1329+ require.NoError(t, err)
1331+ taskAgent := cfg.Agents[AgentTask]
1332+ // Should preserve default values for protected fields
1333+ assert.Equal(t, "Task", taskAgent.Name) // Should remain default
1334+ assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description) // Should remain default
1335+ assert.False(t, taskAgent.Disabled) // Should remain default
1336+ assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools) // Should remain default
1338+ // Should update configurable fields
1339+ assert.Equal(t, SmallModel, taskAgent.Model)
1340+ assert.Equal(t, map[string][]string{"search-mcp": nil}, taskAgent.AllowedMCP)
1341+ assert.Equal(t, []string{"python"}, taskAgent.AllowedLSP)
1344+func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
1346+ testConfigDir = t.TempDir()
1347+ cwdDir := t.TempDir()
1349+ // Set up a provider
1350+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1352+ // Create global config with custom agent
1353+ globalConfig := Config{
1354+ Agents: map[AgentID]Agent{
1355+ AgentID("test-agent"): {
1356+ ID: AgentID("test-agent"),
1357+ Name: "Global Agent",
1358+ Description: "Global description",
1360+ AllowedTools: []string{"glob"},
1365+ configPath := filepath.Join(testConfigDir, "crush.json")
1366+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1367+ data, err := json.Marshal(globalConfig)
1368+ require.NoError(t, err)
1369+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1371+ // Create local config that overrides
1372+ localConfig := Config{
1373+ Agents: map[AgentID]Agent{
1374+ AgentID("test-agent"): {
1375+ Name: "Local Agent",
1376+ Description: "Local description",
1379+ AllowedTools: []string{"grep", "view"},
1380+ AllowedMCP: map[string][]string{"local-mcp": {"tool1"}},
1385+ localConfigPath := filepath.Join(cwdDir, "crush.json")
1386+ data, err = json.Marshal(localConfig)
1387+ require.NoError(t, err)
1388+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1390+ cfg, err := Init(cwdDir, false)
1392+ require.NoError(t, err)
1394+ testAgent := cfg.Agents[AgentID("test-agent")]
1395+ // Local should override global
1396+ assert.Equal(t, "Local Agent", testAgent.Name)
1397+ assert.Equal(t, "Local description", testAgent.Description)
1398+ assert.Equal(t, SmallModel, testAgent.Model)
1399+ assert.True(t, testAgent.Disabled)
1400+ assert.Equal(t, []string{"grep", "view"}, testAgent.AllowedTools)
1401+ assert.Equal(t, map[string][]string{"local-mcp": {"tool1"}}, testAgent.AllowedMCP)
1404+func TestAgentModelTypeAssignment(t *testing.T) {
1406+ testConfigDir = t.TempDir()
1407+ cwdDir := t.TempDir()
1409+ // Set up a provider
1410+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1412+ // Create config with agents using different model types
1413+ globalConfig := Config{
1414+ Agents: map[AgentID]Agent{
1415+ AgentID("large-agent"): {
1416+ ID: AgentID("large-agent"),
1417+ Name: "Large Model Agent",
1420+ AgentID("small-agent"): {
1421+ ID: AgentID("small-agent"),
1422+ Name: "Small Model Agent",
1425+ AgentID("default-agent"): {
1426+ ID: AgentID("default-agent"),
1427+ Name: "Default Model Agent",
1428+ // No model specified - should default to LargeModel
1433+ configPath := filepath.Join(testConfigDir, "crush.json")
1434+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1435+ data, err := json.Marshal(globalConfig)
1436+ require.NoError(t, err)
1437+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1439+ cfg, err := Init(cwdDir, false)
1441+ require.NoError(t, err)
1443+ assert.Equal(t, LargeModel, cfg.Agents[AgentID("large-agent")].Model)
1444+ assert.Equal(t, SmallModel, cfg.Agents[AgentID("small-agent")].Model)
1445+ assert.Equal(t, LargeModel, cfg.Agents[AgentID("default-agent")].Model) // Should default to LargeModel
1448+func TestAgentContextPathOverrides(t *testing.T) {
1450+ testConfigDir = t.TempDir()
1451+ cwdDir := t.TempDir()
1453+ // Set up a provider
1454+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1456+ // Create config with custom context paths
1457+ globalConfig := Config{
1459+ ContextPaths: []string{"global-context.md", "shared-context.md"},
1461+ Agents: map[AgentID]Agent{
1462+ AgentID("custom-context-agent"): {
1463+ ID: AgentID("custom-context-agent"),
1464+ Name: "Custom Context Agent",
1465+ ContextPaths: []string{"agent-specific.md", "custom.md"},
1467+ AgentID("default-context-agent"): {
1468+ ID: AgentID("default-context-agent"),
1469+ Name: "Default Context Agent",
1470+ // No ContextPaths specified - should use global
1475+ configPath := filepath.Join(testConfigDir, "crush.json")
1476+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1477+ data, err := json.Marshal(globalConfig)
1478+ require.NoError(t, err)
1479+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1481+ cfg, err := Init(cwdDir, false)
1483+ require.NoError(t, err)
1485+ // Agent with custom context paths should have default + global + custom paths (additive)
1486+ customAgent := cfg.Agents[AgentID("custom-context-agent")]
1487+ expectedCustomPaths := append(defaultContextPaths, "global-context.md", "shared-context.md", "agent-specific.md", "custom.md")
1488+ assert.Equal(t, expectedCustomPaths, customAgent.ContextPaths)
1490+ // Agent without custom context paths should use global + defaults
1491+ defaultAgent := cfg.Agents[AgentID("default-context-agent")]
1492+ expectedContextPaths := append(defaultContextPaths, "global-context.md", "shared-context.md")
1493+ assert.Equal(t, expectedContextPaths, defaultAgent.ContextPaths)
1495+ // Default agents should also use the merged context paths
1496+ coderAgent := cfg.Agents[AgentCoder]
1497+ assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1500+// Options and Settings Tests
1502+func TestOptionsMerging_ContextPaths(t *testing.T) {
1504+ testConfigDir = t.TempDir()
1505+ cwdDir := t.TempDir()
1507+ // Set up a provider
1508+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1510+ // Create global config with context paths
1511+ globalConfig := Config{
1513+ ContextPaths: []string{"global1.md", "global2.md"},
1517+ configPath := filepath.Join(testConfigDir, "crush.json")
1518+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1519+ data, err := json.Marshal(globalConfig)
1520+ require.NoError(t, err)
1521+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1523+ // Create local config with additional context paths
1524+ localConfig := Config{
1526+ ContextPaths: []string{"local1.md", "local2.md"},
1530+ localConfigPath := filepath.Join(cwdDir, "crush.json")
1531+ data, err = json.Marshal(localConfig)
1532+ require.NoError(t, err)
1533+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1535+ cfg, err := Init(cwdDir, false)
1537+ require.NoError(t, err)
1539+ // Context paths should be merged: defaults + global + local
1540+ expectedContextPaths := append(defaultContextPaths, "global1.md", "global2.md", "local1.md", "local2.md")
1541+ assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1544+func TestOptionsMerging_TUIOptions(t *testing.T) {
1546+ testConfigDir = t.TempDir()
1547+ cwdDir := t.TempDir()
1549+ // Set up a provider
1550+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1552+ // Create global config with TUI options
1553+ globalConfig := Config{
1556+ CompactMode: false, // Default value
1561+ configPath := filepath.Join(testConfigDir, "crush.json")
1562+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1563+ data, err := json.Marshal(globalConfig)
1564+ require.NoError(t, err)
1565+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1567+ // Create local config that enables compact mode
1568+ localConfig := Config{
1576+ localConfigPath := filepath.Join(cwdDir, "crush.json")
1577+ data, err = json.Marshal(localConfig)
1578+ require.NoError(t, err)
1579+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1581+ cfg, err := Init(cwdDir, false)
1583+ require.NoError(t, err)
1585+ // Local config should override global
1586+ assert.True(t, cfg.Options.TUI.CompactMode)
1589+func TestOptionsMerging_DebugFlags(t *testing.T) {
1591+ testConfigDir = t.TempDir()
1592+ cwdDir := t.TempDir()
1594+ // Set up a provider
1595+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1597+ // Create global config with debug flags
1598+ globalConfig := Config{
1602+ DisableAutoSummarize: false,
1606+ configPath := filepath.Join(testConfigDir, "crush.json")
1607+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1608+ data, err := json.Marshal(globalConfig)
1609+ require.NoError(t, err)
1610+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1612+ // Create local config that enables debug flags
1613+ localConfig := Config{
1616+ DisableAutoSummarize: true,
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))
1625+ cfg, err := Init(cwdDir, false)
1627+ require.NoError(t, err)
1629+ // Local config should override global for boolean flags
1630+ assert.False(t, cfg.Options.Debug) // Not set in local, remains global value
1631+ assert.True(t, cfg.Options.DebugLSP) // Set to true in local
1632+ assert.True(t, cfg.Options.DisableAutoSummarize) // Set to true in local
1635+func TestOptionsMerging_DataDirectory(t *testing.T) {
1637+ testConfigDir = t.TempDir()
1638+ cwdDir := t.TempDir()
1640+ // Set up a provider
1641+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1643+ // Create global config with custom data directory
1644+ globalConfig := Config{
1646+ DataDirectory: "global-data",
1650+ configPath := filepath.Join(testConfigDir, "crush.json")
1651+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1652+ data, err := json.Marshal(globalConfig)
1653+ require.NoError(t, err)
1654+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1656+ // Create local config with different data directory
1657+ localConfig := Config{
1659+ DataDirectory: "local-data",
1663+ localConfigPath := filepath.Join(cwdDir, "crush.json")
1664+ data, err = json.Marshal(localConfig)
1665+ require.NoError(t, err)
1666+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1668+ cfg, err := Init(cwdDir, false)
1670+ require.NoError(t, err)
1672+ // Local config should override global
1673+ assert.Equal(t, "local-data", cfg.Options.DataDirectory)
1676+func TestOptionsMerging_DefaultValues(t *testing.T) {
1678+ testConfigDir = t.TempDir()
1679+ cwdDir := t.TempDir()
1681+ // Set up a provider
1682+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1684+ // No config files - should use defaults
1685+ cfg, err := Init(cwdDir, false)
1687+ require.NoError(t, err)
1689+ // Should have default values
1690+ assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
1691+ assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
1692+ assert.False(t, cfg.Options.TUI.CompactMode)
1693+ assert.False(t, cfg.Options.Debug)
1694+ assert.False(t, cfg.Options.DebugLSP)
1695+ assert.False(t, cfg.Options.DisableAutoSummarize)
1698+func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
1700+ testConfigDir = t.TempDir()
1701+ cwdDir := t.TempDir()
1703+ // Set up a provider
1704+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1706+ // Create config with debug false
1707+ globalConfig := Config{
1713+ configPath := filepath.Join(testConfigDir, "crush.json")
1714+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1715+ data, err := json.Marshal(globalConfig)
1716+ require.NoError(t, err)
1717+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1719+ // Init with debug=true should override config
1720+ cfg, err := Init(cwdDir, true)
1722+ require.NoError(t, err)
1724+ // Debug flag from Init should take precedence
1725+ assert.True(t, cfg.Options.Debug)
1728+func TestOptionsMerging_ComplexScenario(t *testing.T) {
1730+ testConfigDir = t.TempDir()
1731+ cwdDir := t.TempDir()
1733+ // Set up a provider
1734+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
1736+ // Create global config with various options
1737+ globalConfig := Config{
1739+ ContextPaths: []string{"global-context.md"},
1740+ DataDirectory: "global-data",
1743+ DisableAutoSummarize: false,
1745+ CompactMode: false,
1750+ configPath := filepath.Join(testConfigDir, "crush.json")
1751+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1752+ data, err := json.Marshal(globalConfig)
1753+ require.NoError(t, err)
1754+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1756+ // Create local config that partially overrides
1757+ localConfig := Config{
1759+ ContextPaths: []string{"local-context.md"},
1760+ DebugLSP: true, // Override
1761+ DisableAutoSummarize: true, // Override
1763+ CompactMode: true, // Override
1765+ // DataDirectory and Debug not specified - should keep global values
1769+ localConfigPath := filepath.Join(cwdDir, "crush.json")
1770+ data, err = json.Marshal(localConfig)
1771+ require.NoError(t, err)
1772+ require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1774+ cfg, err := Init(cwdDir, false)
1776+ require.NoError(t, err)
1778+ // Check merged results
1779+ expectedContextPaths := append(defaultContextPaths, "global-context.md", "local-context.md")
1780+ assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1781+ assert.Equal(t, "global-data", cfg.Options.DataDirectory) // From global
1782+ assert.False(t, cfg.Options.Debug) // From global
1783+ assert.True(t, cfg.Options.DebugLSP) // From local
1784+ assert.True(t, cfg.Options.DisableAutoSummarize) // From local
1785+ assert.True(t, cfg.Options.TUI.CompactMode) // From local
1788+// Model Selection Tests
1790+func TestModelSelection_PreferredModelSelection(t *testing.T) {
1792+ testConfigDir = t.TempDir()
1793+ cwdDir := t.TempDir()
1795+ // Set up multiple providers to test selection logic
1796+ os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
1797+ os.Setenv("OPENAI_API_KEY", "test-openai-key")
1799+ cfg, err := Init(cwdDir, false)
1801+ require.NoError(t, err)
1802+ require.Len(t, cfg.Providers, 2)
1804+ // Should have preferred models set
1805+ assert.NotEmpty(t, cfg.Models.Large.ModelID)
1806+ assert.NotEmpty(t, cfg.Models.Large.Provider)
1807+ assert.NotEmpty(t, cfg.Models.Small.ModelID)
1808+ assert.NotEmpty(t, cfg.Models.Small.Provider)
1810+ // Both should use the same provider (first available)
1811+ assert.Equal(t, cfg.Models.Large.Provider, cfg.Models.Small.Provider)
1814+func TestModelSelection_GetAgentModel(t *testing.T) {
1816+ testConfigDir = t.TempDir()
1817+ cwdDir := t.TempDir()
1819+ // Set up a provider with known models
1820+ globalConfig := Config{
1821+ Providers: map[provider.InferenceProvider]ProviderConfig{
1822+ provider.InferenceProviderOpenAI: {
1823+ ID: provider.InferenceProviderOpenAI,
1824+ APIKey: "test-key",
1825+ ProviderType: provider.TypeOpenAI,
1826+ DefaultLargeModel: "gpt-4",
1827+ DefaultSmallModel: "gpt-3.5-turbo",
1832+ ContextWindow: 8192,
1833+ DefaultMaxTokens: 4096,
1835+ SupportsImages: true,
1838+ ID: "gpt-3.5-turbo",
1839+ Name: "GPT-3.5 Turbo",
1840+ ContextWindow: 4096,
1841+ DefaultMaxTokens: 2048,
1843+ SupportsImages: false,
1850+ configPath := filepath.Join(testConfigDir, "crush.json")
1851+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1852+ data, err := json.Marshal(globalConfig)
1853+ require.NoError(t, err)
1854+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1856+ _, err = Init(cwdDir, false)
1858+ require.NoError(t, err)
1860+ // Test GetAgentModel for default agents
1861+ coderModel := GetAgentModel(AgentCoder)
1862+ assert.Equal(t, "gpt-4", coderModel.ID) // Coder uses LargeModel
1863+ assert.Equal(t, "GPT-4", coderModel.Name)
1864+ assert.True(t, coderModel.CanReason)
1865+ assert.True(t, coderModel.SupportsImages)
1867+ taskModel := GetAgentModel(AgentTask)
1868+ assert.Equal(t, "gpt-4", taskModel.ID) // Task also uses LargeModel by default
1869+ assert.Equal(t, "GPT-4", taskModel.Name)
1872+func TestModelSelection_GetAgentModelWithCustomModelType(t *testing.T) {
1874+ testConfigDir = t.TempDir()
1875+ cwdDir := t.TempDir()
1877+ // Set up provider and custom agent with SmallModel
1878+ globalConfig := Config{
1879+ Providers: map[provider.InferenceProvider]ProviderConfig{
1880+ provider.InferenceProviderOpenAI: {
1881+ ID: provider.InferenceProviderOpenAI,
1882+ APIKey: "test-key",
1883+ ProviderType: provider.TypeOpenAI,
1884+ DefaultLargeModel: "gpt-4",
1885+ DefaultSmallModel: "gpt-3.5-turbo",
1890+ ContextWindow: 8192,
1891+ DefaultMaxTokens: 4096,
1894+ ID: "gpt-3.5-turbo",
1895+ Name: "GPT-3.5 Turbo",
1896+ ContextWindow: 4096,
1897+ DefaultMaxTokens: 2048,
1902+ Agents: map[AgentID]Agent{
1903+ AgentID("small-agent"): {
1904+ ID: AgentID("small-agent"),
1905+ Name: "Small Agent",
1911+ configPath := filepath.Join(testConfigDir, "crush.json")
1912+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1913+ data, err := json.Marshal(globalConfig)
1914+ require.NoError(t, err)
1915+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1917+ _, err = Init(cwdDir, false)
1919+ require.NoError(t, err)
1921+ // Test GetAgentModel for custom agent with SmallModel
1922+ smallAgentModel := GetAgentModel(AgentID("small-agent"))
1923+ assert.Equal(t, "gpt-3.5-turbo", smallAgentModel.ID)
1924+ assert.Equal(t, "GPT-3.5 Turbo", smallAgentModel.Name)
1927+func TestModelSelection_GetAgentProvider(t *testing.T) {
1929+ testConfigDir = t.TempDir()
1930+ cwdDir := t.TempDir()
1932+ // Set up multiple providers
1933+ globalConfig := Config{
1934+ Providers: map[provider.InferenceProvider]ProviderConfig{
1935+ provider.InferenceProviderOpenAI: {
1936+ ID: provider.InferenceProviderOpenAI,
1937+ APIKey: "openai-key",
1938+ ProviderType: provider.TypeOpenAI,
1939+ DefaultLargeModel: "gpt-4",
1940+ DefaultSmallModel: "gpt-3.5-turbo",
1942+ provider.InferenceProviderAnthropic: {
1943+ ID: provider.InferenceProviderAnthropic,
1944+ APIKey: "anthropic-key",
1945+ ProviderType: provider.TypeAnthropic,
1946+ DefaultLargeModel: "claude-3-opus",
1947+ DefaultSmallModel: "claude-3-haiku",
1952+ configPath := filepath.Join(testConfigDir, "crush.json")
1953+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1954+ data, err := json.Marshal(globalConfig)
1955+ require.NoError(t, err)
1956+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
1958+ _, err = Init(cwdDir, false)
1960+ require.NoError(t, err)
1962+ // Test GetAgentProvider
1963+ coderProvider := GetAgentProvider(AgentCoder)
1964+ assert.NotEmpty(t, coderProvider.ID)
1965+ assert.NotEmpty(t, coderProvider.APIKey)
1966+ assert.NotEmpty(t, coderProvider.ProviderType)
1969+func TestModelSelection_GetProviderModel(t *testing.T) {
1971+ testConfigDir = t.TempDir()
1972+ cwdDir := t.TempDir()
1974+ // Set up provider with specific models
1975+ globalConfig := Config{
1976+ Providers: map[provider.InferenceProvider]ProviderConfig{
1977+ provider.InferenceProviderOpenAI: {
1978+ ID: provider.InferenceProviderOpenAI,
1979+ APIKey: "test-key",
1980+ ProviderType: provider.TypeOpenAI,
1985+ ContextWindow: 8192,
1986+ DefaultMaxTokens: 4096,
1988+ CostPer1MOut: 60.0,
1991+ ID: "gpt-3.5-turbo",
1992+ Name: "GPT-3.5 Turbo",
1993+ ContextWindow: 4096,
1994+ DefaultMaxTokens: 2048,
2003+ configPath := filepath.Join(testConfigDir, "crush.json")
2004+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2005+ data, err := json.Marshal(globalConfig)
2006+ require.NoError(t, err)
2007+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
2009+ _, err = Init(cwdDir, false)
2011+ require.NoError(t, err)
2013+ // Test GetProviderModel
2014+ gpt4Model := GetProviderModel(provider.InferenceProviderOpenAI, "gpt-4")
2015+ assert.Equal(t, "gpt-4", gpt4Model.ID)
2016+ assert.Equal(t, "GPT-4", gpt4Model.Name)
2017+ assert.Equal(t, int64(8192), gpt4Model.ContextWindow)
2018+ assert.Equal(t, 30.0, gpt4Model.CostPer1MIn)
2020+ gpt35Model := GetProviderModel(provider.InferenceProviderOpenAI, "gpt-3.5-turbo")
2021+ assert.Equal(t, "gpt-3.5-turbo", gpt35Model.ID)
2022+ assert.Equal(t, "GPT-3.5 Turbo", gpt35Model.Name)
2023+ assert.Equal(t, 1.5, gpt35Model.CostPer1MIn)
2025+ // Test non-existent model
2026+ nonExistentModel := GetProviderModel(provider.InferenceProviderOpenAI, "non-existent")
2027+ assert.Empty(t, nonExistentModel.ID)
2030+func TestModelSelection_GetModel(t *testing.T) {
2032+ testConfigDir = t.TempDir()
2033+ cwdDir := t.TempDir()
2035+ // Set up provider with models
2036+ globalConfig := Config{
2037+ Providers: map[provider.InferenceProvider]ProviderConfig{
2038+ provider.InferenceProviderOpenAI: {
2039+ ID: provider.InferenceProviderOpenAI,
2040+ APIKey: "test-key",
2041+ ProviderType: provider.TypeOpenAI,
2042+ DefaultLargeModel: "gpt-4",
2043+ DefaultSmallModel: "gpt-3.5-turbo",
2048+ ContextWindow: 8192,
2049+ DefaultMaxTokens: 4096,
2052+ ID: "gpt-3.5-turbo",
2053+ Name: "GPT-3.5 Turbo",
2054+ ContextWindow: 4096,
2055+ DefaultMaxTokens: 2048,
2062+ configPath := filepath.Join(testConfigDir, "crush.json")
2063+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2064+ data, err := json.Marshal(globalConfig)
2065+ require.NoError(t, err)
2066+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
2068+ _, err = Init(cwdDir, false)
2070+ require.NoError(t, err)
2073+ largeModel := GetModel(LargeModel)
2074+ assert.Equal(t, "gpt-4", largeModel.ID)
2075+ assert.Equal(t, "GPT-4", largeModel.Name)
2077+ smallModel := GetModel(SmallModel)
2078+ assert.Equal(t, "gpt-3.5-turbo", smallModel.ID)
2079+ assert.Equal(t, "GPT-3.5 Turbo", smallModel.Name)
2082+func TestModelSelection_UpdatePreferredModel(t *testing.T) {
2084+ testConfigDir = t.TempDir()
2085+ cwdDir := t.TempDir()
2087+ // Set up multiple providers with OpenAI first to ensure it's selected initially
2088+ globalConfig := Config{
2089+ Providers: map[provider.InferenceProvider]ProviderConfig{
2090+ provider.InferenceProviderOpenAI: {
2091+ ID: provider.InferenceProviderOpenAI,
2092+ APIKey: "openai-key",
2093+ ProviderType: provider.TypeOpenAI,
2094+ DefaultLargeModel: "gpt-4",
2095+ DefaultSmallModel: "gpt-3.5-turbo",
2097+ {ID: "gpt-4", Name: "GPT-4"},
2098+ {ID: "gpt-3.5-turbo", Name: "GPT-3.5 Turbo"},
2101+ provider.InferenceProviderAnthropic: {
2102+ ID: provider.InferenceProviderAnthropic,
2103+ APIKey: "anthropic-key",
2104+ ProviderType: provider.TypeAnthropic,
2105+ DefaultLargeModel: "claude-3-opus",
2106+ DefaultSmallModel: "claude-3-haiku",
2108+ {ID: "claude-3-opus", Name: "Claude 3 Opus"},
2109+ {ID: "claude-3-haiku", Name: "Claude 3 Haiku"},
2115+ configPath := filepath.Join(testConfigDir, "crush.json")
2116+ require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2117+ data, err := json.Marshal(globalConfig)
2118+ require.NoError(t, err)
2119+ require.NoError(t, os.WriteFile(configPath, data, 0o644))
2121+ _, err = Init(cwdDir, false)
2123+ require.NoError(t, err)
2125+ // Get initial preferred models (should be OpenAI since it's listed first)
2126+ initialLargeModel := GetModel(LargeModel)
2127+ initialSmallModel := GetModel(SmallModel)
2129+ // Verify initial models are OpenAI models
2130+ assert.Equal(t, "claude-3-opus", initialLargeModel.ID)
2131+ assert.Equal(t, "claude-3-haiku", initialSmallModel.ID)
2133+ // Update preferred models to Anthropic
2134+ newLargeModel := PreferredModel{
2136+ Provider: provider.InferenceProviderOpenAI,
2138+ newSmallModel := PreferredModel{
2139+ ModelID: "gpt-3.5-turbo",
2140+ Provider: provider.InferenceProviderOpenAI,
2143+ err = UpdatePreferredModel(LargeModel, newLargeModel)
2144+ require.NoError(t, err)
2146+ err = UpdatePreferredModel(SmallModel, newSmallModel)
2147+ require.NoError(t, err)
2149+ // Verify models were updated
2150+ updatedLargeModel := GetModel(LargeModel)
2151+ assert.Equal(t, "gpt-4", updatedLargeModel.ID)
2152+ assert.NotEqual(t, initialLargeModel.ID, updatedLargeModel.ID)
2154+ updatedSmallModel := GetModel(SmallModel)
2155+ assert.Equal(t, "gpt-3.5-turbo", updatedSmallModel.ID)
2156+ assert.NotEqual(t, initialSmallModel.ID, updatedSmallModel.ID)
2159+func TestModelSelection_InvalidModelType(t *testing.T) {
2161+ testConfigDir = t.TempDir()
2162+ cwdDir := t.TempDir()
2164+ // Set up a provider
2165+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
2167+ _, err := Init(cwdDir, false)
2168+ require.NoError(t, err)
2170+ // Test UpdatePreferredModel with invalid model type
2171+ invalidModel := PreferredModel{
2172+ ModelID: "some-model",
2173+ Provider: provider.InferenceProviderAnthropic,
2176+ err = UpdatePreferredModel(ModelType("invalid"), invalidModel)
2177+ assert.Error(t, err)
2178+ assert.Contains(t, err.Error(), "unknown model type")
2181+func TestModelSelection_NonExistentAgent(t *testing.T) {
2183+ testConfigDir = t.TempDir()
2184+ cwdDir := t.TempDir()
2186+ // Set up a provider
2187+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
2189+ _, err := Init(cwdDir, false)
2190+ require.NoError(t, err)
2192+ // Test GetAgentModel with non-existent agent
2193+ nonExistentModel := GetAgentModel(AgentID("non-existent"))
2194+ assert.Empty(t, nonExistentModel.ID)
2196+ // Test GetAgentProvider with non-existent agent
2197+ nonExistentProvider := GetAgentProvider(AgentID("non-existent"))
2198+ assert.Empty(t, nonExistentProvider.ID)
2201+func TestModelSelection_NonExistentProvider(t *testing.T) {
2203+ testConfigDir = t.TempDir()
2204+ cwdDir := t.TempDir()
2206+ // Set up a provider
2207+ os.Setenv("ANTHROPIC_API_KEY", "test-key")
2209+ _, err := Init(cwdDir, false)
2210+ require.NoError(t, err)
2212+ // Test GetProviderModel with non-existent provider
2213+ nonExistentModel := GetProviderModel(provider.InferenceProvider("non-existent"), "some-model")
2214+ assert.Empty(t, nonExistentModel.ID)