@@ -1,2075 +0,0 @@
10− "github.com/charmbracelet/crush/internal/fur/provider"
11− "github.com/stretchr/testify/assert"
12− "github.com/stretchr/testify/require"
16− // Clear all environment variables that could affect config
17− envVarsToUnset := []string{
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 {
54− // Enable mock providers for all tests to avoid API calls
55− UseMockProviders = true
59−// Core Configuration Loading Tests
61−func TestInit_ValidWorkingDirectory(t *testing.T) {
63− testConfigDir = t.TempDir()
64− cwdDir := t.TempDir()
66− cfg, err := Init(cwdDir, false)
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)
75−func TestInit_WithDebugFlag(t *testing.T) {
77− testConfigDir = t.TempDir()
78− cwdDir := t.TempDir()
80− cfg, err := Init(cwdDir, true)
82− require.NoError(t, err)
83− assert.True(t, cfg.Options.Debug)
86−func TestInit_SingletonBehavior(t *testing.T) {
88− testConfigDir = t.TempDir()
89− cwdDir := t.TempDir()
91− cfg1, err1 := Init(cwdDir, false)
92− cfg2, err2 := Init(cwdDir, false)
94− require.NoError(t, err1)
95− require.NoError(t, err2)
96− assert.Same(t, cfg1, cfg2)
99−func TestGet_BeforeInitialization(t *testing.T) {
102− assert.Panics(t, func() {
107−func TestGet_AfterInitialization(t *testing.T) {
109− testConfigDir = t.TempDir()
110− cwdDir := t.TempDir()
112− cfg1, err := Init(cwdDir, false)
113− require.NoError(t, err)
116− assert.Same(t, cfg1, cfg2)
119−func TestLoadConfig_NoConfigFiles(t *testing.T) {
121− testConfigDir = t.TempDir()
122− cwdDir := t.TempDir()
124− cfg, err := Init(cwdDir, false)
126− require.NoError(t, err)
127− assert.Len(t, cfg.Providers, 0)
128− assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
131−func TestLoadConfig_OnlyGlobalConfig(t *testing.T) {
133− testConfigDir = t.TempDir()
134− cwdDir := t.TempDir()
136− globalConfig := Config{
137− Providers: map[provider.InferenceProvider]ProviderConfig{
138− provider.InferenceProviderOpenAI: {
139− ID: provider.InferenceProviderOpenAI,
141− ProviderType: provider.TypeOpenAI,
142− DefaultLargeModel: "gpt-4",
143− DefaultSmallModel: "gpt-3.5-turbo",
150− ContextWindow: 8192,
151− DefaultMaxTokens: 4096,
154− ID: "gpt-3.5-turbo",
155− Name: "GPT-3.5 Turbo",
158− ContextWindow: 4096,
159− DefaultMaxTokens: 4096,
165− ContextPaths: []string{"custom-context.md"},
169− configPath := filepath.Join(testConfigDir, "crush.json")
170− require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
172− data, err := json.Marshal(globalConfig)
173− require.NoError(t, err)
174− require.NoError(t, os.WriteFile(configPath, data, 0o644))
176− cfg, err := Init(cwdDir, false)
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")
184−func TestLoadConfig_OnlyLocalConfig(t *testing.T) {
186− testConfigDir = t.TempDir()
187− cwdDir := t.TempDir()
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",
199− ID: "claude-3-opus",
200− Name: "Claude 3 Opus",
203− ContextWindow: 200000,
204− DefaultMaxTokens: 4096,
207− ID: "claude-3-haiku",
208− Name: "Claude 3 Haiku",
211− ContextWindow: 200000,
212− DefaultMaxTokens: 4096,
218− TUI: TUIOptions{CompactMode: true},
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))
227− cfg, err := Init(cwdDir, false)
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)
235−func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
237− testConfigDir = t.TempDir()
238− cwdDir := t.TempDir()
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",
254− ContextWindow: 8192,
255− DefaultMaxTokens: 4096,
258− ID: "gpt-3.5-turbo",
259− Name: "GPT-3.5 Turbo",
262− ContextWindow: 4096,
263− DefaultMaxTokens: 4096,
269− ContextPaths: []string{"global-context.md"},
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))
279− localConfig := Config{
280− Providers: map[provider.InferenceProvider]ProviderConfig{
281− provider.InferenceProviderOpenAI: {
282− APIKey: "local-key", // Override global
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",
292− ID: "claude-3-opus",
293− Name: "Claude 3 Opus",
296− ContextWindow: 200000,
297− DefaultMaxTokens: 4096,
300− ID: "claude-3-haiku",
301− Name: "Claude 3 Haiku",
304− ContextWindow: 200000,
305− DefaultMaxTokens: 4096,
311− ContextPaths: []string{"local-context.md"},
312− TUI: TUIOptions{CompactMode: true},
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))
321− cfg, err := Init(cwdDir, false)
323− require.NoError(t, err)
324− assert.Len(t, cfg.Providers, 2)
326− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
327− assert.Equal(t, "local-key", openaiProvider.APIKey)
329− assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
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)
336−func TestLoadConfig_MalformedGlobalJSON(t *testing.T) {
338− testConfigDir = t.TempDir()
339− cwdDir := t.TempDir()
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))
345− _, err := Init(cwdDir, false)
346− assert.Error(t, err)
349−func TestLoadConfig_MalformedLocalJSON(t *testing.T) {
351− testConfigDir = t.TempDir()
352− cwdDir := t.TempDir()
354− localConfigPath := filepath.Join(cwdDir, "crush.json")
355− require.NoError(t, os.WriteFile(localConfigPath, []byte(`{invalid json`), 0o644))
357− _, err := Init(cwdDir, false)
358− assert.Error(t, err)
361−func TestConfigWithoutEnv(t *testing.T) {
363− testConfigDir = t.TempDir()
364− cwdDir := t.TempDir()
366− cfg, _ := Init(cwdDir, false)
367− assert.Len(t, cfg.Providers, 0)
370−func TestConfigWithEnv(t *testing.T) {
372− testConfigDir = t.TempDir()
373− cwdDir := t.TempDir()
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")
381− cfg, _ := Init(cwdDir, false)
382− assert.Len(t, cfg.Providers, 5)
385−// Environment Variable Tests
387−func TestEnvVars_NoEnvironmentVariables(t *testing.T) {
389− testConfigDir = t.TempDir()
390− cwdDir := t.TempDir()
392− cfg, err := Init(cwdDir, false)
394− require.NoError(t, err)
395− assert.Len(t, cfg.Providers, 0)
398−func TestEnvVars_AllSupportedAPIKeys(t *testing.T) {
400− testConfigDir = t.TempDir()
401− cwdDir := t.TempDir()
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")
409− cfg, err := Init(cwdDir, false)
411− require.NoError(t, err)
412− assert.Len(t, cfg.Providers, 5)
414− anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
415− assert.Equal(t, "test-anthropic-key", anthropicProvider.APIKey)
416− assert.Equal(t, provider.TypeAnthropic, anthropicProvider.ProviderType)
418− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
419− assert.Equal(t, "test-openai-key", openaiProvider.APIKey)
420− assert.Equal(t, provider.TypeOpenAI, openaiProvider.ProviderType)
422− geminiProvider := cfg.Providers[provider.InferenceProviderGemini]
423− assert.Equal(t, "test-gemini-key", geminiProvider.APIKey)
424− assert.Equal(t, provider.TypeGemini, geminiProvider.ProviderType)
426− xaiProvider := cfg.Providers[provider.InferenceProviderXAI]
427− assert.Equal(t, "test-xai-key", xaiProvider.APIKey)
428− assert.Equal(t, provider.TypeXAI, xaiProvider.ProviderType)
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)
436−func TestEnvVars_PartialEnvironmentVariables(t *testing.T) {
438− testConfigDir = t.TempDir()
439− cwdDir := t.TempDir()
441− os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
442− os.Setenv("OPENAI_API_KEY", "test-openai-key")
444− cfg, err := Init(cwdDir, false)
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)
453−func TestEnvVars_VertexAIConfiguration(t *testing.T) {
455− testConfigDir = t.TempDir()
456− cwdDir := t.TempDir()
458− os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
459− os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
460− os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
462− cfg, err := Init(cwdDir, false)
464− require.NoError(t, err)
465− assert.Contains(t, cfg.Providers, provider.InferenceProviderVertexAI)
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"])
473−func TestEnvVars_VertexAIWithoutUseFlag(t *testing.T) {
475− testConfigDir = t.TempDir()
476− cwdDir := t.TempDir()
478− os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
479− os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
481− cfg, err := Init(cwdDir, false)
483− require.NoError(t, err)
484− assert.NotContains(t, cfg.Providers, provider.InferenceProviderVertexAI)
487−func TestEnvVars_AWSBedrockWithAccessKeys(t *testing.T) {
489− testConfigDir = t.TempDir()
490− cwdDir := t.TempDir()
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")
496− cfg, err := Init(cwdDir, false)
498− require.NoError(t, err)
499− assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
501− bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
502− assert.Equal(t, provider.TypeBedrock, bedrockProvider.ProviderType)
503− assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
506−func TestEnvVars_AWSBedrockWithProfile(t *testing.T) {
508− testConfigDir = t.TempDir()
509− cwdDir := t.TempDir()
511− os.Setenv("AWS_PROFILE", "test-profile")
512− os.Setenv("AWS_REGION", "eu-west-1")
514− cfg, err := Init(cwdDir, false)
516− require.NoError(t, err)
517− assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
519− bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
520− assert.Equal(t, "eu-west-1", bedrockProvider.ExtraParams["region"])
523−func TestEnvVars_AWSBedrockWithContainerCredentials(t *testing.T) {
525− testConfigDir = t.TempDir()
526− cwdDir := t.TempDir()
528− os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
529− os.Setenv("AWS_DEFAULT_REGION", "ap-southeast-1")
531− cfg, err := Init(cwdDir, false)
533− require.NoError(t, err)
534− assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
537−func TestEnvVars_AWSBedrockRegionPriority(t *testing.T) {
539− testConfigDir = t.TempDir()
540− cwdDir := t.TempDir()
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")
547− cfg, err := Init(cwdDir, false)
549− require.NoError(t, err)
550− bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
551− assert.Equal(t, "us-west-2", bedrockProvider.ExtraParams["region"])
554−func TestEnvVars_AWSBedrockFallbackRegion(t *testing.T) {
556− testConfigDir = t.TempDir()
557− cwdDir := t.TempDir()
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")
563− cfg, err := Init(cwdDir, false)
565− require.NoError(t, err)
566− bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
567− assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
570−func TestEnvVars_NoAWSCredentials(t *testing.T) {
572− testConfigDir = t.TempDir()
573− cwdDir := t.TempDir()
575− cfg, err := Init(cwdDir, false)
577− require.NoError(t, err)
578− assert.NotContains(t, cfg.Providers, provider.InferenceProviderBedrock)
581−func TestEnvVars_CustomEnvironmentVariables(t *testing.T) {
583− testConfigDir = t.TempDir()
584− cwdDir := t.TempDir()
586− os.Setenv("ANTHROPIC_API_KEY", "resolved-anthropic-key")
588− cfg, err := Init(cwdDir, false)
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)
598−func TestEnvVars_CombinedEnvironmentVariables(t *testing.T) {
600− testConfigDir = t.TempDir()
601− cwdDir := t.TempDir()
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")
612− cfg, err := Init(cwdDir, false)
614− require.NoError(t, err)
616− expectedProviders := []provider.InferenceProvider{
617− provider.InferenceProviderAnthropic,
618− provider.InferenceProviderOpenAI,
619− provider.InferenceProviderVertexAI,
620− provider.InferenceProviderBedrock,
623− for _, expectedProvider := range expectedProviders {
624− assert.Contains(t, cfg.Providers, expectedProvider)
628−func TestHasAWSCredentials_AccessKeys(t *testing.T) {
631− os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
632− os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
634− assert.True(t, hasAWSCredentials())
637−func TestHasAWSCredentials_Profile(t *testing.T) {
640− os.Setenv("AWS_PROFILE", "test-profile")
642− assert.True(t, hasAWSCredentials())
645−func TestHasAWSCredentials_DefaultProfile(t *testing.T) {
648− os.Setenv("AWS_DEFAULT_PROFILE", "default")
650− assert.True(t, hasAWSCredentials())
653−func TestHasAWSCredentials_Region(t *testing.T) {
656− os.Setenv("AWS_REGION", "us-east-1")
658− assert.True(t, hasAWSCredentials())
661−func TestHasAWSCredentials_ContainerCredentials(t *testing.T) {
664− os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
666− assert.True(t, hasAWSCredentials())
669−func TestHasAWSCredentials_NoCredentials(t *testing.T) {
672− assert.False(t, hasAWSCredentials())
675−func TestProviderMerging_GlobalToBase(t *testing.T) {
677− testConfigDir = t.TempDir()
678− cwdDir := t.TempDir()
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",
692− ContextWindow: 8192,
693− DefaultMaxTokens: 4096,
696− ID: "gpt-3.5-turbo",
697− Name: "GPT-3.5 Turbo",
698− ContextWindow: 4096,
699− DefaultMaxTokens: 2048,
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))
712− cfg, err := Init(cwdDir, false)
714− require.NoError(t, err)
715− assert.Len(t, cfg.Providers, 1)
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)
724−func TestProviderMerging_LocalToBase(t *testing.T) {
726− testConfigDir = t.TempDir()
727− cwdDir := t.TempDir()
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",
739− ID: "claude-3-opus",
740− Name: "Claude 3 Opus",
741− ContextWindow: 200000,
742− DefaultMaxTokens: 4096,
747− ID: "claude-3-haiku",
748− Name: "Claude 3 Haiku",
749− ContextWindow: 200000,
750− DefaultMaxTokens: 4096,
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))
764− cfg, err := Init(cwdDir, false)
766− require.NoError(t, err)
767− assert.Len(t, cfg.Providers, 1)
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)
776−func TestProviderMerging_ConflictingSettings(t *testing.T) {
778− testConfigDir = t.TempDir()
779− cwdDir := t.TempDir()
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",
793− ContextWindow: 8192,
794− DefaultMaxTokens: 4096,
797− ID: "gpt-3.5-turbo",
798− Name: "GPT-3.5 Turbo",
799− ContextWindow: 4096,
800− DefaultMaxTokens: 2048,
804− Name: "GPT-4 Turbo",
805− ContextWindow: 128000,
806− DefaultMaxTokens: 4096,
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))
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",
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))
834− cfg, err := Init(cwdDir, false)
836− require.NoError(t, err)
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)
845−func TestProviderMerging_CustomVsKnownProviders(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{
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",
865− ContextWindow: 8192,
866− DefaultMaxTokens: 4096,
869− ID: "gpt-3.5-turbo",
870− Name: "GPT-3.5 Turbo",
871− ContextWindow: 4096,
872− DefaultMaxTokens: 2048,
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",
886− Name: "Custom Large",
887− ContextWindow: 8192,
888− DefaultMaxTokens: 4096,
892− Name: "Custom Small",
893− ContextWindow: 4096,
894− DefaultMaxTokens: 2048,
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
908− BaseURL: "https://updated-custom.api.com",
909− ProviderType: provider.TypeOpenAI,
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))
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))
925− cfg, err := Init(cwdDir, false)
927− require.NoError(t, err)
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)
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)
939−func TestProviderValidation_CustomProviderMissingBaseURL(t *testing.T) {
941− testConfigDir = t.TempDir()
942− cwdDir := t.TempDir()
944− customProviderID := provider.InferenceProvider("custom-provider")
946− globalConfig := Config{
947− Providers: map[provider.InferenceProvider]ProviderConfig{
949− ID: customProviderID,
950− APIKey: "custom-key",
951− ProviderType: provider.TypeOpenAI,
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))
962− cfg, err := Init(cwdDir, false)
964− require.NoError(t, err)
965− assert.NotContains(t, cfg.Providers, customProviderID)
968−func TestProviderValidation_CustomProviderMissingAPIKey(t *testing.T) {
970− testConfigDir = t.TempDir()
971− cwdDir := t.TempDir()
973− customProviderID := provider.InferenceProvider("custom-provider")
975− globalConfig := Config{
976− Providers: map[provider.InferenceProvider]ProviderConfig{
978− ID: customProviderID,
979− BaseURL: "https://custom.api.com",
980− ProviderType: provider.TypeOpenAI,
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))
991− cfg, err := Init(cwdDir, false)
993− require.NoError(t, err)
994− assert.NotContains(t, cfg.Providers, customProviderID)
997−func TestProviderValidation_CustomProviderInvalidType(t *testing.T) {
999− testConfigDir = t.TempDir()
1000− cwdDir := t.TempDir()
1002− customProviderID := provider.InferenceProvider("custom-provider")
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"),
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))
1021− cfg, err := Init(cwdDir, false)
1023− require.NoError(t, err)
1024− assert.NotContains(t, cfg.Providers, customProviderID)
1027−func TestProviderValidation_KnownProviderValid(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,
1038− DefaultLargeModel: "gpt-4",
1039− DefaultSmallModel: "gpt-3.5-turbo",
1044− ContextWindow: 8192,
1045− DefaultMaxTokens: 4096,
1048− ID: "gpt-3.5-turbo",
1049− Name: "GPT-3.5 Turbo",
1050− ContextWindow: 4096,
1051− DefaultMaxTokens: 2048,
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))
1064− cfg, err := Init(cwdDir, false)
1066− require.NoError(t, err)
1067− assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1070−func TestProviderValidation_DisabledProvider(t *testing.T) {
1072− testConfigDir = t.TempDir()
1073− cwdDir := t.TempDir()
1075− globalConfig := Config{
1076− Providers: map[provider.InferenceProvider]ProviderConfig{
1077− provider.InferenceProviderOpenAI: {
1078− ID: provider.InferenceProviderOpenAI,
1079− APIKey: "openai-key",
1080− ProviderType: provider.TypeOpenAI,
1082− DefaultLargeModel: "gpt-4",
1083− DefaultSmallModel: "gpt-3.5-turbo",
1088− ContextWindow: 8192,
1089− DefaultMaxTokens: 4096,
1092− ID: "gpt-3.5-turbo",
1093− Name: "GPT-3.5 Turbo",
1094− ContextWindow: 4096,
1095− DefaultMaxTokens: 2048,
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",
1108− ID: "claude-3-opus",
1109− Name: "Claude 3 Opus",
1110− ContextWindow: 200000,
1111− DefaultMaxTokens: 4096,
1114− ID: "claude-3-haiku",
1115− Name: "Claude 3 Haiku",
1116− ContextWindow: 200000,
1117− DefaultMaxTokens: 4096,
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))
1130− cfg, err := Init(cwdDir, false)
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)
1139−func TestProviderModels_AddingNewModels(t *testing.T) {
1141− testConfigDir = t.TempDir()
1142− cwdDir := t.TempDir()
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",
1156− ContextWindow: 8192,
1157− DefaultMaxTokens: 4096,
1164− localConfig := Config{
1165− Providers: map[provider.InferenceProvider]ProviderConfig{
1166− provider.InferenceProviderOpenAI: {
1170− Name: "GPT-4 Turbo",
1171− ContextWindow: 128000,
1172− DefaultMaxTokens: 4096,
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))
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))
1190− cfg, err := Init(cwdDir, false)
1192− require.NoError(t, err)
1194− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1195− assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
1197− modelIDs := make([]string, len(openaiProvider.Models))
1198− for i, model := range openaiProvider.Models {
1199− modelIDs[i] = model.ID
1201− assert.Contains(t, modelIDs, "gpt-4")
1202− assert.Contains(t, modelIDs, "gpt-4-turbo")
1205−func TestProviderModels_DuplicateModelHandling(t *testing.T) {
1207− testConfigDir = t.TempDir()
1208− cwdDir := t.TempDir()
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",
1222− ContextWindow: 8192,
1223− DefaultMaxTokens: 4096,
1230− localConfig := Config{
1231− Providers: map[provider.InferenceProvider]ProviderConfig{
1232− provider.InferenceProviderOpenAI: {
1236− Name: "GPT-4 Updated",
1237− ContextWindow: 16384,
1238− DefaultMaxTokens: 8192,
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))
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))
1256− cfg, err := Init(cwdDir, false)
1258− require.NoError(t, err)
1260− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1261− assert.GreaterOrEqual(t, len(openaiProvider.Models), 1)
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" {
1272− // If gpt-4 not found, use the first available model
1273− if testModel == nil {
1274− testModel = &openaiProvider.Models[0]
1277− assert.NotEmpty(t, testModel.ID)
1278− assert.NotEmpty(t, testModel.Name)
1279− assert.Greater(t, testModel.ContextWindow, int64(0))
1282−func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
1284− testConfigDir = t.TempDir()
1285− cwdDir := t.TempDir()
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",
1300− CostPer1MOut: 60.0,
1301− CostPer1MInCached: 15.0,
1302− CostPer1MOutCached: 30.0,
1303− ContextWindow: 8192,
1304− DefaultMaxTokens: 4096,
1306− ReasoningEffort: "medium",
1307− SupportsImages: true,
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))
1320− cfg, err := Init(cwdDir, false)
1322− require.NoError(t, err)
1324− openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1325− require.GreaterOrEqual(t, len(openaiProvider.Models), 1)
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" {
1336− if testModel == nil {
1337− testModel = &openaiProvider.Models[0]
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)
1352−func TestDefaultAgents_CoderAgent(t *testing.T) {
1354− testConfigDir = t.TempDir()
1355− cwdDir := t.TempDir()
1357− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1359− cfg, err := Init(cwdDir, false)
1361− require.NoError(t, err)
1362− assert.Contains(t, cfg.Agents, AgentCoder)
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)
1374−func TestDefaultAgents_TaskAgent(t *testing.T) {
1376− testConfigDir = t.TempDir()
1377− cwdDir := t.TempDir()
1379− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1381− cfg, err := Init(cwdDir, false)
1383− require.NoError(t, err)
1384− assert.Contains(t, cfg.Agents, AgentTask)
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)
1394− expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
1395− assert.Equal(t, expectedTools, taskAgent.AllowedTools)
1397− assert.Equal(t, map[string][]string{}, taskAgent.AllowedMCP)
1398− assert.Equal(t, []string{}, taskAgent.AllowedLSP)
1401−func TestAgentMerging_CustomAgent(t *testing.T) {
1403− testConfigDir = t.TempDir()
1404− cwdDir := t.TempDir()
1406− os.Setenv("ANTHROPIC_API_KEY", "test-key")
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",
1415− AllowedTools: []string{"glob", "grep"},
1416− AllowedMCP: map[string][]string{"mcp1": {"tool1", "tool2"}},
1417− AllowedLSP: []string{"typescript", "go"},
1418− ContextPaths: []string{"custom-context.md"},
1421− MCP: map[string]MCP{
1424− Command: "test-mcp-command",
1425− Args: []string{"--test"},
1428− LSP: map[string]LSPConfig{
1430− Command: "typescript-language-server",
1431− Args: []string{"--stdio"},
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))
1446− cfg, err := Init(cwdDir, false)
1448− require.NoError(t, err)
1450− assert.Contains(t, cfg.Agents, AgentCoder)
1451− assert.Contains(t, cfg.Agents, AgentTask)
1452− assert.Contains(t, cfg.Agents, AgentID("custom-agent"))
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)
1465−func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
1467− testConfigDir = t.TempDir()
1468− cwdDir := t.TempDir()
1470− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1472− globalConfig := Config{
1473− Agents: map[AgentID]Agent{
1476− AllowedMCP: map[string][]string{"mcp1": {"tool1"}},
1477− AllowedLSP: []string{"typescript"},
1478− ContextPaths: []string{"coder-specific.md"},
1481− MCP: map[string]MCP{
1484− Command: "test-mcp-command",
1485− Args: []string{"--test"},
1488− LSP: map[string]LSPConfig{
1490− Command: "typescript-language-server",
1491− Args: []string{"--stdio"},
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))
1502− cfg, err := Init(cwdDir, false)
1504− require.NoError(t, err)
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)
1511− expectedContextPaths := append(cfg.Options.ContextPaths, "coder-specific.md")
1512− assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
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)
1519−func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
1521− testConfigDir = t.TempDir()
1522− cwdDir := t.TempDir()
1524− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1526− globalConfig := Config{
1527− Agents: map[AgentID]Agent{
1530− AllowedMCP: map[string][]string{"search-mcp": nil},
1531− AllowedLSP: []string{"python"},
1532− Name: "Search Agent",
1533− Description: "Custom search agent",
1535− AllowedTools: []string{"glob", "grep", "view"},
1538− MCP: map[string]MCP{
1541− Command: "search-mcp-command",
1542− Args: []string{"--search"},
1545− LSP: map[string]LSPConfig{
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))
1559− cfg, err := Init(cwdDir, false)
1561− require.NoError(t, err)
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)
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)
1574−func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
1576− testConfigDir = t.TempDir()
1577− cwdDir := t.TempDir()
1579− os.Setenv("ANTHROPIC_API_KEY", "test-key")
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",
1588− AllowedTools: []string{"glob"},
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))
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",
1607− AllowedTools: []string{"grep", "view"},
1608− AllowedMCP: map[string][]string{"local-mcp": {"tool1"}},
1611− MCP: map[string]MCP{
1614− Command: "local-mcp-command",
1615− Args: []string{"--local"},
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− 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)
1638−func TestAgentModelTypeAssignment(t *testing.T) {
1640− testConfigDir = t.TempDir()
1641− cwdDir := t.TempDir()
1643− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1645− globalConfig := Config{
1646− Agents: map[AgentID]Agent{
1647− AgentID("large-agent"): {
1648− ID: AgentID("large-agent"),
1649− Name: "Large Model Agent",
1652− AgentID("small-agent"): {
1653− ID: AgentID("small-agent"),
1654− Name: "Small Model Agent",
1657− AgentID("default-agent"): {
1658− ID: AgentID("default-agent"),
1659− Name: "Default Model Agent",
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))
1670− cfg, err := Init(cwdDir, false)
1672− require.NoError(t, err)
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)
1679−func TestAgentContextPathOverrides(t *testing.T) {
1681− testConfigDir = t.TempDir()
1682− cwdDir := t.TempDir()
1684− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1686− globalConfig := Config{
1688− ContextPaths: []string{"global-context.md", "shared-context.md"},
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"},
1696− AgentID("default-context-agent"): {
1697− ID: AgentID("default-context-agent"),
1698− Name: "Default Context Agent",
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))
1709− cfg, err := Init(cwdDir, false)
1711− require.NoError(t, err)
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)
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)
1721− coderAgent := cfg.Agents[AgentCoder]
1722− assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1725−func TestOptionsMerging_ContextPaths(t *testing.T) {
1727− testConfigDir = t.TempDir()
1728− cwdDir := t.TempDir()
1730− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1732− globalConfig := Config{
1734− ContextPaths: []string{"global1.md", "global2.md"},
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))
1744− localConfig := Config{
1746− ContextPaths: []string{"local1.md", "local2.md"},
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))
1755− cfg, err := Init(cwdDir, false)
1757− require.NoError(t, err)
1759− expectedContextPaths := append(defaultContextPaths, "global1.md", "global2.md", "local1.md", "local2.md")
1760− assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1763−func TestOptionsMerging_TUIOptions(t *testing.T) {
1765− testConfigDir = t.TempDir()
1766− cwdDir := t.TempDir()
1768− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1770− globalConfig := Config{
1773− CompactMode: false,
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))
1784− localConfig := Config{
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))
1797− cfg, err := Init(cwdDir, false)
1799− require.NoError(t, err)
1801− assert.True(t, cfg.Options.TUI.CompactMode)
1804−func TestOptionsMerging_DebugFlags(t *testing.T) {
1806− testConfigDir = t.TempDir()
1807− cwdDir := t.TempDir()
1809− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1811− globalConfig := Config{
1815− DisableAutoSummarize: false,
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))
1825− localConfig := Config{
1828− DisableAutoSummarize: true,
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))
1837− cfg, err := Init(cwdDir, false)
1839− require.NoError(t, err)
1841− assert.False(t, cfg.Options.Debug)
1842− assert.True(t, cfg.Options.DebugLSP)
1843− assert.True(t, cfg.Options.DisableAutoSummarize)
1846−func TestOptionsMerging_DataDirectory(t *testing.T) {
1848− testConfigDir = t.TempDir()
1849− cwdDir := t.TempDir()
1851− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1853− globalConfig := Config{
1855− DataDirectory: "global-data",
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))
1865− localConfig := Config{
1867− DataDirectory: "local-data",
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))
1876− cfg, err := Init(cwdDir, false)
1878− require.NoError(t, err)
1880− assert.Equal(t, "local-data", cfg.Options.DataDirectory)
1883−func TestOptionsMerging_DefaultValues(t *testing.T) {
1885− testConfigDir = t.TempDir()
1886− cwdDir := t.TempDir()
1888− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1890− cfg, err := Init(cwdDir, false)
1892− require.NoError(t, err)
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)
1902−func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
1904− testConfigDir = t.TempDir()
1905− cwdDir := t.TempDir()
1907− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1909− globalConfig := Config{
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))
1921− cfg, err := Init(cwdDir, true)
1923− require.NoError(t, err)
1925− // Debug flag from Init should take precedence
1926− assert.True(t, cfg.Options.Debug)
1929−func TestOptionsMerging_ComplexScenario(t *testing.T) {
1931− testConfigDir = t.TempDir()
1932− cwdDir := t.TempDir()
1934− // Set up a provider
1935− os.Setenv("ANTHROPIC_API_KEY", "test-key")
1937− // Create global config with various options
1938− globalConfig := Config{
1940− ContextPaths: []string{"global-context.md"},
1941− DataDirectory: "global-data",
1944− DisableAutoSummarize: false,
1946− CompactMode: false,
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))
1957− // Create local config that partially overrides
1958− localConfig := Config{
1960− ContextPaths: []string{"local-context.md"},
1961− DebugLSP: true, // Override
1962− DisableAutoSummarize: true, // Override
1964− CompactMode: true, // Override
1966− // DataDirectory and Debug not specified - should keep global values
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))
1975− cfg, err := Init(cwdDir, false)
1977− require.NoError(t, err)
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
1989−// Model Selection Tests
1991−func TestModelSelection_PreferredModelSelection(t *testing.T) {
1993− testConfigDir = t.TempDir()
1994− cwdDir := t.TempDir()
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")
2000− cfg, err := Init(cwdDir, false)
2002− require.NoError(t, err)
2003− require.Len(t, cfg.Providers, 2)
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)
2011− // Both should use the same provider (first available)
2012− assert.Equal(t, cfg.Models.Large.Provider, cfg.Models.Small.Provider)
2015−func TestValidation_InvalidModelReference(t *testing.T) {
2017− testConfigDir = t.TempDir()
2018− cwdDir := t.TempDir()
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",
2030− ID: "gpt-3.5-turbo",
2031− Name: "GPT-3.5 Turbo",
2032− ContextWindow: 4096,
2033− DefaultMaxTokens: 2048,
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))
2046− _, err = Init(cwdDir, false)
2047− assert.Error(t, err)
2050−func TestValidation_InvalidAgentModelType(t *testing.T) {
2052− testConfigDir = t.TempDir()
2053− cwdDir := t.TempDir()
2055− os.Setenv("ANTHROPIC_API_KEY", "test-key")
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"),
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))
2073− _, err = Init(cwdDir, false)
2074− assert.Error(t, err)