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