config_test.go

   1package config
   2
   3import (
   4	"encoding/json"
   5	"os"
   6	"path/filepath"
   7	"sync"
   8	"testing"
   9
  10	"github.com/charmbracelet/crush/internal/fur/provider"
  11	"github.com/stretchr/testify/assert"
  12	"github.com/stretchr/testify/require"
  13)
  14
  15func reset() {
  16	// Clear all environment variables that could affect config
  17	envVarsToUnset := []string{
  18		// API Keys
  19		"ANTHROPIC_API_KEY",
  20		"OPENAI_API_KEY",
  21		"GEMINI_API_KEY",
  22		"XAI_API_KEY",
  23		"OPENROUTER_API_KEY",
  24
  25		// Google Cloud / VertexAI
  26		"GOOGLE_GENAI_USE_VERTEXAI",
  27		"GOOGLE_CLOUD_PROJECT",
  28		"GOOGLE_CLOUD_LOCATION",
  29
  30		// AWS Credentials
  31		"AWS_ACCESS_KEY_ID",
  32		"AWS_SECRET_ACCESS_KEY",
  33		"AWS_REGION",
  34		"AWS_DEFAULT_REGION",
  35		"AWS_PROFILE",
  36		"AWS_DEFAULT_PROFILE",
  37		"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
  38		"AWS_CONTAINER_CREDENTIALS_FULL_URI",
  39
  40		// Other
  41		"CRUSH_DEV_DEBUG",
  42	}
  43
  44	for _, envVar := range envVarsToUnset {
  45		os.Unsetenv(envVar)
  46	}
  47
  48	// Reset singleton
  49	once = sync.Once{}
  50	instance = nil
  51	cwd = ""
  52	testConfigDir = ""
  53
  54	// Enable mock providers for all tests to avoid API calls
  55	UseMockProviders = true
  56	ResetProviders()
  57}
  58
  59// Core Configuration Loading Tests
  60
  61func TestInit_ValidWorkingDirectory(t *testing.T) {
  62	reset()
  63	testConfigDir = t.TempDir()
  64	cwdDir := t.TempDir()
  65
  66	cfg, err := Init(cwdDir, false)
  67
  68	require.NoError(t, err)
  69	assert.NotNil(t, cfg)
  70	assert.Equal(t, cwdDir, WorkingDirectory())
  71	assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
  72	assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
  73}
  74
  75func TestInit_WithDebugFlag(t *testing.T) {
  76	reset()
  77	testConfigDir = t.TempDir()
  78	cwdDir := t.TempDir()
  79
  80	cfg, err := Init(cwdDir, true)
  81
  82	require.NoError(t, err)
  83	assert.True(t, cfg.Options.Debug)
  84}
  85
  86func TestInit_SingletonBehavior(t *testing.T) {
  87	reset()
  88	testConfigDir = t.TempDir()
  89	cwdDir := t.TempDir()
  90
  91	cfg1, err1 := Init(cwdDir, false)
  92	cfg2, err2 := Init(cwdDir, false)
  93
  94	require.NoError(t, err1)
  95	require.NoError(t, err2)
  96	assert.Same(t, cfg1, cfg2)
  97}
  98
  99func TestGet_BeforeInitialization(t *testing.T) {
 100	reset()
 101
 102	assert.Panics(t, func() {
 103		Get()
 104	})
 105}
 106
 107func TestGet_AfterInitialization(t *testing.T) {
 108	reset()
 109	testConfigDir = t.TempDir()
 110	cwdDir := t.TempDir()
 111
 112	cfg1, err := Init(cwdDir, false)
 113	require.NoError(t, err)
 114
 115	cfg2 := Get()
 116	assert.Same(t, cfg1, cfg2)
 117}
 118
 119func TestLoadConfig_NoConfigFiles(t *testing.T) {
 120	reset()
 121	testConfigDir = t.TempDir()
 122	cwdDir := t.TempDir()
 123
 124	cfg, err := Init(cwdDir, false)
 125
 126	require.NoError(t, err)
 127	assert.Len(t, cfg.Providers, 0)
 128	assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
 129}
 130
 131func TestLoadConfig_OnlyGlobalConfig(t *testing.T) {
 132	reset()
 133	testConfigDir = t.TempDir()
 134	cwdDir := t.TempDir()
 135
 136	globalConfig := Config{
 137		Providers: map[provider.InferenceProvider]ProviderConfig{
 138			provider.InferenceProviderOpenAI: {
 139				ID:                provider.InferenceProviderOpenAI,
 140				APIKey:            "test-key",
 141				ProviderType:      provider.TypeOpenAI,
 142				DefaultLargeModel: "gpt-4",
 143				DefaultSmallModel: "gpt-3.5-turbo",
 144				Models: []Model{
 145					{
 146						ID:               "gpt-4",
 147						Name:             "GPT-4",
 148						CostPer1MIn:      30.0,
 149						CostPer1MOut:     60.0,
 150						ContextWindow:    8192,
 151						DefaultMaxTokens: 4096,
 152					},
 153					{
 154						ID:               "gpt-3.5-turbo",
 155						Name:             "GPT-3.5 Turbo",
 156						CostPer1MIn:      1.0,
 157						CostPer1MOut:     2.0,
 158						ContextWindow:    4096,
 159						DefaultMaxTokens: 4096,
 160					},
 161				},
 162			},
 163		},
 164		Options: Options{
 165			ContextPaths: []string{"custom-context.md"},
 166		},
 167	}
 168
 169	configPath := filepath.Join(testConfigDir, "crush.json")
 170	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
 171
 172	data, err := json.Marshal(globalConfig)
 173	require.NoError(t, err)
 174	require.NoError(t, os.WriteFile(configPath, data, 0o644))
 175
 176	cfg, err := Init(cwdDir, false)
 177
 178	require.NoError(t, err)
 179	assert.Len(t, cfg.Providers, 1)
 180	assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
 181	assert.Contains(t, cfg.Options.ContextPaths, "custom-context.md")
 182}
 183
 184func TestLoadConfig_OnlyLocalConfig(t *testing.T) {
 185	reset()
 186	testConfigDir = t.TempDir()
 187	cwdDir := t.TempDir()
 188
 189	localConfig := Config{
 190		Providers: map[provider.InferenceProvider]ProviderConfig{
 191			provider.InferenceProviderAnthropic: {
 192				ID:                provider.InferenceProviderAnthropic,
 193				APIKey:            "local-key",
 194				ProviderType:      provider.TypeAnthropic,
 195				DefaultLargeModel: "claude-3-opus",
 196				DefaultSmallModel: "claude-3-haiku",
 197				Models: []Model{
 198					{
 199						ID:               "claude-3-opus",
 200						Name:             "Claude 3 Opus",
 201						CostPer1MIn:      15.0,
 202						CostPer1MOut:     75.0,
 203						ContextWindow:    200000,
 204						DefaultMaxTokens: 4096,
 205					},
 206					{
 207						ID:               "claude-3-haiku",
 208						Name:             "Claude 3 Haiku",
 209						CostPer1MIn:      0.25,
 210						CostPer1MOut:     1.25,
 211						ContextWindow:    200000,
 212						DefaultMaxTokens: 4096,
 213					},
 214				},
 215			},
 216		},
 217		Options: Options{
 218			TUI: TUIOptions{CompactMode: true},
 219		},
 220	}
 221
 222	localConfigPath := filepath.Join(cwdDir, "crush.json")
 223	data, err := json.Marshal(localConfig)
 224	require.NoError(t, err)
 225	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
 226
 227	cfg, err := Init(cwdDir, false)
 228
 229	require.NoError(t, err)
 230	assert.Len(t, cfg.Providers, 1)
 231	assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
 232	assert.True(t, cfg.Options.TUI.CompactMode)
 233}
 234
 235func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
 236	reset()
 237	testConfigDir = t.TempDir()
 238	cwdDir := t.TempDir()
 239
 240	globalConfig := Config{
 241		Providers: map[provider.InferenceProvider]ProviderConfig{
 242			provider.InferenceProviderOpenAI: {
 243				ID:                provider.InferenceProviderOpenAI,
 244				APIKey:            "global-key",
 245				ProviderType:      provider.TypeOpenAI,
 246				DefaultLargeModel: "gpt-4",
 247				DefaultSmallModel: "gpt-3.5-turbo",
 248				Models: []Model{
 249					{
 250						ID:               "gpt-4",
 251						Name:             "GPT-4",
 252						CostPer1MIn:      30.0,
 253						CostPer1MOut:     60.0,
 254						ContextWindow:    8192,
 255						DefaultMaxTokens: 4096,
 256					},
 257					{
 258						ID:               "gpt-3.5-turbo",
 259						Name:             "GPT-3.5 Turbo",
 260						CostPer1MIn:      1.0,
 261						CostPer1MOut:     2.0,
 262						ContextWindow:    4096,
 263						DefaultMaxTokens: 4096,
 264					},
 265				},
 266			},
 267		},
 268		Options: Options{
 269			ContextPaths: []string{"global-context.md"},
 270		},
 271	}
 272
 273	configPath := filepath.Join(testConfigDir, "crush.json")
 274	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
 275	data, err := json.Marshal(globalConfig)
 276	require.NoError(t, err)
 277	require.NoError(t, os.WriteFile(configPath, data, 0o644))
 278
 279	localConfig := Config{
 280		Providers: map[provider.InferenceProvider]ProviderConfig{
 281			provider.InferenceProviderOpenAI: {
 282				APIKey: "local-key", // Override global
 283			},
 284			provider.InferenceProviderAnthropic: {
 285				ID:                provider.InferenceProviderAnthropic,
 286				APIKey:            "anthropic-key",
 287				ProviderType:      provider.TypeAnthropic,
 288				DefaultLargeModel: "claude-3-opus",
 289				DefaultSmallModel: "claude-3-haiku",
 290				Models: []Model{
 291					{
 292						ID:               "claude-3-opus",
 293						Name:             "Claude 3 Opus",
 294						CostPer1MIn:      15.0,
 295						CostPer1MOut:     75.0,
 296						ContextWindow:    200000,
 297						DefaultMaxTokens: 4096,
 298					},
 299					{
 300						ID:               "claude-3-haiku",
 301						Name:             "Claude 3 Haiku",
 302						CostPer1MIn:      0.25,
 303						CostPer1MOut:     1.25,
 304						ContextWindow:    200000,
 305						DefaultMaxTokens: 4096,
 306					},
 307				},
 308			},
 309		},
 310		Options: Options{
 311			ContextPaths: []string{"local-context.md"},
 312			TUI:          TUIOptions{CompactMode: true},
 313		},
 314	}
 315
 316	localConfigPath := filepath.Join(cwdDir, "crush.json")
 317	data, err = json.Marshal(localConfig)
 318	require.NoError(t, err)
 319	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
 320
 321	cfg, err := Init(cwdDir, false)
 322
 323	require.NoError(t, err)
 324	assert.Len(t, cfg.Providers, 2)
 325
 326	openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
 327	assert.Equal(t, "local-key", openaiProvider.APIKey)
 328
 329	assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
 330
 331	assert.Contains(t, cfg.Options.ContextPaths, "global-context.md")
 332	assert.Contains(t, cfg.Options.ContextPaths, "local-context.md")
 333	assert.True(t, cfg.Options.TUI.CompactMode)
 334}
 335
 336func TestLoadConfig_MalformedGlobalJSON(t *testing.T) {
 337	reset()
 338	testConfigDir = t.TempDir()
 339	cwdDir := t.TempDir()
 340
 341	configPath := filepath.Join(testConfigDir, "crush.json")
 342	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
 343	require.NoError(t, os.WriteFile(configPath, []byte(`{invalid json`), 0o644))
 344
 345	_, err := Init(cwdDir, false)
 346	assert.Error(t, err)
 347}
 348
 349func TestLoadConfig_MalformedLocalJSON(t *testing.T) {
 350	reset()
 351	testConfigDir = t.TempDir()
 352	cwdDir := t.TempDir()
 353
 354	localConfigPath := filepath.Join(cwdDir, "crush.json")
 355	require.NoError(t, os.WriteFile(localConfigPath, []byte(`{invalid json`), 0o644))
 356
 357	_, err := Init(cwdDir, false)
 358	assert.Error(t, err)
 359}
 360
 361func TestConfigWithoutEnv(t *testing.T) {
 362	reset()
 363	testConfigDir = t.TempDir()
 364	cwdDir := t.TempDir()
 365
 366	cfg, _ := Init(cwdDir, false)
 367	assert.Len(t, cfg.Providers, 0)
 368}
 369
 370func TestConfigWithEnv(t *testing.T) {
 371	reset()
 372	testConfigDir = t.TempDir()
 373	cwdDir := t.TempDir()
 374
 375	os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
 376	os.Setenv("OPENAI_API_KEY", "test-openai-key")
 377	os.Setenv("GEMINI_API_KEY", "test-gemini-key")
 378	os.Setenv("XAI_API_KEY", "test-xai-key")
 379	os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
 380
 381	cfg, _ := Init(cwdDir, false)
 382	assert.Len(t, cfg.Providers, 5)
 383}
 384
 385// Environment Variable Tests
 386
 387func TestEnvVars_NoEnvironmentVariables(t *testing.T) {
 388	reset()
 389	testConfigDir = t.TempDir()
 390	cwdDir := t.TempDir()
 391
 392	cfg, err := Init(cwdDir, false)
 393
 394	require.NoError(t, err)
 395	assert.Len(t, cfg.Providers, 0)
 396}
 397
 398func TestEnvVars_AllSupportedAPIKeys(t *testing.T) {
 399	reset()
 400	testConfigDir = t.TempDir()
 401	cwdDir := t.TempDir()
 402
 403	os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
 404	os.Setenv("OPENAI_API_KEY", "test-openai-key")
 405	os.Setenv("GEMINI_API_KEY", "test-gemini-key")
 406	os.Setenv("XAI_API_KEY", "test-xai-key")
 407	os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
 408
 409	cfg, err := Init(cwdDir, false)
 410
 411	require.NoError(t, err)
 412	assert.Len(t, cfg.Providers, 5)
 413
 414	anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
 415	assert.Equal(t, "test-anthropic-key", anthropicProvider.APIKey)
 416	assert.Equal(t, provider.TypeAnthropic, anthropicProvider.ProviderType)
 417
 418	openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
 419	assert.Equal(t, "test-openai-key", openaiProvider.APIKey)
 420	assert.Equal(t, provider.TypeOpenAI, openaiProvider.ProviderType)
 421
 422	geminiProvider := cfg.Providers[provider.InferenceProviderGemini]
 423	assert.Equal(t, "test-gemini-key", geminiProvider.APIKey)
 424	assert.Equal(t, provider.TypeGemini, geminiProvider.ProviderType)
 425
 426	xaiProvider := cfg.Providers[provider.InferenceProviderXAI]
 427	assert.Equal(t, "test-xai-key", xaiProvider.APIKey)
 428	assert.Equal(t, provider.TypeXAI, xaiProvider.ProviderType)
 429
 430	openrouterProvider := cfg.Providers[provider.InferenceProviderOpenRouter]
 431	assert.Equal(t, "test-openrouter-key", openrouterProvider.APIKey)
 432	assert.Equal(t, provider.TypeOpenAI, openrouterProvider.ProviderType)
 433	assert.Equal(t, "https://openrouter.ai/api/v1", openrouterProvider.BaseURL)
 434}
 435
 436func TestEnvVars_PartialEnvironmentVariables(t *testing.T) {
 437	reset()
 438	testConfigDir = t.TempDir()
 439	cwdDir := t.TempDir()
 440
 441	os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
 442	os.Setenv("OPENAI_API_KEY", "test-openai-key")
 443
 444	cfg, err := Init(cwdDir, false)
 445
 446	require.NoError(t, err)
 447	assert.Len(t, cfg.Providers, 2)
 448	assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
 449	assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
 450	assert.NotContains(t, cfg.Providers, provider.InferenceProviderGemini)
 451}
 452
 453func TestEnvVars_VertexAIConfiguration(t *testing.T) {
 454	reset()
 455	testConfigDir = t.TempDir()
 456	cwdDir := t.TempDir()
 457
 458	os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
 459	os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
 460	os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
 461
 462	cfg, err := Init(cwdDir, false)
 463
 464	require.NoError(t, err)
 465	assert.Contains(t, cfg.Providers, provider.InferenceProviderVertexAI)
 466
 467	vertexProvider := cfg.Providers[provider.InferenceProviderVertexAI]
 468	assert.Equal(t, provider.TypeVertexAI, vertexProvider.ProviderType)
 469	assert.Equal(t, "test-project", vertexProvider.ExtraParams["project"])
 470	assert.Equal(t, "us-central1", vertexProvider.ExtraParams["location"])
 471}
 472
 473func TestEnvVars_VertexAIWithoutUseFlag(t *testing.T) {
 474	reset()
 475	testConfigDir = t.TempDir()
 476	cwdDir := t.TempDir()
 477
 478	os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
 479	os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
 480
 481	cfg, err := Init(cwdDir, false)
 482
 483	require.NoError(t, err)
 484	assert.NotContains(t, cfg.Providers, provider.InferenceProviderVertexAI)
 485}
 486
 487func TestEnvVars_AWSBedrockWithAccessKeys(t *testing.T) {
 488	reset()
 489	testConfigDir = t.TempDir()
 490	cwdDir := t.TempDir()
 491
 492	os.Setenv("AWS_ACCESS_KEY_ID", "test-access-key")
 493	os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
 494	os.Setenv("AWS_DEFAULT_REGION", "us-east-1")
 495
 496	cfg, err := Init(cwdDir, false)
 497
 498	require.NoError(t, err)
 499	assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
 500
 501	bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
 502	assert.Equal(t, provider.TypeBedrock, bedrockProvider.ProviderType)
 503	assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
 504}
 505
 506func TestEnvVars_AWSBedrockWithProfile(t *testing.T) {
 507	reset()
 508	testConfigDir = t.TempDir()
 509	cwdDir := t.TempDir()
 510
 511	os.Setenv("AWS_PROFILE", "test-profile")
 512	os.Setenv("AWS_REGION", "eu-west-1")
 513
 514	cfg, err := Init(cwdDir, false)
 515
 516	require.NoError(t, err)
 517	assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
 518
 519	bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
 520	assert.Equal(t, "eu-west-1", bedrockProvider.ExtraParams["region"])
 521}
 522
 523func TestEnvVars_AWSBedrockWithContainerCredentials(t *testing.T) {
 524	reset()
 525	testConfigDir = t.TempDir()
 526	cwdDir := t.TempDir()
 527
 528	os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
 529	os.Setenv("AWS_DEFAULT_REGION", "ap-southeast-1")
 530
 531	cfg, err := Init(cwdDir, false)
 532
 533	require.NoError(t, err)
 534	assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
 535}
 536
 537func TestEnvVars_AWSBedrockRegionPriority(t *testing.T) {
 538	reset()
 539	testConfigDir = t.TempDir()
 540	cwdDir := t.TempDir()
 541
 542	os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
 543	os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
 544	os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
 545	os.Setenv("AWS_REGION", "us-east-1")
 546
 547	cfg, err := Init(cwdDir, false)
 548
 549	require.NoError(t, err)
 550	bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
 551	assert.Equal(t, "us-west-2", bedrockProvider.ExtraParams["region"])
 552}
 553
 554func TestEnvVars_AWSBedrockFallbackRegion(t *testing.T) {
 555	reset()
 556	testConfigDir = t.TempDir()
 557	cwdDir := t.TempDir()
 558
 559	os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
 560	os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
 561	os.Setenv("AWS_REGION", "us-east-1")
 562
 563	cfg, err := Init(cwdDir, false)
 564
 565	require.NoError(t, err)
 566	bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
 567	assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
 568}
 569
 570func TestEnvVars_NoAWSCredentials(t *testing.T) {
 571	reset()
 572	testConfigDir = t.TempDir()
 573	cwdDir := t.TempDir()
 574
 575	cfg, err := Init(cwdDir, false)
 576
 577	require.NoError(t, err)
 578	assert.NotContains(t, cfg.Providers, provider.InferenceProviderBedrock)
 579}
 580
 581func TestEnvVars_CustomEnvironmentVariables(t *testing.T) {
 582	reset()
 583	testConfigDir = t.TempDir()
 584	cwdDir := t.TempDir()
 585
 586	os.Setenv("ANTHROPIC_API_KEY", "resolved-anthropic-key")
 587
 588	cfg, err := Init(cwdDir, false)
 589
 590	require.NoError(t, err)
 591	if len(cfg.Providers) > 0 {
 592		if anthropicProvider, exists := cfg.Providers[provider.InferenceProviderAnthropic]; exists {
 593			assert.Equal(t, "resolved-anthropic-key", anthropicProvider.APIKey)
 594		}
 595	}
 596}
 597
 598func TestEnvVars_CombinedEnvironmentVariables(t *testing.T) {
 599	reset()
 600	testConfigDir = t.TempDir()
 601	cwdDir := t.TempDir()
 602
 603	os.Setenv("ANTHROPIC_API_KEY", "test-anthropic")
 604	os.Setenv("OPENAI_API_KEY", "test-openai")
 605	os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
 606	os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
 607	os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
 608	os.Setenv("AWS_ACCESS_KEY_ID", "test-aws-key")
 609	os.Setenv("AWS_SECRET_ACCESS_KEY", "test-aws-secret")
 610	os.Setenv("AWS_DEFAULT_REGION", "us-west-1")
 611
 612	cfg, err := Init(cwdDir, false)
 613
 614	require.NoError(t, err)
 615
 616	expectedProviders := []provider.InferenceProvider{
 617		provider.InferenceProviderAnthropic,
 618		provider.InferenceProviderOpenAI,
 619		provider.InferenceProviderVertexAI,
 620		provider.InferenceProviderBedrock,
 621	}
 622
 623	for _, expectedProvider := range expectedProviders {
 624		assert.Contains(t, cfg.Providers, expectedProvider)
 625	}
 626}
 627
 628func TestHasAWSCredentials_AccessKeys(t *testing.T) {
 629	reset()
 630
 631	os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
 632	os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
 633
 634	assert.True(t, hasAWSCredentials())
 635}
 636
 637func TestHasAWSCredentials_Profile(t *testing.T) {
 638	reset()
 639
 640	os.Setenv("AWS_PROFILE", "test-profile")
 641
 642	assert.True(t, hasAWSCredentials())
 643}
 644
 645func TestHasAWSCredentials_DefaultProfile(t *testing.T) {
 646	reset()
 647
 648	os.Setenv("AWS_DEFAULT_PROFILE", "default")
 649
 650	assert.True(t, hasAWSCredentials())
 651}
 652
 653func TestHasAWSCredentials_Region(t *testing.T) {
 654	reset()
 655
 656	os.Setenv("AWS_REGION", "us-east-1")
 657
 658	assert.True(t, hasAWSCredentials())
 659}
 660
 661func TestHasAWSCredentials_ContainerCredentials(t *testing.T) {
 662	reset()
 663
 664	os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
 665
 666	assert.True(t, hasAWSCredentials())
 667}
 668
 669func TestHasAWSCredentials_NoCredentials(t *testing.T) {
 670	reset()
 671
 672	assert.False(t, hasAWSCredentials())
 673}
 674
 675func TestProviderMerging_GlobalToBase(t *testing.T) {
 676	reset()
 677	testConfigDir = t.TempDir()
 678	cwdDir := t.TempDir()
 679
 680	globalConfig := Config{
 681		Providers: map[provider.InferenceProvider]ProviderConfig{
 682			provider.InferenceProviderOpenAI: {
 683				ID:                provider.InferenceProviderOpenAI,
 684				APIKey:            "global-openai-key",
 685				ProviderType:      provider.TypeOpenAI,
 686				DefaultLargeModel: "gpt-4",
 687				DefaultSmallModel: "gpt-3.5-turbo",
 688				Models: []Model{
 689					{
 690						ID:               "gpt-4",
 691						Name:             "GPT-4",
 692						ContextWindow:    8192,
 693						DefaultMaxTokens: 4096,
 694					},
 695					{
 696						ID:               "gpt-3.5-turbo",
 697						Name:             "GPT-3.5 Turbo",
 698						ContextWindow:    4096,
 699						DefaultMaxTokens: 2048,
 700					},
 701				},
 702			},
 703		},
 704	}
 705
 706	configPath := filepath.Join(testConfigDir, "crush.json")
 707	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
 708	data, err := json.Marshal(globalConfig)
 709	require.NoError(t, err)
 710	require.NoError(t, os.WriteFile(configPath, data, 0o644))
 711
 712	cfg, err := Init(cwdDir, false)
 713
 714	require.NoError(t, err)
 715	assert.Len(t, cfg.Providers, 1)
 716
 717	openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
 718	assert.Equal(t, "global-openai-key", openaiProvider.APIKey)
 719	assert.Equal(t, "gpt-4", openaiProvider.DefaultLargeModel)
 720	assert.Equal(t, "gpt-4o", openaiProvider.DefaultSmallModel)
 721	assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
 722}
 723
 724func TestProviderMerging_LocalToBase(t *testing.T) {
 725	reset()
 726	testConfigDir = t.TempDir()
 727	cwdDir := t.TempDir()
 728
 729	localConfig := Config{
 730		Providers: map[provider.InferenceProvider]ProviderConfig{
 731			provider.InferenceProviderAnthropic: {
 732				ID:                provider.InferenceProviderAnthropic,
 733				APIKey:            "local-anthropic-key",
 734				ProviderType:      provider.TypeAnthropic,
 735				DefaultLargeModel: "claude-3-opus",
 736				DefaultSmallModel: "claude-3-haiku",
 737				Models: []Model{
 738					{
 739						ID:               "claude-3-opus",
 740						Name:             "Claude 3 Opus",
 741						ContextWindow:    200000,
 742						DefaultMaxTokens: 4096,
 743						CostPer1MIn:      15.0,
 744						CostPer1MOut:     75.0,
 745					},
 746					{
 747						ID:               "claude-3-haiku",
 748						Name:             "Claude 3 Haiku",
 749						ContextWindow:    200000,
 750						DefaultMaxTokens: 4096,
 751						CostPer1MIn:      0.25,
 752						CostPer1MOut:     1.25,
 753					},
 754				},
 755			},
 756		},
 757	}
 758
 759	localConfigPath := filepath.Join(cwdDir, "crush.json")
 760	data, err := json.Marshal(localConfig)
 761	require.NoError(t, err)
 762	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
 763
 764	cfg, err := Init(cwdDir, false)
 765
 766	require.NoError(t, err)
 767	assert.Len(t, cfg.Providers, 1)
 768
 769	anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
 770	assert.Equal(t, "local-anthropic-key", anthropicProvider.APIKey)
 771	assert.Equal(t, "claude-3-opus", anthropicProvider.DefaultLargeModel)
 772	assert.Equal(t, "claude-3-5-haiku-20241022", anthropicProvider.DefaultSmallModel)
 773	assert.GreaterOrEqual(t, len(anthropicProvider.Models), 2)
 774}
 775
 776func TestProviderMerging_ConflictingSettings(t *testing.T) {
 777	reset()
 778	testConfigDir = t.TempDir()
 779	cwdDir := t.TempDir()
 780
 781	globalConfig := Config{
 782		Providers: map[provider.InferenceProvider]ProviderConfig{
 783			provider.InferenceProviderOpenAI: {
 784				ID:                provider.InferenceProviderOpenAI,
 785				APIKey:            "global-key",
 786				ProviderType:      provider.TypeOpenAI,
 787				DefaultLargeModel: "gpt-4",
 788				DefaultSmallModel: "gpt-3.5-turbo",
 789				Models: []Model{
 790					{
 791						ID:               "gpt-4",
 792						Name:             "GPT-4",
 793						ContextWindow:    8192,
 794						DefaultMaxTokens: 4096,
 795					},
 796					{
 797						ID:               "gpt-3.5-turbo",
 798						Name:             "GPT-3.5 Turbo",
 799						ContextWindow:    4096,
 800						DefaultMaxTokens: 2048,
 801					},
 802					{
 803						ID:               "gpt-4-turbo",
 804						Name:             "GPT-4 Turbo",
 805						ContextWindow:    128000,
 806						DefaultMaxTokens: 4096,
 807					},
 808				},
 809			},
 810		},
 811	}
 812
 813	configPath := filepath.Join(testConfigDir, "crush.json")
 814	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
 815	data, err := json.Marshal(globalConfig)
 816	require.NoError(t, err)
 817	require.NoError(t, os.WriteFile(configPath, data, 0o644))
 818
 819	// Create local config that overrides
 820	localConfig := Config{
 821		Providers: map[provider.InferenceProvider]ProviderConfig{
 822			provider.InferenceProviderOpenAI: {
 823				APIKey:            "local-key",
 824				DefaultLargeModel: "gpt-4-turbo",
 825			},
 826		},
 827	}
 828
 829	localConfigPath := filepath.Join(cwdDir, "crush.json")
 830	data, err = json.Marshal(localConfig)
 831	require.NoError(t, err)
 832	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
 833
 834	cfg, err := Init(cwdDir, false)
 835
 836	require.NoError(t, err)
 837
 838	openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
 839	assert.Equal(t, "local-key", openaiProvider.APIKey)
 840	assert.Equal(t, "gpt-4-turbo", openaiProvider.DefaultLargeModel)
 841	assert.False(t, openaiProvider.Disabled)
 842	assert.Equal(t, "gpt-4o", openaiProvider.DefaultSmallModel)
 843}
 844
 845func TestProviderMerging_CustomVsKnownProviders(t *testing.T) {
 846	reset()
 847	testConfigDir = t.TempDir()
 848	cwdDir := t.TempDir()
 849
 850	customProviderID := provider.InferenceProvider("custom-provider")
 851
 852	globalConfig := Config{
 853		Providers: map[provider.InferenceProvider]ProviderConfig{
 854			provider.InferenceProviderOpenAI: {
 855				ID:                provider.InferenceProviderOpenAI,
 856				APIKey:            "openai-key",
 857				BaseURL:           "should-not-override",
 858				ProviderType:      provider.TypeAnthropic,
 859				DefaultLargeModel: "gpt-4",
 860				DefaultSmallModel: "gpt-3.5-turbo",
 861				Models: []Model{
 862					{
 863						ID:               "gpt-4",
 864						Name:             "GPT-4",
 865						ContextWindow:    8192,
 866						DefaultMaxTokens: 4096,
 867					},
 868					{
 869						ID:               "gpt-3.5-turbo",
 870						Name:             "GPT-3.5 Turbo",
 871						ContextWindow:    4096,
 872						DefaultMaxTokens: 2048,
 873					},
 874				},
 875			},
 876			customProviderID: {
 877				ID:                customProviderID,
 878				APIKey:            "custom-key",
 879				BaseURL:           "https://custom.api.com",
 880				ProviderType:      provider.TypeOpenAI,
 881				DefaultLargeModel: "custom-large",
 882				DefaultSmallModel: "custom-small",
 883				Models: []Model{
 884					{
 885						ID:               "custom-large",
 886						Name:             "Custom Large",
 887						ContextWindow:    8192,
 888						DefaultMaxTokens: 4096,
 889					},
 890					{
 891						ID:               "custom-small",
 892						Name:             "Custom Small",
 893						ContextWindow:    4096,
 894						DefaultMaxTokens: 2048,
 895					},
 896				},
 897			},
 898		},
 899	}
 900
 901	localConfig := Config{
 902		Providers: map[provider.InferenceProvider]ProviderConfig{
 903			provider.InferenceProviderOpenAI: {
 904				BaseURL:      "https://should-not-change.com",
 905				ProviderType: provider.TypeGemini, // Should not change
 906			},
 907			customProviderID: {
 908				BaseURL:      "https://updated-custom.api.com",
 909				ProviderType: provider.TypeOpenAI,
 910			},
 911		},
 912	}
 913
 914	configPath := filepath.Join(testConfigDir, "crush.json")
 915	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
 916	data, err := json.Marshal(globalConfig)
 917	require.NoError(t, err)
 918	require.NoError(t, os.WriteFile(configPath, data, 0o644))
 919
 920	localConfigPath := filepath.Join(cwdDir, "crush.json")
 921	data, err = json.Marshal(localConfig)
 922	require.NoError(t, err)
 923	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
 924
 925	cfg, err := Init(cwdDir, false)
 926
 927	require.NoError(t, err)
 928
 929	openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
 930	assert.NotEqual(t, "https://should-not-change.com", openaiProvider.BaseURL)
 931	assert.NotEqual(t, provider.TypeGemini, openaiProvider.ProviderType)
 932
 933	customProvider := cfg.Providers[customProviderID]
 934	assert.Equal(t, "custom-key", customProvider.APIKey)
 935	assert.Equal(t, "https://updated-custom.api.com", customProvider.BaseURL)
 936	assert.Equal(t, provider.TypeOpenAI, customProvider.ProviderType)
 937}
 938
 939func TestProviderValidation_CustomProviderMissingBaseURL(t *testing.T) {
 940	reset()
 941	testConfigDir = t.TempDir()
 942	cwdDir := t.TempDir()
 943
 944	customProviderID := provider.InferenceProvider("custom-provider")
 945
 946	globalConfig := Config{
 947		Providers: map[provider.InferenceProvider]ProviderConfig{
 948			customProviderID: {
 949				ID:           customProviderID,
 950				APIKey:       "custom-key",
 951				ProviderType: provider.TypeOpenAI,
 952			},
 953		},
 954	}
 955
 956	configPath := filepath.Join(testConfigDir, "crush.json")
 957	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
 958	data, err := json.Marshal(globalConfig)
 959	require.NoError(t, err)
 960	require.NoError(t, os.WriteFile(configPath, data, 0o644))
 961
 962	cfg, err := Init(cwdDir, false)
 963
 964	require.NoError(t, err)
 965	assert.NotContains(t, cfg.Providers, customProviderID)
 966}
 967
 968func TestProviderValidation_CustomProviderMissingAPIKey(t *testing.T) {
 969	reset()
 970	testConfigDir = t.TempDir()
 971	cwdDir := t.TempDir()
 972
 973	customProviderID := provider.InferenceProvider("custom-provider")
 974
 975	globalConfig := Config{
 976		Providers: map[provider.InferenceProvider]ProviderConfig{
 977			customProviderID: {
 978				ID:           customProviderID,
 979				BaseURL:      "https://custom.api.com",
 980				ProviderType: provider.TypeOpenAI,
 981			},
 982		},
 983	}
 984
 985	configPath := filepath.Join(testConfigDir, "crush.json")
 986	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
 987	data, err := json.Marshal(globalConfig)
 988	require.NoError(t, err)
 989	require.NoError(t, os.WriteFile(configPath, data, 0o644))
 990
 991	cfg, err := Init(cwdDir, false)
 992
 993	require.NoError(t, err)
 994	assert.NotContains(t, cfg.Providers, customProviderID)
 995}
 996
 997func TestProviderValidation_CustomProviderInvalidType(t *testing.T) {
 998	reset()
 999	testConfigDir = t.TempDir()
1000	cwdDir := t.TempDir()
1001
1002	customProviderID := provider.InferenceProvider("custom-provider")
1003
1004	globalConfig := Config{
1005		Providers: map[provider.InferenceProvider]ProviderConfig{
1006			customProviderID: {
1007				ID:           customProviderID,
1008				APIKey:       "custom-key",
1009				BaseURL:      "https://custom.api.com",
1010				ProviderType: provider.Type("invalid-type"),
1011			},
1012		},
1013	}
1014
1015	configPath := filepath.Join(testConfigDir, "crush.json")
1016	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1017	data, err := json.Marshal(globalConfig)
1018	require.NoError(t, err)
1019	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1020
1021	cfg, err := Init(cwdDir, false)
1022
1023	require.NoError(t, err)
1024	assert.NotContains(t, cfg.Providers, customProviderID)
1025}
1026
1027func TestProviderValidation_KnownProviderValid(t *testing.T) {
1028	reset()
1029	testConfigDir = t.TempDir()
1030	cwdDir := t.TempDir()
1031
1032	globalConfig := Config{
1033		Providers: map[provider.InferenceProvider]ProviderConfig{
1034			provider.InferenceProviderOpenAI: {
1035				ID:                provider.InferenceProviderOpenAI,
1036				APIKey:            "openai-key",
1037				ProviderType:      provider.TypeOpenAI,
1038				DefaultLargeModel: "gpt-4",
1039				DefaultSmallModel: "gpt-3.5-turbo",
1040				Models: []Model{
1041					{
1042						ID:               "gpt-4",
1043						Name:             "GPT-4",
1044						ContextWindow:    8192,
1045						DefaultMaxTokens: 4096,
1046					},
1047					{
1048						ID:               "gpt-3.5-turbo",
1049						Name:             "GPT-3.5 Turbo",
1050						ContextWindow:    4096,
1051						DefaultMaxTokens: 2048,
1052					},
1053				},
1054			},
1055		},
1056	}
1057
1058	configPath := filepath.Join(testConfigDir, "crush.json")
1059	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1060	data, err := json.Marshal(globalConfig)
1061	require.NoError(t, err)
1062	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1063
1064	cfg, err := Init(cwdDir, false)
1065
1066	require.NoError(t, err)
1067	assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1068}
1069
1070func TestProviderValidation_DisabledProvider(t *testing.T) {
1071	reset()
1072	testConfigDir = t.TempDir()
1073	cwdDir := t.TempDir()
1074
1075	globalConfig := Config{
1076		Providers: map[provider.InferenceProvider]ProviderConfig{
1077			provider.InferenceProviderOpenAI: {
1078				ID:                provider.InferenceProviderOpenAI,
1079				APIKey:            "openai-key",
1080				ProviderType:      provider.TypeOpenAI,
1081				Disabled:          true,
1082				DefaultLargeModel: "gpt-4",
1083				DefaultSmallModel: "gpt-3.5-turbo",
1084				Models: []Model{
1085					{
1086						ID:               "gpt-4",
1087						Name:             "GPT-4",
1088						ContextWindow:    8192,
1089						DefaultMaxTokens: 4096,
1090					},
1091					{
1092						ID:               "gpt-3.5-turbo",
1093						Name:             "GPT-3.5 Turbo",
1094						ContextWindow:    4096,
1095						DefaultMaxTokens: 2048,
1096					},
1097				},
1098			},
1099			provider.InferenceProviderAnthropic: {
1100				ID:                provider.InferenceProviderAnthropic,
1101				APIKey:            "anthropic-key",
1102				ProviderType:      provider.TypeAnthropic,
1103				Disabled:          false, // This one is enabled
1104				DefaultLargeModel: "claude-3-opus",
1105				DefaultSmallModel: "claude-3-haiku",
1106				Models: []Model{
1107					{
1108						ID:               "claude-3-opus",
1109						Name:             "Claude 3 Opus",
1110						ContextWindow:    200000,
1111						DefaultMaxTokens: 4096,
1112					},
1113					{
1114						ID:               "claude-3-haiku",
1115						Name:             "Claude 3 Haiku",
1116						ContextWindow:    200000,
1117						DefaultMaxTokens: 4096,
1118					},
1119				},
1120			},
1121		},
1122	}
1123
1124	configPath := filepath.Join(testConfigDir, "crush.json")
1125	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1126	data, err := json.Marshal(globalConfig)
1127	require.NoError(t, err)
1128	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1129
1130	cfg, err := Init(cwdDir, false)
1131
1132	require.NoError(t, err)
1133	assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
1134	assert.True(t, cfg.Providers[provider.InferenceProviderOpenAI].Disabled)
1135	assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
1136	assert.False(t, cfg.Providers[provider.InferenceProviderAnthropic].Disabled)
1137}
1138
1139func TestProviderModels_AddingNewModels(t *testing.T) {
1140	reset()
1141	testConfigDir = t.TempDir()
1142	cwdDir := t.TempDir()
1143
1144	globalConfig := Config{
1145		Providers: map[provider.InferenceProvider]ProviderConfig{
1146			provider.InferenceProviderOpenAI: {
1147				ID:                provider.InferenceProviderOpenAI,
1148				APIKey:            "openai-key",
1149				ProviderType:      provider.TypeOpenAI,
1150				DefaultLargeModel: "gpt-4",
1151				DefaultSmallModel: "gpt-4-turbo",
1152				Models: []Model{
1153					{
1154						ID:               "gpt-4",
1155						Name:             "GPT-4",
1156						ContextWindow:    8192,
1157						DefaultMaxTokens: 4096,
1158					},
1159				},
1160			},
1161		},
1162	}
1163
1164	localConfig := Config{
1165		Providers: map[provider.InferenceProvider]ProviderConfig{
1166			provider.InferenceProviderOpenAI: {
1167				Models: []Model{
1168					{
1169						ID:               "gpt-4-turbo",
1170						Name:             "GPT-4 Turbo",
1171						ContextWindow:    128000,
1172						DefaultMaxTokens: 4096,
1173					},
1174				},
1175			},
1176		},
1177	}
1178
1179	configPath := filepath.Join(testConfigDir, "crush.json")
1180	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1181	data, err := json.Marshal(globalConfig)
1182	require.NoError(t, err)
1183	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1184
1185	localConfigPath := filepath.Join(cwdDir, "crush.json")
1186	data, err = json.Marshal(localConfig)
1187	require.NoError(t, err)
1188	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1189
1190	cfg, err := Init(cwdDir, false)
1191
1192	require.NoError(t, err)
1193
1194	openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1195	assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
1196
1197	modelIDs := make([]string, len(openaiProvider.Models))
1198	for i, model := range openaiProvider.Models {
1199		modelIDs[i] = model.ID
1200	}
1201	assert.Contains(t, modelIDs, "gpt-4")
1202	assert.Contains(t, modelIDs, "gpt-4-turbo")
1203}
1204
1205func TestProviderModels_DuplicateModelHandling(t *testing.T) {
1206	reset()
1207	testConfigDir = t.TempDir()
1208	cwdDir := t.TempDir()
1209
1210	globalConfig := Config{
1211		Providers: map[provider.InferenceProvider]ProviderConfig{
1212			provider.InferenceProviderOpenAI: {
1213				ID:                provider.InferenceProviderOpenAI,
1214				APIKey:            "openai-key",
1215				ProviderType:      provider.TypeOpenAI,
1216				DefaultLargeModel: "gpt-4",
1217				DefaultSmallModel: "gpt-4",
1218				Models: []Model{
1219					{
1220						ID:               "gpt-4",
1221						Name:             "GPT-4",
1222						ContextWindow:    8192,
1223						DefaultMaxTokens: 4096,
1224					},
1225				},
1226			},
1227		},
1228	}
1229
1230	localConfig := Config{
1231		Providers: map[provider.InferenceProvider]ProviderConfig{
1232			provider.InferenceProviderOpenAI: {
1233				Models: []Model{
1234					{
1235						ID:               "gpt-4",
1236						Name:             "GPT-4 Updated",
1237						ContextWindow:    16384,
1238						DefaultMaxTokens: 8192,
1239					},
1240				},
1241			},
1242		},
1243	}
1244
1245	configPath := filepath.Join(testConfigDir, "crush.json")
1246	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1247	data, err := json.Marshal(globalConfig)
1248	require.NoError(t, err)
1249	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1250
1251	localConfigPath := filepath.Join(cwdDir, "crush.json")
1252	data, err = json.Marshal(localConfig)
1253	require.NoError(t, err)
1254	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1255
1256	cfg, err := Init(cwdDir, false)
1257
1258	require.NoError(t, err)
1259
1260	openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1261	assert.GreaterOrEqual(t, len(openaiProvider.Models), 1)
1262
1263	// Find the first model that matches our test data
1264	var testModel *Model
1265	for _, model := range openaiProvider.Models {
1266		if model.ID == "gpt-4" {
1267			testModel = &model
1268			break
1269		}
1270	}
1271
1272	// If gpt-4 not found, use the first available model
1273	if testModel == nil {
1274		testModel = &openaiProvider.Models[0]
1275	}
1276
1277	assert.NotEmpty(t, testModel.ID)
1278	assert.NotEmpty(t, testModel.Name)
1279	assert.Greater(t, testModel.ContextWindow, int64(0))
1280}
1281
1282func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
1283	reset()
1284	testConfigDir = t.TempDir()
1285	cwdDir := t.TempDir()
1286
1287	globalConfig := Config{
1288		Providers: map[provider.InferenceProvider]ProviderConfig{
1289			provider.InferenceProviderOpenAI: {
1290				ID:                provider.InferenceProviderOpenAI,
1291				APIKey:            "openai-key",
1292				ProviderType:      provider.TypeOpenAI,
1293				DefaultLargeModel: "gpt-4",
1294				DefaultSmallModel: "gpt-4",
1295				Models: []Model{
1296					{
1297						ID:                 "gpt-4",
1298						Name:               "GPT-4",
1299						CostPer1MIn:        30.0,
1300						CostPer1MOut:       60.0,
1301						CostPer1MInCached:  15.0,
1302						CostPer1MOutCached: 30.0,
1303						ContextWindow:      8192,
1304						DefaultMaxTokens:   4096,
1305						CanReason:          true,
1306						ReasoningEffort:    "medium",
1307						SupportsImages:     true,
1308					},
1309				},
1310			},
1311		},
1312	}
1313
1314	configPath := filepath.Join(testConfigDir, "crush.json")
1315	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1316	data, err := json.Marshal(globalConfig)
1317	require.NoError(t, err)
1318	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1319
1320	cfg, err := Init(cwdDir, false)
1321
1322	require.NoError(t, err)
1323
1324	openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
1325	require.GreaterOrEqual(t, len(openaiProvider.Models), 1)
1326
1327	// Find the test model or use the first one
1328	var testModel *Model
1329	for _, model := range openaiProvider.Models {
1330		if model.ID == "gpt-4" {
1331			testModel = &model
1332			break
1333		}
1334	}
1335
1336	if testModel == nil {
1337		testModel = &openaiProvider.Models[0]
1338	}
1339
1340	// Only test the custom properties if this is actually our test model
1341	if testModel.ID == "gpt-4" {
1342		assert.Equal(t, 30.0, testModel.CostPer1MIn)
1343		assert.Equal(t, 60.0, testModel.CostPer1MOut)
1344		assert.Equal(t, 15.0, testModel.CostPer1MInCached)
1345		assert.Equal(t, 30.0, testModel.CostPer1MOutCached)
1346		assert.True(t, testModel.CanReason)
1347		assert.Equal(t, "medium", testModel.ReasoningEffort)
1348		assert.True(t, testModel.SupportsImages)
1349	}
1350}
1351
1352func TestDefaultAgents_CoderAgent(t *testing.T) {
1353	reset()
1354	testConfigDir = t.TempDir()
1355	cwdDir := t.TempDir()
1356
1357	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1358
1359	cfg, err := Init(cwdDir, false)
1360
1361	require.NoError(t, err)
1362	assert.Contains(t, cfg.Agents, AgentCoder)
1363
1364	coderAgent := cfg.Agents[AgentCoder]
1365	assert.Equal(t, AgentCoder, coderAgent.ID)
1366	assert.Equal(t, "Coder", coderAgent.Name)
1367	assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1368	assert.Equal(t, LargeModel, coderAgent.Model)
1369	assert.False(t, coderAgent.Disabled)
1370	assert.Equal(t, cfg.Options.ContextPaths, coderAgent.ContextPaths)
1371	assert.Nil(t, coderAgent.AllowedTools)
1372}
1373
1374func TestDefaultAgents_TaskAgent(t *testing.T) {
1375	reset()
1376	testConfigDir = t.TempDir()
1377	cwdDir := t.TempDir()
1378
1379	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1380
1381	cfg, err := Init(cwdDir, false)
1382
1383	require.NoError(t, err)
1384	assert.Contains(t, cfg.Agents, AgentTask)
1385
1386	taskAgent := cfg.Agents[AgentTask]
1387	assert.Equal(t, AgentTask, taskAgent.ID)
1388	assert.Equal(t, "Task", taskAgent.Name)
1389	assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1390	assert.Equal(t, LargeModel, taskAgent.Model)
1391	assert.False(t, taskAgent.Disabled)
1392	assert.Equal(t, cfg.Options.ContextPaths, taskAgent.ContextPaths)
1393
1394	expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
1395	assert.Equal(t, expectedTools, taskAgent.AllowedTools)
1396
1397	assert.Equal(t, map[string][]string{}, taskAgent.AllowedMCP)
1398	assert.Equal(t, []string{}, taskAgent.AllowedLSP)
1399}
1400
1401func TestAgentMerging_CustomAgent(t *testing.T) {
1402	reset()
1403	testConfigDir = t.TempDir()
1404	cwdDir := t.TempDir()
1405
1406	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1407
1408	globalConfig := Config{
1409		Agents: map[AgentID]Agent{
1410			AgentID("custom-agent"): {
1411				ID:           AgentID("custom-agent"),
1412				Name:         "Custom Agent",
1413				Description:  "A custom agent for testing",
1414				Model:        SmallModel,
1415				AllowedTools: []string{"glob", "grep"},
1416				AllowedMCP:   map[string][]string{"mcp1": {"tool1", "tool2"}},
1417				AllowedLSP:   []string{"typescript", "go"},
1418				ContextPaths: []string{"custom-context.md"},
1419			},
1420		},
1421		MCP: map[string]MCP{
1422			"mcp1": {
1423				Type:    MCPStdio,
1424				Command: "test-mcp-command",
1425				Args:    []string{"--test"},
1426			},
1427		},
1428		LSP: map[string]LSPConfig{
1429			"typescript": {
1430				Command: "typescript-language-server",
1431				Args:    []string{"--stdio"},
1432			},
1433			"go": {
1434				Command: "gopls",
1435				Args:    []string{},
1436			},
1437		},
1438	}
1439
1440	configPath := filepath.Join(testConfigDir, "crush.json")
1441	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1442	data, err := json.Marshal(globalConfig)
1443	require.NoError(t, err)
1444	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1445
1446	cfg, err := Init(cwdDir, false)
1447
1448	require.NoError(t, err)
1449
1450	assert.Contains(t, cfg.Agents, AgentCoder)
1451	assert.Contains(t, cfg.Agents, AgentTask)
1452	assert.Contains(t, cfg.Agents, AgentID("custom-agent"))
1453
1454	customAgent := cfg.Agents[AgentID("custom-agent")]
1455	assert.Equal(t, "Custom Agent", customAgent.Name)
1456	assert.Equal(t, "A custom agent for testing", customAgent.Description)
1457	assert.Equal(t, SmallModel, customAgent.Model)
1458	assert.Equal(t, []string{"glob", "grep"}, customAgent.AllowedTools)
1459	assert.Equal(t, map[string][]string{"mcp1": {"tool1", "tool2"}}, customAgent.AllowedMCP)
1460	assert.Equal(t, []string{"typescript", "go"}, customAgent.AllowedLSP)
1461	expectedContextPaths := append(defaultContextPaths, "custom-context.md")
1462	assert.Equal(t, expectedContextPaths, customAgent.ContextPaths)
1463}
1464
1465func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
1466	reset()
1467	testConfigDir = t.TempDir()
1468	cwdDir := t.TempDir()
1469
1470	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1471
1472	globalConfig := Config{
1473		Agents: map[AgentID]Agent{
1474			AgentCoder: {
1475				Model:        SmallModel,
1476				AllowedMCP:   map[string][]string{"mcp1": {"tool1"}},
1477				AllowedLSP:   []string{"typescript"},
1478				ContextPaths: []string{"coder-specific.md"},
1479			},
1480		},
1481		MCP: map[string]MCP{
1482			"mcp1": {
1483				Type:    MCPStdio,
1484				Command: "test-mcp-command",
1485				Args:    []string{"--test"},
1486			},
1487		},
1488		LSP: map[string]LSPConfig{
1489			"typescript": {
1490				Command: "typescript-language-server",
1491				Args:    []string{"--stdio"},
1492			},
1493		},
1494	}
1495
1496	configPath := filepath.Join(testConfigDir, "crush.json")
1497	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1498	data, err := json.Marshal(globalConfig)
1499	require.NoError(t, err)
1500	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1501
1502	cfg, err := Init(cwdDir, false)
1503
1504	require.NoError(t, err)
1505
1506	coderAgent := cfg.Agents[AgentCoder]
1507	assert.Equal(t, AgentCoder, coderAgent.ID)
1508	assert.Equal(t, "Coder", coderAgent.Name)
1509	assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
1510
1511	expectedContextPaths := append(cfg.Options.ContextPaths, "coder-specific.md")
1512	assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1513
1514	assert.Equal(t, SmallModel, coderAgent.Model)
1515	assert.Equal(t, map[string][]string{"mcp1": {"tool1"}}, coderAgent.AllowedMCP)
1516	assert.Equal(t, []string{"typescript"}, coderAgent.AllowedLSP)
1517}
1518
1519func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
1520	reset()
1521	testConfigDir = t.TempDir()
1522	cwdDir := t.TempDir()
1523
1524	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1525
1526	globalConfig := Config{
1527		Agents: map[AgentID]Agent{
1528			AgentTask: {
1529				Model:        SmallModel,
1530				AllowedMCP:   map[string][]string{"search-mcp": nil},
1531				AllowedLSP:   []string{"python"},
1532				Name:         "Search Agent",
1533				Description:  "Custom search agent",
1534				Disabled:     true,
1535				AllowedTools: []string{"glob", "grep", "view"},
1536			},
1537		},
1538		MCP: map[string]MCP{
1539			"search-mcp": {
1540				Type:    MCPStdio,
1541				Command: "search-mcp-command",
1542				Args:    []string{"--search"},
1543			},
1544		},
1545		LSP: map[string]LSPConfig{
1546			"python": {
1547				Command: "pylsp",
1548				Args:    []string{},
1549			},
1550		},
1551	}
1552
1553	configPath := filepath.Join(testConfigDir, "crush.json")
1554	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1555	data, err := json.Marshal(globalConfig)
1556	require.NoError(t, err)
1557	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1558
1559	cfg, err := Init(cwdDir, false)
1560
1561	require.NoError(t, err)
1562
1563	taskAgent := cfg.Agents[AgentTask]
1564	assert.Equal(t, "Task", taskAgent.Name)
1565	assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
1566	assert.False(t, taskAgent.Disabled)
1567	assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools)
1568
1569	assert.Equal(t, SmallModel, taskAgent.Model)
1570	assert.Equal(t, map[string][]string{"search-mcp": nil}, taskAgent.AllowedMCP)
1571	assert.Equal(t, []string{"python"}, taskAgent.AllowedLSP)
1572}
1573
1574func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
1575	reset()
1576	testConfigDir = t.TempDir()
1577	cwdDir := t.TempDir()
1578
1579	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1580
1581	globalConfig := Config{
1582		Agents: map[AgentID]Agent{
1583			AgentID("test-agent"): {
1584				ID:           AgentID("test-agent"),
1585				Name:         "Global Agent",
1586				Description:  "Global description",
1587				Model:        LargeModel,
1588				AllowedTools: []string{"glob"},
1589			},
1590		},
1591	}
1592
1593	configPath := filepath.Join(testConfigDir, "crush.json")
1594	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1595	data, err := json.Marshal(globalConfig)
1596	require.NoError(t, err)
1597	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1598
1599	// Create local config that overrides
1600	localConfig := Config{
1601		Agents: map[AgentID]Agent{
1602			AgentID("test-agent"): {
1603				Name:         "Local Agent",
1604				Description:  "Local description",
1605				Model:        SmallModel,
1606				Disabled:     true,
1607				AllowedTools: []string{"grep", "view"},
1608				AllowedMCP:   map[string][]string{"local-mcp": {"tool1"}},
1609			},
1610		},
1611		MCP: map[string]MCP{
1612			"local-mcp": {
1613				Type:    MCPStdio,
1614				Command: "local-mcp-command",
1615				Args:    []string{"--local"},
1616			},
1617		},
1618	}
1619
1620	localConfigPath := filepath.Join(cwdDir, "crush.json")
1621	data, err = json.Marshal(localConfig)
1622	require.NoError(t, err)
1623	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1624
1625	cfg, err := Init(cwdDir, false)
1626
1627	require.NoError(t, err)
1628
1629	testAgent := cfg.Agents[AgentID("test-agent")]
1630	assert.Equal(t, "Local Agent", testAgent.Name)
1631	assert.Equal(t, "Local description", testAgent.Description)
1632	assert.Equal(t, SmallModel, testAgent.Model)
1633	assert.True(t, testAgent.Disabled)
1634	assert.Equal(t, []string{"grep", "view"}, testAgent.AllowedTools)
1635	assert.Equal(t, map[string][]string{"local-mcp": {"tool1"}}, testAgent.AllowedMCP)
1636}
1637
1638func TestAgentModelTypeAssignment(t *testing.T) {
1639	reset()
1640	testConfigDir = t.TempDir()
1641	cwdDir := t.TempDir()
1642
1643	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1644
1645	globalConfig := Config{
1646		Agents: map[AgentID]Agent{
1647			AgentID("large-agent"): {
1648				ID:    AgentID("large-agent"),
1649				Name:  "Large Model Agent",
1650				Model: LargeModel,
1651			},
1652			AgentID("small-agent"): {
1653				ID:    AgentID("small-agent"),
1654				Name:  "Small Model Agent",
1655				Model: SmallModel,
1656			},
1657			AgentID("default-agent"): {
1658				ID:   AgentID("default-agent"),
1659				Name: "Default Model Agent",
1660			},
1661		},
1662	}
1663
1664	configPath := filepath.Join(testConfigDir, "crush.json")
1665	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1666	data, err := json.Marshal(globalConfig)
1667	require.NoError(t, err)
1668	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1669
1670	cfg, err := Init(cwdDir, false)
1671
1672	require.NoError(t, err)
1673
1674	assert.Equal(t, LargeModel, cfg.Agents[AgentID("large-agent")].Model)
1675	assert.Equal(t, SmallModel, cfg.Agents[AgentID("small-agent")].Model)
1676	assert.Equal(t, LargeModel, cfg.Agents[AgentID("default-agent")].Model)
1677}
1678
1679func TestAgentContextPathOverrides(t *testing.T) {
1680	reset()
1681	testConfigDir = t.TempDir()
1682	cwdDir := t.TempDir()
1683
1684	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1685
1686	globalConfig := Config{
1687		Options: Options{
1688			ContextPaths: []string{"global-context.md", "shared-context.md"},
1689		},
1690		Agents: map[AgentID]Agent{
1691			AgentID("custom-context-agent"): {
1692				ID:           AgentID("custom-context-agent"),
1693				Name:         "Custom Context Agent",
1694				ContextPaths: []string{"agent-specific.md", "custom.md"},
1695			},
1696			AgentID("default-context-agent"): {
1697				ID:   AgentID("default-context-agent"),
1698				Name: "Default Context Agent",
1699			},
1700		},
1701	}
1702
1703	configPath := filepath.Join(testConfigDir, "crush.json")
1704	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1705	data, err := json.Marshal(globalConfig)
1706	require.NoError(t, err)
1707	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1708
1709	cfg, err := Init(cwdDir, false)
1710
1711	require.NoError(t, err)
1712
1713	customAgent := cfg.Agents[AgentID("custom-context-agent")]
1714	expectedCustomPaths := append(defaultContextPaths, "global-context.md", "shared-context.md", "agent-specific.md", "custom.md")
1715	assert.Equal(t, expectedCustomPaths, customAgent.ContextPaths)
1716
1717	defaultAgent := cfg.Agents[AgentID("default-context-agent")]
1718	expectedContextPaths := append(defaultContextPaths, "global-context.md", "shared-context.md")
1719	assert.Equal(t, expectedContextPaths, defaultAgent.ContextPaths)
1720
1721	coderAgent := cfg.Agents[AgentCoder]
1722	assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
1723}
1724
1725func TestOptionsMerging_ContextPaths(t *testing.T) {
1726	reset()
1727	testConfigDir = t.TempDir()
1728	cwdDir := t.TempDir()
1729
1730	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1731
1732	globalConfig := Config{
1733		Options: Options{
1734			ContextPaths: []string{"global1.md", "global2.md"},
1735		},
1736	}
1737
1738	configPath := filepath.Join(testConfigDir, "crush.json")
1739	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1740	data, err := json.Marshal(globalConfig)
1741	require.NoError(t, err)
1742	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1743
1744	localConfig := Config{
1745		Options: Options{
1746			ContextPaths: []string{"local1.md", "local2.md"},
1747		},
1748	}
1749
1750	localConfigPath := filepath.Join(cwdDir, "crush.json")
1751	data, err = json.Marshal(localConfig)
1752	require.NoError(t, err)
1753	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1754
1755	cfg, err := Init(cwdDir, false)
1756
1757	require.NoError(t, err)
1758
1759	expectedContextPaths := append(defaultContextPaths, "global1.md", "global2.md", "local1.md", "local2.md")
1760	assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1761}
1762
1763func TestOptionsMerging_TUIOptions(t *testing.T) {
1764	reset()
1765	testConfigDir = t.TempDir()
1766	cwdDir := t.TempDir()
1767
1768	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1769
1770	globalConfig := Config{
1771		Options: Options{
1772			TUI: TUIOptions{
1773				CompactMode: false,
1774			},
1775		},
1776	}
1777
1778	configPath := filepath.Join(testConfigDir, "crush.json")
1779	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1780	data, err := json.Marshal(globalConfig)
1781	require.NoError(t, err)
1782	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1783
1784	localConfig := Config{
1785		Options: Options{
1786			TUI: TUIOptions{
1787				CompactMode: true,
1788			},
1789		},
1790	}
1791
1792	localConfigPath := filepath.Join(cwdDir, "crush.json")
1793	data, err = json.Marshal(localConfig)
1794	require.NoError(t, err)
1795	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1796
1797	cfg, err := Init(cwdDir, false)
1798
1799	require.NoError(t, err)
1800
1801	assert.True(t, cfg.Options.TUI.CompactMode)
1802}
1803
1804func TestOptionsMerging_DebugFlags(t *testing.T) {
1805	reset()
1806	testConfigDir = t.TempDir()
1807	cwdDir := t.TempDir()
1808
1809	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1810
1811	globalConfig := Config{
1812		Options: Options{
1813			Debug:                false,
1814			DebugLSP:             false,
1815			DisableAutoSummarize: false,
1816		},
1817	}
1818
1819	configPath := filepath.Join(testConfigDir, "crush.json")
1820	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1821	data, err := json.Marshal(globalConfig)
1822	require.NoError(t, err)
1823	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1824
1825	localConfig := Config{
1826		Options: Options{
1827			DebugLSP:             true,
1828			DisableAutoSummarize: true,
1829		},
1830	}
1831
1832	localConfigPath := filepath.Join(cwdDir, "crush.json")
1833	data, err = json.Marshal(localConfig)
1834	require.NoError(t, err)
1835	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1836
1837	cfg, err := Init(cwdDir, false)
1838
1839	require.NoError(t, err)
1840
1841	assert.False(t, cfg.Options.Debug)
1842	assert.True(t, cfg.Options.DebugLSP)
1843	assert.True(t, cfg.Options.DisableAutoSummarize)
1844}
1845
1846func TestOptionsMerging_DataDirectory(t *testing.T) {
1847	reset()
1848	testConfigDir = t.TempDir()
1849	cwdDir := t.TempDir()
1850
1851	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1852
1853	globalConfig := Config{
1854		Options: Options{
1855			DataDirectory: "global-data",
1856		},
1857	}
1858
1859	configPath := filepath.Join(testConfigDir, "crush.json")
1860	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1861	data, err := json.Marshal(globalConfig)
1862	require.NoError(t, err)
1863	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1864
1865	localConfig := Config{
1866		Options: Options{
1867			DataDirectory: "local-data",
1868		},
1869	}
1870
1871	localConfigPath := filepath.Join(cwdDir, "crush.json")
1872	data, err = json.Marshal(localConfig)
1873	require.NoError(t, err)
1874	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1875
1876	cfg, err := Init(cwdDir, false)
1877
1878	require.NoError(t, err)
1879
1880	assert.Equal(t, "local-data", cfg.Options.DataDirectory)
1881}
1882
1883func TestOptionsMerging_DefaultValues(t *testing.T) {
1884	reset()
1885	testConfigDir = t.TempDir()
1886	cwdDir := t.TempDir()
1887
1888	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1889
1890	cfg, err := Init(cwdDir, false)
1891
1892	require.NoError(t, err)
1893
1894	assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
1895	assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
1896	assert.False(t, cfg.Options.TUI.CompactMode)
1897	assert.False(t, cfg.Options.Debug)
1898	assert.False(t, cfg.Options.DebugLSP)
1899	assert.False(t, cfg.Options.DisableAutoSummarize)
1900}
1901
1902func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
1903	reset()
1904	testConfigDir = t.TempDir()
1905	cwdDir := t.TempDir()
1906
1907	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1908
1909	globalConfig := Config{
1910		Options: Options{
1911			Debug: false,
1912		},
1913	}
1914
1915	configPath := filepath.Join(testConfigDir, "crush.json")
1916	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1917	data, err := json.Marshal(globalConfig)
1918	require.NoError(t, err)
1919	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1920
1921	cfg, err := Init(cwdDir, true)
1922
1923	require.NoError(t, err)
1924
1925	// Debug flag from Init should take precedence
1926	assert.True(t, cfg.Options.Debug)
1927}
1928
1929func TestOptionsMerging_ComplexScenario(t *testing.T) {
1930	reset()
1931	testConfigDir = t.TempDir()
1932	cwdDir := t.TempDir()
1933
1934	// Set up a provider
1935	os.Setenv("ANTHROPIC_API_KEY", "test-key")
1936
1937	// Create global config with various options
1938	globalConfig := Config{
1939		Options: Options{
1940			ContextPaths:         []string{"global-context.md"},
1941			DataDirectory:        "global-data",
1942			Debug:                false,
1943			DebugLSP:             false,
1944			DisableAutoSummarize: false,
1945			TUI: TUIOptions{
1946				CompactMode: false,
1947			},
1948		},
1949	}
1950
1951	configPath := filepath.Join(testConfigDir, "crush.json")
1952	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
1953	data, err := json.Marshal(globalConfig)
1954	require.NoError(t, err)
1955	require.NoError(t, os.WriteFile(configPath, data, 0o644))
1956
1957	// Create local config that partially overrides
1958	localConfig := Config{
1959		Options: Options{
1960			ContextPaths:         []string{"local-context.md"},
1961			DebugLSP:             true, // Override
1962			DisableAutoSummarize: true, // Override
1963			TUI: TUIOptions{
1964				CompactMode: true, // Override
1965			},
1966			// DataDirectory and Debug not specified - should keep global values
1967		},
1968	}
1969
1970	localConfigPath := filepath.Join(cwdDir, "crush.json")
1971	data, err = json.Marshal(localConfig)
1972	require.NoError(t, err)
1973	require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
1974
1975	cfg, err := Init(cwdDir, false)
1976
1977	require.NoError(t, err)
1978
1979	// Check merged results
1980	expectedContextPaths := append(defaultContextPaths, "global-context.md", "local-context.md")
1981	assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
1982	assert.Equal(t, "global-data", cfg.Options.DataDirectory) // From global
1983	assert.False(t, cfg.Options.Debug)                        // From global
1984	assert.True(t, cfg.Options.DebugLSP)                      // From local
1985	assert.True(t, cfg.Options.DisableAutoSummarize)          // From local
1986	assert.True(t, cfg.Options.TUI.CompactMode)               // From local
1987}
1988
1989// Model Selection Tests
1990
1991func TestModelSelection_PreferredModelSelection(t *testing.T) {
1992	reset()
1993	testConfigDir = t.TempDir()
1994	cwdDir := t.TempDir()
1995
1996	// Set up multiple providers to test selection logic
1997	os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
1998	os.Setenv("OPENAI_API_KEY", "test-openai-key")
1999
2000	cfg, err := Init(cwdDir, false)
2001
2002	require.NoError(t, err)
2003	require.Len(t, cfg.Providers, 2)
2004
2005	// Should have preferred models set
2006	assert.NotEmpty(t, cfg.Models.Large.ModelID)
2007	assert.NotEmpty(t, cfg.Models.Large.Provider)
2008	assert.NotEmpty(t, cfg.Models.Small.ModelID)
2009	assert.NotEmpty(t, cfg.Models.Small.Provider)
2010
2011	// Both should use the same provider (first available)
2012	assert.Equal(t, cfg.Models.Large.Provider, cfg.Models.Small.Provider)
2013}
2014
2015func TestValidation_InvalidModelReference(t *testing.T) {
2016	reset()
2017	testConfigDir = t.TempDir()
2018	cwdDir := t.TempDir()
2019
2020	globalConfig := Config{
2021		Providers: map[provider.InferenceProvider]ProviderConfig{
2022			provider.InferenceProviderOpenAI: {
2023				ID:                provider.InferenceProviderOpenAI,
2024				APIKey:            "test-key",
2025				ProviderType:      provider.TypeOpenAI,
2026				DefaultLargeModel: "non-existent-model",
2027				DefaultSmallModel: "gpt-3.5-turbo",
2028				Models: []Model{
2029					{
2030						ID:               "gpt-3.5-turbo",
2031						Name:             "GPT-3.5 Turbo",
2032						ContextWindow:    4096,
2033						DefaultMaxTokens: 2048,
2034					},
2035				},
2036			},
2037		},
2038	}
2039
2040	configPath := filepath.Join(testConfigDir, "crush.json")
2041	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2042	data, err := json.Marshal(globalConfig)
2043	require.NoError(t, err)
2044	require.NoError(t, os.WriteFile(configPath, data, 0o644))
2045
2046	_, err = Init(cwdDir, false)
2047	assert.Error(t, err)
2048}
2049
2050func TestValidation_InvalidAgentModelType(t *testing.T) {
2051	reset()
2052	testConfigDir = t.TempDir()
2053	cwdDir := t.TempDir()
2054
2055	os.Setenv("ANTHROPIC_API_KEY", "test-key")
2056
2057	globalConfig := Config{
2058		Agents: map[AgentID]Agent{
2059			AgentID("invalid-agent"): {
2060				ID:    AgentID("invalid-agent"),
2061				Name:  "Invalid Agent",
2062				Model: ModelType("invalid"),
2063			},
2064		},
2065	}
2066
2067	configPath := filepath.Join(testConfigDir, "crush.json")
2068	require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
2069	data, err := json.Marshal(globalConfig)
2070	require.NoError(t, err)
2071	require.NoError(t, os.WriteFile(configPath, data, 0o644))
2072
2073	_, err = Init(cwdDir, false)
2074	assert.Error(t, err)
2075}