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