mirror of
https://github.com/charmbracelet/crush.git
synced 2025-08-02 05:20:46 +03:00
2076 lines
57 KiB
Go
2076 lines
57 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/charmbracelet/crush/internal/fur/provider"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func reset() {
|
|
// Clear all environment variables that could affect config
|
|
envVarsToUnset := []string{
|
|
// API Keys
|
|
"ANTHROPIC_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"GEMINI_API_KEY",
|
|
"XAI_API_KEY",
|
|
"OPENROUTER_API_KEY",
|
|
|
|
// Google Cloud / VertexAI
|
|
"GOOGLE_GENAI_USE_VERTEXAI",
|
|
"GOOGLE_CLOUD_PROJECT",
|
|
"GOOGLE_CLOUD_LOCATION",
|
|
|
|
// AWS Credentials
|
|
"AWS_ACCESS_KEY_ID",
|
|
"AWS_SECRET_ACCESS_KEY",
|
|
"AWS_REGION",
|
|
"AWS_DEFAULT_REGION",
|
|
"AWS_PROFILE",
|
|
"AWS_DEFAULT_PROFILE",
|
|
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
|
|
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
|
|
|
|
// Other
|
|
"CRUSH_DEV_DEBUG",
|
|
}
|
|
|
|
for _, envVar := range envVarsToUnset {
|
|
os.Unsetenv(envVar)
|
|
}
|
|
|
|
// Reset singleton
|
|
once = sync.Once{}
|
|
instance = nil
|
|
cwd = ""
|
|
testConfigDir = ""
|
|
|
|
// Enable mock providers for all tests to avoid API calls
|
|
UseMockProviders = true
|
|
ResetProviders()
|
|
}
|
|
|
|
// Core Configuration Loading Tests
|
|
|
|
func TestInit_ValidWorkingDirectory(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, cfg)
|
|
assert.Equal(t, cwdDir, WorkingDirectory())
|
|
assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
|
|
assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
|
|
}
|
|
|
|
func TestInit_WithDebugFlag(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
cfg, err := Init(cwdDir, true)
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, cfg.Options.Debug)
|
|
}
|
|
|
|
func TestInit_SingletonBehavior(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
cfg1, err1 := Init(cwdDir, false)
|
|
cfg2, err2 := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err1)
|
|
require.NoError(t, err2)
|
|
assert.Same(t, cfg1, cfg2)
|
|
}
|
|
|
|
func TestGet_BeforeInitialization(t *testing.T) {
|
|
reset()
|
|
|
|
assert.Panics(t, func() {
|
|
Get()
|
|
})
|
|
}
|
|
|
|
func TestGet_AfterInitialization(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
cfg1, err := Init(cwdDir, false)
|
|
require.NoError(t, err)
|
|
|
|
cfg2 := Get()
|
|
assert.Same(t, cfg1, cfg2)
|
|
}
|
|
|
|
func TestLoadConfig_NoConfigFiles(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 0)
|
|
assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
|
|
}
|
|
|
|
func TestLoadConfig_OnlyGlobalConfig(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "test-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-3.5-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
CostPer1MIn: 30.0,
|
|
CostPer1MOut: 60.0,
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "gpt-3.5-turbo",
|
|
Name: "GPT-3.5 Turbo",
|
|
CostPer1MIn: 1.0,
|
|
CostPer1MOut: 2.0,
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Options: Options{
|
|
ContextPaths: []string{"custom-context.md"},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 1)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
|
|
assert.Contains(t, cfg.Options.ContextPaths, "custom-context.md")
|
|
}
|
|
|
|
func TestLoadConfig_OnlyLocalConfig(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
localConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderAnthropic: {
|
|
ID: provider.InferenceProviderAnthropic,
|
|
APIKey: "local-key",
|
|
ProviderType: provider.TypeAnthropic,
|
|
DefaultLargeModel: "claude-3-opus",
|
|
DefaultSmallModel: "claude-3-haiku",
|
|
Models: []Model{
|
|
{
|
|
ID: "claude-3-opus",
|
|
Name: "Claude 3 Opus",
|
|
CostPer1MIn: 15.0,
|
|
CostPer1MOut: 75.0,
|
|
ContextWindow: 200000,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "claude-3-haiku",
|
|
Name: "Claude 3 Haiku",
|
|
CostPer1MIn: 0.25,
|
|
CostPer1MOut: 1.25,
|
|
ContextWindow: 200000,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Options: Options{
|
|
TUI: TUIOptions{CompactMode: true},
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err := json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 1)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
|
|
assert.True(t, cfg.Options.TUI.CompactMode)
|
|
}
|
|
|
|
func TestLoadConfig_BothGlobalAndLocal(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "global-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-3.5-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
CostPer1MIn: 30.0,
|
|
CostPer1MOut: 60.0,
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "gpt-3.5-turbo",
|
|
Name: "GPT-3.5 Turbo",
|
|
CostPer1MIn: 1.0,
|
|
CostPer1MOut: 2.0,
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Options: Options{
|
|
ContextPaths: []string{"global-context.md"},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
localConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
APIKey: "local-key", // Override global
|
|
},
|
|
provider.InferenceProviderAnthropic: {
|
|
ID: provider.InferenceProviderAnthropic,
|
|
APIKey: "anthropic-key",
|
|
ProviderType: provider.TypeAnthropic,
|
|
DefaultLargeModel: "claude-3-opus",
|
|
DefaultSmallModel: "claude-3-haiku",
|
|
Models: []Model{
|
|
{
|
|
ID: "claude-3-opus",
|
|
Name: "Claude 3 Opus",
|
|
CostPer1MIn: 15.0,
|
|
CostPer1MOut: 75.0,
|
|
ContextWindow: 200000,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "claude-3-haiku",
|
|
Name: "Claude 3 Haiku",
|
|
CostPer1MIn: 0.25,
|
|
CostPer1MOut: 1.25,
|
|
ContextWindow: 200000,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Options: Options{
|
|
ContextPaths: []string{"local-context.md"},
|
|
TUI: TUIOptions{CompactMode: true},
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 2)
|
|
|
|
openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
|
|
assert.Equal(t, "local-key", openaiProvider.APIKey)
|
|
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
|
|
|
|
assert.Contains(t, cfg.Options.ContextPaths, "global-context.md")
|
|
assert.Contains(t, cfg.Options.ContextPaths, "local-context.md")
|
|
assert.True(t, cfg.Options.TUI.CompactMode)
|
|
}
|
|
|
|
func TestLoadConfig_MalformedGlobalJSON(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
require.NoError(t, os.WriteFile(configPath, []byte(`{invalid json`), 0o644))
|
|
|
|
_, err := Init(cwdDir, false)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestLoadConfig_MalformedLocalJSON(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
require.NoError(t, os.WriteFile(localConfigPath, []byte(`{invalid json`), 0o644))
|
|
|
|
_, err := Init(cwdDir, false)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestConfigWithoutEnv(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
cfg, _ := Init(cwdDir, false)
|
|
assert.Len(t, cfg.Providers, 0)
|
|
}
|
|
|
|
func TestConfigWithEnv(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
|
|
os.Setenv("OPENAI_API_KEY", "test-openai-key")
|
|
os.Setenv("GEMINI_API_KEY", "test-gemini-key")
|
|
os.Setenv("XAI_API_KEY", "test-xai-key")
|
|
os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
|
|
|
|
cfg, _ := Init(cwdDir, false)
|
|
assert.Len(t, cfg.Providers, 5)
|
|
}
|
|
|
|
// Environment Variable Tests
|
|
|
|
func TestEnvVars_NoEnvironmentVariables(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 0)
|
|
}
|
|
|
|
func TestEnvVars_AllSupportedAPIKeys(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
|
|
os.Setenv("OPENAI_API_KEY", "test-openai-key")
|
|
os.Setenv("GEMINI_API_KEY", "test-gemini-key")
|
|
os.Setenv("XAI_API_KEY", "test-xai-key")
|
|
os.Setenv("OPENROUTER_API_KEY", "test-openrouter-key")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 5)
|
|
|
|
anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
|
|
assert.Equal(t, "test-anthropic-key", anthropicProvider.APIKey)
|
|
assert.Equal(t, provider.TypeAnthropic, anthropicProvider.ProviderType)
|
|
|
|
openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
|
|
assert.Equal(t, "test-openai-key", openaiProvider.APIKey)
|
|
assert.Equal(t, provider.TypeOpenAI, openaiProvider.ProviderType)
|
|
|
|
geminiProvider := cfg.Providers[provider.InferenceProviderGemini]
|
|
assert.Equal(t, "test-gemini-key", geminiProvider.APIKey)
|
|
assert.Equal(t, provider.TypeGemini, geminiProvider.ProviderType)
|
|
|
|
xaiProvider := cfg.Providers[provider.InferenceProviderXAI]
|
|
assert.Equal(t, "test-xai-key", xaiProvider.APIKey)
|
|
assert.Equal(t, provider.TypeXAI, xaiProvider.ProviderType)
|
|
|
|
openrouterProvider := cfg.Providers[provider.InferenceProviderOpenRouter]
|
|
assert.Equal(t, "test-openrouter-key", openrouterProvider.APIKey)
|
|
assert.Equal(t, provider.TypeOpenAI, openrouterProvider.ProviderType)
|
|
assert.Equal(t, "https://openrouter.ai/api/v1", openrouterProvider.BaseURL)
|
|
}
|
|
|
|
func TestEnvVars_PartialEnvironmentVariables(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
|
|
os.Setenv("OPENAI_API_KEY", "test-openai-key")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 2)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
|
|
assert.NotContains(t, cfg.Providers, provider.InferenceProviderGemini)
|
|
}
|
|
|
|
func TestEnvVars_VertexAIConfiguration(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
|
|
os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
|
|
os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderVertexAI)
|
|
|
|
vertexProvider := cfg.Providers[provider.InferenceProviderVertexAI]
|
|
assert.Equal(t, provider.TypeVertexAI, vertexProvider.ProviderType)
|
|
assert.Equal(t, "test-project", vertexProvider.ExtraParams["project"])
|
|
assert.Equal(t, "us-central1", vertexProvider.ExtraParams["location"])
|
|
}
|
|
|
|
func TestEnvVars_VertexAIWithoutUseFlag(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
|
|
os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, cfg.Providers, provider.InferenceProviderVertexAI)
|
|
}
|
|
|
|
func TestEnvVars_AWSBedrockWithAccessKeys(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("AWS_ACCESS_KEY_ID", "test-access-key")
|
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
|
|
os.Setenv("AWS_DEFAULT_REGION", "us-east-1")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
|
|
|
|
bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
|
|
assert.Equal(t, provider.TypeBedrock, bedrockProvider.ProviderType)
|
|
assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
|
|
}
|
|
|
|
func TestEnvVars_AWSBedrockWithProfile(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("AWS_PROFILE", "test-profile")
|
|
os.Setenv("AWS_REGION", "eu-west-1")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
|
|
|
|
bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
|
|
assert.Equal(t, "eu-west-1", bedrockProvider.ExtraParams["region"])
|
|
}
|
|
|
|
func TestEnvVars_AWSBedrockWithContainerCredentials(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
|
|
os.Setenv("AWS_DEFAULT_REGION", "ap-southeast-1")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderBedrock)
|
|
}
|
|
|
|
func TestEnvVars_AWSBedrockRegionPriority(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
|
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
|
|
os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
|
|
os.Setenv("AWS_REGION", "us-east-1")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
|
|
assert.Equal(t, "us-west-2", bedrockProvider.ExtraParams["region"])
|
|
}
|
|
|
|
func TestEnvVars_AWSBedrockFallbackRegion(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
|
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
|
|
os.Setenv("AWS_REGION", "us-east-1")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
bedrockProvider := cfg.Providers[provider.InferenceProviderBedrock]
|
|
assert.Equal(t, "us-east-1", bedrockProvider.ExtraParams["region"])
|
|
}
|
|
|
|
func TestEnvVars_NoAWSCredentials(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, cfg.Providers, provider.InferenceProviderBedrock)
|
|
}
|
|
|
|
func TestEnvVars_CustomEnvironmentVariables(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "resolved-anthropic-key")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
if len(cfg.Providers) > 0 {
|
|
if anthropicProvider, exists := cfg.Providers[provider.InferenceProviderAnthropic]; exists {
|
|
assert.Equal(t, "resolved-anthropic-key", anthropicProvider.APIKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnvVars_CombinedEnvironmentVariables(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-anthropic")
|
|
os.Setenv("OPENAI_API_KEY", "test-openai")
|
|
os.Setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
|
|
os.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
|
|
os.Setenv("GOOGLE_CLOUD_LOCATION", "us-central1")
|
|
os.Setenv("AWS_ACCESS_KEY_ID", "test-aws-key")
|
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "test-aws-secret")
|
|
os.Setenv("AWS_DEFAULT_REGION", "us-west-1")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
expectedProviders := []provider.InferenceProvider{
|
|
provider.InferenceProviderAnthropic,
|
|
provider.InferenceProviderOpenAI,
|
|
provider.InferenceProviderVertexAI,
|
|
provider.InferenceProviderBedrock,
|
|
}
|
|
|
|
for _, expectedProvider := range expectedProviders {
|
|
assert.Contains(t, cfg.Providers, expectedProvider)
|
|
}
|
|
}
|
|
|
|
func TestHasAWSCredentials_AccessKeys(t *testing.T) {
|
|
reset()
|
|
|
|
os.Setenv("AWS_ACCESS_KEY_ID", "test-key")
|
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
|
|
|
|
assert.True(t, hasAWSCredentials())
|
|
}
|
|
|
|
func TestHasAWSCredentials_Profile(t *testing.T) {
|
|
reset()
|
|
|
|
os.Setenv("AWS_PROFILE", "test-profile")
|
|
|
|
assert.True(t, hasAWSCredentials())
|
|
}
|
|
|
|
func TestHasAWSCredentials_DefaultProfile(t *testing.T) {
|
|
reset()
|
|
|
|
os.Setenv("AWS_DEFAULT_PROFILE", "default")
|
|
|
|
assert.True(t, hasAWSCredentials())
|
|
}
|
|
|
|
func TestHasAWSCredentials_Region(t *testing.T) {
|
|
reset()
|
|
|
|
os.Setenv("AWS_REGION", "us-east-1")
|
|
|
|
assert.True(t, hasAWSCredentials())
|
|
}
|
|
|
|
func TestHasAWSCredentials_ContainerCredentials(t *testing.T) {
|
|
reset()
|
|
|
|
os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/test")
|
|
|
|
assert.True(t, hasAWSCredentials())
|
|
}
|
|
|
|
func TestHasAWSCredentials_NoCredentials(t *testing.T) {
|
|
reset()
|
|
|
|
assert.False(t, hasAWSCredentials())
|
|
}
|
|
|
|
func TestProviderMerging_GlobalToBase(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "global-openai-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-3.5-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "gpt-3.5-turbo",
|
|
Name: "GPT-3.5 Turbo",
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 2048,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 1)
|
|
|
|
openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
|
|
assert.Equal(t, "global-openai-key", openaiProvider.APIKey)
|
|
assert.Equal(t, "gpt-4", openaiProvider.DefaultLargeModel)
|
|
assert.Equal(t, "gpt-4o", openaiProvider.DefaultSmallModel)
|
|
assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
|
|
}
|
|
|
|
func TestProviderMerging_LocalToBase(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
localConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderAnthropic: {
|
|
ID: provider.InferenceProviderAnthropic,
|
|
APIKey: "local-anthropic-key",
|
|
ProviderType: provider.TypeAnthropic,
|
|
DefaultLargeModel: "claude-3-opus",
|
|
DefaultSmallModel: "claude-3-haiku",
|
|
Models: []Model{
|
|
{
|
|
ID: "claude-3-opus",
|
|
Name: "Claude 3 Opus",
|
|
ContextWindow: 200000,
|
|
DefaultMaxTokens: 4096,
|
|
CostPer1MIn: 15.0,
|
|
CostPer1MOut: 75.0,
|
|
},
|
|
{
|
|
ID: "claude-3-haiku",
|
|
Name: "Claude 3 Haiku",
|
|
ContextWindow: 200000,
|
|
DefaultMaxTokens: 4096,
|
|
CostPer1MIn: 0.25,
|
|
CostPer1MOut: 1.25,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err := json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Providers, 1)
|
|
|
|
anthropicProvider := cfg.Providers[provider.InferenceProviderAnthropic]
|
|
assert.Equal(t, "local-anthropic-key", anthropicProvider.APIKey)
|
|
assert.Equal(t, "claude-3-opus", anthropicProvider.DefaultLargeModel)
|
|
assert.Equal(t, "claude-3-5-haiku-20241022", anthropicProvider.DefaultSmallModel)
|
|
assert.GreaterOrEqual(t, len(anthropicProvider.Models), 2)
|
|
}
|
|
|
|
func TestProviderMerging_ConflictingSettings(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "global-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-3.5-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "gpt-3.5-turbo",
|
|
Name: "GPT-3.5 Turbo",
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 2048,
|
|
},
|
|
{
|
|
ID: "gpt-4-turbo",
|
|
Name: "GPT-4 Turbo",
|
|
ContextWindow: 128000,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
// Create local config that overrides
|
|
localConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
APIKey: "local-key",
|
|
DefaultLargeModel: "gpt-4-turbo",
|
|
},
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
|
|
assert.Equal(t, "local-key", openaiProvider.APIKey)
|
|
assert.Equal(t, "gpt-4-turbo", openaiProvider.DefaultLargeModel)
|
|
assert.False(t, openaiProvider.Disabled)
|
|
assert.Equal(t, "gpt-4o", openaiProvider.DefaultSmallModel)
|
|
}
|
|
|
|
func TestProviderMerging_CustomVsKnownProviders(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
customProviderID := provider.InferenceProvider("custom-provider")
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "openai-key",
|
|
BaseURL: "should-not-override",
|
|
ProviderType: provider.TypeAnthropic,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-3.5-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "gpt-3.5-turbo",
|
|
Name: "GPT-3.5 Turbo",
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 2048,
|
|
},
|
|
},
|
|
},
|
|
customProviderID: {
|
|
ID: customProviderID,
|
|
APIKey: "custom-key",
|
|
BaseURL: "https://custom.api.com",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "custom-large",
|
|
DefaultSmallModel: "custom-small",
|
|
Models: []Model{
|
|
{
|
|
ID: "custom-large",
|
|
Name: "Custom Large",
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "custom-small",
|
|
Name: "Custom Small",
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 2048,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
localConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
BaseURL: "https://should-not-change.com",
|
|
ProviderType: provider.TypeGemini, // Should not change
|
|
},
|
|
customProviderID: {
|
|
BaseURL: "https://updated-custom.api.com",
|
|
ProviderType: provider.TypeOpenAI,
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
|
|
assert.NotEqual(t, "https://should-not-change.com", openaiProvider.BaseURL)
|
|
assert.NotEqual(t, provider.TypeGemini, openaiProvider.ProviderType)
|
|
|
|
customProvider := cfg.Providers[customProviderID]
|
|
assert.Equal(t, "custom-key", customProvider.APIKey)
|
|
assert.Equal(t, "https://updated-custom.api.com", customProvider.BaseURL)
|
|
assert.Equal(t, provider.TypeOpenAI, customProvider.ProviderType)
|
|
}
|
|
|
|
func TestProviderValidation_CustomProviderMissingBaseURL(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
customProviderID := provider.InferenceProvider("custom-provider")
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
customProviderID: {
|
|
ID: customProviderID,
|
|
APIKey: "custom-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, cfg.Providers, customProviderID)
|
|
}
|
|
|
|
func TestProviderValidation_CustomProviderMissingAPIKey(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
customProviderID := provider.InferenceProvider("custom-provider")
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
customProviderID: {
|
|
ID: customProviderID,
|
|
BaseURL: "https://custom.api.com",
|
|
ProviderType: provider.TypeOpenAI,
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, cfg.Providers, customProviderID)
|
|
}
|
|
|
|
func TestProviderValidation_CustomProviderInvalidType(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
customProviderID := provider.InferenceProvider("custom-provider")
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
customProviderID: {
|
|
ID: customProviderID,
|
|
APIKey: "custom-key",
|
|
BaseURL: "https://custom.api.com",
|
|
ProviderType: provider.Type("invalid-type"),
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, cfg.Providers, customProviderID)
|
|
}
|
|
|
|
func TestProviderValidation_KnownProviderValid(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "openai-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-3.5-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "gpt-3.5-turbo",
|
|
Name: "GPT-3.5 Turbo",
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 2048,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
|
|
}
|
|
|
|
func TestProviderValidation_DisabledProvider(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "openai-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
Disabled: true,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-3.5-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "gpt-3.5-turbo",
|
|
Name: "GPT-3.5 Turbo",
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 2048,
|
|
},
|
|
},
|
|
},
|
|
provider.InferenceProviderAnthropic: {
|
|
ID: provider.InferenceProviderAnthropic,
|
|
APIKey: "anthropic-key",
|
|
ProviderType: provider.TypeAnthropic,
|
|
Disabled: false, // This one is enabled
|
|
DefaultLargeModel: "claude-3-opus",
|
|
DefaultSmallModel: "claude-3-haiku",
|
|
Models: []Model{
|
|
{
|
|
ID: "claude-3-opus",
|
|
Name: "Claude 3 Opus",
|
|
ContextWindow: 200000,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
{
|
|
ID: "claude-3-haiku",
|
|
Name: "Claude 3 Haiku",
|
|
ContextWindow: 200000,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderOpenAI)
|
|
assert.True(t, cfg.Providers[provider.InferenceProviderOpenAI].Disabled)
|
|
assert.Contains(t, cfg.Providers, provider.InferenceProviderAnthropic)
|
|
assert.False(t, cfg.Providers[provider.InferenceProviderAnthropic].Disabled)
|
|
}
|
|
|
|
func TestProviderModels_AddingNewModels(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "openai-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-4-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
localConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4-turbo",
|
|
Name: "GPT-4 Turbo",
|
|
ContextWindow: 128000,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
|
|
assert.GreaterOrEqual(t, len(openaiProvider.Models), 2)
|
|
|
|
modelIDs := make([]string, len(openaiProvider.Models))
|
|
for i, model := range openaiProvider.Models {
|
|
modelIDs[i] = model.ID
|
|
}
|
|
assert.Contains(t, modelIDs, "gpt-4")
|
|
assert.Contains(t, modelIDs, "gpt-4-turbo")
|
|
}
|
|
|
|
func TestProviderModels_DuplicateModelHandling(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "openai-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-4",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
localConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4 Updated",
|
|
ContextWindow: 16384,
|
|
DefaultMaxTokens: 8192,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
|
|
assert.GreaterOrEqual(t, len(openaiProvider.Models), 1)
|
|
|
|
// Find the first model that matches our test data
|
|
var testModel *Model
|
|
for _, model := range openaiProvider.Models {
|
|
if model.ID == "gpt-4" {
|
|
testModel = &model
|
|
break
|
|
}
|
|
}
|
|
|
|
// If gpt-4 not found, use the first available model
|
|
if testModel == nil {
|
|
testModel = &openaiProvider.Models[0]
|
|
}
|
|
|
|
assert.NotEmpty(t, testModel.ID)
|
|
assert.NotEmpty(t, testModel.Name)
|
|
assert.Greater(t, testModel.ContextWindow, int64(0))
|
|
}
|
|
|
|
func TestProviderModels_ModelCostAndCapabilities(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "openai-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "gpt-4",
|
|
DefaultSmallModel: "gpt-4",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-4",
|
|
Name: "GPT-4",
|
|
CostPer1MIn: 30.0,
|
|
CostPer1MOut: 60.0,
|
|
CostPer1MInCached: 15.0,
|
|
CostPer1MOutCached: 30.0,
|
|
ContextWindow: 8192,
|
|
DefaultMaxTokens: 4096,
|
|
CanReason: true,
|
|
ReasoningEffort: "medium",
|
|
SupportsImages: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
openaiProvider := cfg.Providers[provider.InferenceProviderOpenAI]
|
|
require.GreaterOrEqual(t, len(openaiProvider.Models), 1)
|
|
|
|
// Find the test model or use the first one
|
|
var testModel *Model
|
|
for _, model := range openaiProvider.Models {
|
|
if model.ID == "gpt-4" {
|
|
testModel = &model
|
|
break
|
|
}
|
|
}
|
|
|
|
if testModel == nil {
|
|
testModel = &openaiProvider.Models[0]
|
|
}
|
|
|
|
// Only test the custom properties if this is actually our test model
|
|
if testModel.ID == "gpt-4" {
|
|
assert.Equal(t, 30.0, testModel.CostPer1MIn)
|
|
assert.Equal(t, 60.0, testModel.CostPer1MOut)
|
|
assert.Equal(t, 15.0, testModel.CostPer1MInCached)
|
|
assert.Equal(t, 30.0, testModel.CostPer1MOutCached)
|
|
assert.True(t, testModel.CanReason)
|
|
assert.Equal(t, "medium", testModel.ReasoningEffort)
|
|
assert.True(t, testModel.SupportsImages)
|
|
}
|
|
}
|
|
|
|
func TestDefaultAgents_CoderAgent(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, cfg.Agents, AgentCoder)
|
|
|
|
coderAgent := cfg.Agents[AgentCoder]
|
|
assert.Equal(t, AgentCoder, coderAgent.ID)
|
|
assert.Equal(t, "Coder", coderAgent.Name)
|
|
assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
|
|
assert.Equal(t, LargeModel, coderAgent.Model)
|
|
assert.False(t, coderAgent.Disabled)
|
|
assert.Equal(t, cfg.Options.ContextPaths, coderAgent.ContextPaths)
|
|
assert.Nil(t, coderAgent.AllowedTools)
|
|
}
|
|
|
|
func TestDefaultAgents_TaskAgent(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, cfg.Agents, AgentTask)
|
|
|
|
taskAgent := cfg.Agents[AgentTask]
|
|
assert.Equal(t, AgentTask, taskAgent.ID)
|
|
assert.Equal(t, "Task", taskAgent.Name)
|
|
assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
|
|
assert.Equal(t, LargeModel, taskAgent.Model)
|
|
assert.False(t, taskAgent.Disabled)
|
|
assert.Equal(t, cfg.Options.ContextPaths, taskAgent.ContextPaths)
|
|
|
|
expectedTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
|
|
assert.Equal(t, expectedTools, taskAgent.AllowedTools)
|
|
|
|
assert.Equal(t, map[string][]string{}, taskAgent.AllowedMCP)
|
|
assert.Equal(t, []string{}, taskAgent.AllowedLSP)
|
|
}
|
|
|
|
func TestAgentMerging_CustomAgent(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Agents: map[AgentID]Agent{
|
|
AgentID("custom-agent"): {
|
|
ID: AgentID("custom-agent"),
|
|
Name: "Custom Agent",
|
|
Description: "A custom agent for testing",
|
|
Model: SmallModel,
|
|
AllowedTools: []string{"glob", "grep"},
|
|
AllowedMCP: map[string][]string{"mcp1": {"tool1", "tool2"}},
|
|
AllowedLSP: []string{"typescript", "go"},
|
|
ContextPaths: []string{"custom-context.md"},
|
|
},
|
|
},
|
|
MCP: map[string]MCP{
|
|
"mcp1": {
|
|
Type: MCPStdio,
|
|
Command: "test-mcp-command",
|
|
Args: []string{"--test"},
|
|
},
|
|
},
|
|
LSP: map[string]LSPConfig{
|
|
"typescript": {
|
|
Command: "typescript-language-server",
|
|
Args: []string{"--stdio"},
|
|
},
|
|
"go": {
|
|
Command: "gopls",
|
|
Args: []string{},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, cfg.Agents, AgentCoder)
|
|
assert.Contains(t, cfg.Agents, AgentTask)
|
|
assert.Contains(t, cfg.Agents, AgentID("custom-agent"))
|
|
|
|
customAgent := cfg.Agents[AgentID("custom-agent")]
|
|
assert.Equal(t, "Custom Agent", customAgent.Name)
|
|
assert.Equal(t, "A custom agent for testing", customAgent.Description)
|
|
assert.Equal(t, SmallModel, customAgent.Model)
|
|
assert.Equal(t, []string{"glob", "grep"}, customAgent.AllowedTools)
|
|
assert.Equal(t, map[string][]string{"mcp1": {"tool1", "tool2"}}, customAgent.AllowedMCP)
|
|
assert.Equal(t, []string{"typescript", "go"}, customAgent.AllowedLSP)
|
|
expectedContextPaths := append(defaultContextPaths, "custom-context.md")
|
|
assert.Equal(t, expectedContextPaths, customAgent.ContextPaths)
|
|
}
|
|
|
|
func TestAgentMerging_ModifyDefaultCoderAgent(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Agents: map[AgentID]Agent{
|
|
AgentCoder: {
|
|
Model: SmallModel,
|
|
AllowedMCP: map[string][]string{"mcp1": {"tool1"}},
|
|
AllowedLSP: []string{"typescript"},
|
|
ContextPaths: []string{"coder-specific.md"},
|
|
},
|
|
},
|
|
MCP: map[string]MCP{
|
|
"mcp1": {
|
|
Type: MCPStdio,
|
|
Command: "test-mcp-command",
|
|
Args: []string{"--test"},
|
|
},
|
|
},
|
|
LSP: map[string]LSPConfig{
|
|
"typescript": {
|
|
Command: "typescript-language-server",
|
|
Args: []string{"--stdio"},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
coderAgent := cfg.Agents[AgentCoder]
|
|
assert.Equal(t, AgentCoder, coderAgent.ID)
|
|
assert.Equal(t, "Coder", coderAgent.Name)
|
|
assert.Equal(t, "An agent that helps with executing coding tasks.", coderAgent.Description)
|
|
|
|
expectedContextPaths := append(cfg.Options.ContextPaths, "coder-specific.md")
|
|
assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
|
|
|
|
assert.Equal(t, SmallModel, coderAgent.Model)
|
|
assert.Equal(t, map[string][]string{"mcp1": {"tool1"}}, coderAgent.AllowedMCP)
|
|
assert.Equal(t, []string{"typescript"}, coderAgent.AllowedLSP)
|
|
}
|
|
|
|
func TestAgentMerging_ModifyDefaultTaskAgent(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Agents: map[AgentID]Agent{
|
|
AgentTask: {
|
|
Model: SmallModel,
|
|
AllowedMCP: map[string][]string{"search-mcp": nil},
|
|
AllowedLSP: []string{"python"},
|
|
Name: "Search Agent",
|
|
Description: "Custom search agent",
|
|
Disabled: true,
|
|
AllowedTools: []string{"glob", "grep", "view"},
|
|
},
|
|
},
|
|
MCP: map[string]MCP{
|
|
"search-mcp": {
|
|
Type: MCPStdio,
|
|
Command: "search-mcp-command",
|
|
Args: []string{"--search"},
|
|
},
|
|
},
|
|
LSP: map[string]LSPConfig{
|
|
"python": {
|
|
Command: "pylsp",
|
|
Args: []string{},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
taskAgent := cfg.Agents[AgentTask]
|
|
assert.Equal(t, "Task", taskAgent.Name)
|
|
assert.Equal(t, "An agent that helps with searching for context and finding implementation details.", taskAgent.Description)
|
|
assert.False(t, taskAgent.Disabled)
|
|
assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools)
|
|
|
|
assert.Equal(t, SmallModel, taskAgent.Model)
|
|
assert.Equal(t, map[string][]string{"search-mcp": nil}, taskAgent.AllowedMCP)
|
|
assert.Equal(t, []string{"python"}, taskAgent.AllowedLSP)
|
|
}
|
|
|
|
func TestAgentMerging_LocalOverridesGlobal(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Agents: map[AgentID]Agent{
|
|
AgentID("test-agent"): {
|
|
ID: AgentID("test-agent"),
|
|
Name: "Global Agent",
|
|
Description: "Global description",
|
|
Model: LargeModel,
|
|
AllowedTools: []string{"glob"},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
// Create local config that overrides
|
|
localConfig := Config{
|
|
Agents: map[AgentID]Agent{
|
|
AgentID("test-agent"): {
|
|
Name: "Local Agent",
|
|
Description: "Local description",
|
|
Model: SmallModel,
|
|
Disabled: true,
|
|
AllowedTools: []string{"grep", "view"},
|
|
AllowedMCP: map[string][]string{"local-mcp": {"tool1"}},
|
|
},
|
|
},
|
|
MCP: map[string]MCP{
|
|
"local-mcp": {
|
|
Type: MCPStdio,
|
|
Command: "local-mcp-command",
|
|
Args: []string{"--local"},
|
|
},
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
testAgent := cfg.Agents[AgentID("test-agent")]
|
|
assert.Equal(t, "Local Agent", testAgent.Name)
|
|
assert.Equal(t, "Local description", testAgent.Description)
|
|
assert.Equal(t, SmallModel, testAgent.Model)
|
|
assert.True(t, testAgent.Disabled)
|
|
assert.Equal(t, []string{"grep", "view"}, testAgent.AllowedTools)
|
|
assert.Equal(t, map[string][]string{"local-mcp": {"tool1"}}, testAgent.AllowedMCP)
|
|
}
|
|
|
|
func TestAgentModelTypeAssignment(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Agents: map[AgentID]Agent{
|
|
AgentID("large-agent"): {
|
|
ID: AgentID("large-agent"),
|
|
Name: "Large Model Agent",
|
|
Model: LargeModel,
|
|
},
|
|
AgentID("small-agent"): {
|
|
ID: AgentID("small-agent"),
|
|
Name: "Small Model Agent",
|
|
Model: SmallModel,
|
|
},
|
|
AgentID("default-agent"): {
|
|
ID: AgentID("default-agent"),
|
|
Name: "Default Model Agent",
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, LargeModel, cfg.Agents[AgentID("large-agent")].Model)
|
|
assert.Equal(t, SmallModel, cfg.Agents[AgentID("small-agent")].Model)
|
|
assert.Equal(t, LargeModel, cfg.Agents[AgentID("default-agent")].Model)
|
|
}
|
|
|
|
func TestAgentContextPathOverrides(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Options: Options{
|
|
ContextPaths: []string{"global-context.md", "shared-context.md"},
|
|
},
|
|
Agents: map[AgentID]Agent{
|
|
AgentID("custom-context-agent"): {
|
|
ID: AgentID("custom-context-agent"),
|
|
Name: "Custom Context Agent",
|
|
ContextPaths: []string{"agent-specific.md", "custom.md"},
|
|
},
|
|
AgentID("default-context-agent"): {
|
|
ID: AgentID("default-context-agent"),
|
|
Name: "Default Context Agent",
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
customAgent := cfg.Agents[AgentID("custom-context-agent")]
|
|
expectedCustomPaths := append(defaultContextPaths, "global-context.md", "shared-context.md", "agent-specific.md", "custom.md")
|
|
assert.Equal(t, expectedCustomPaths, customAgent.ContextPaths)
|
|
|
|
defaultAgent := cfg.Agents[AgentID("default-context-agent")]
|
|
expectedContextPaths := append(defaultContextPaths, "global-context.md", "shared-context.md")
|
|
assert.Equal(t, expectedContextPaths, defaultAgent.ContextPaths)
|
|
|
|
coderAgent := cfg.Agents[AgentCoder]
|
|
assert.Equal(t, expectedContextPaths, coderAgent.ContextPaths)
|
|
}
|
|
|
|
func TestOptionsMerging_ContextPaths(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Options: Options{
|
|
ContextPaths: []string{"global1.md", "global2.md"},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
localConfig := Config{
|
|
Options: Options{
|
|
ContextPaths: []string{"local1.md", "local2.md"},
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
expectedContextPaths := append(defaultContextPaths, "global1.md", "global2.md", "local1.md", "local2.md")
|
|
assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
|
|
}
|
|
|
|
func TestOptionsMerging_TUIOptions(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Options: Options{
|
|
TUI: TUIOptions{
|
|
CompactMode: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
localConfig := Config{
|
|
Options: Options{
|
|
TUI: TUIOptions{
|
|
CompactMode: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, cfg.Options.TUI.CompactMode)
|
|
}
|
|
|
|
func TestOptionsMerging_DebugFlags(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Options: Options{
|
|
Debug: false,
|
|
DebugLSP: false,
|
|
DisableAutoSummarize: false,
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
localConfig := Config{
|
|
Options: Options{
|
|
DebugLSP: true,
|
|
DisableAutoSummarize: true,
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.False(t, cfg.Options.Debug)
|
|
assert.True(t, cfg.Options.DebugLSP)
|
|
assert.True(t, cfg.Options.DisableAutoSummarize)
|
|
}
|
|
|
|
func TestOptionsMerging_DataDirectory(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Options: Options{
|
|
DataDirectory: "global-data",
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
localConfig := Config{
|
|
Options: Options{
|
|
DataDirectory: "local-data",
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "local-data", cfg.Options.DataDirectory)
|
|
}
|
|
|
|
func TestOptionsMerging_DefaultValues(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, defaultDataDirectory, cfg.Options.DataDirectory)
|
|
assert.Equal(t, defaultContextPaths, cfg.Options.ContextPaths)
|
|
assert.False(t, cfg.Options.TUI.CompactMode)
|
|
assert.False(t, cfg.Options.Debug)
|
|
assert.False(t, cfg.Options.DebugLSP)
|
|
assert.False(t, cfg.Options.DisableAutoSummarize)
|
|
}
|
|
|
|
func TestOptionsMerging_DebugFlagFromInit(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Options: Options{
|
|
Debug: false,
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, true)
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Debug flag from Init should take precedence
|
|
assert.True(t, cfg.Options.Debug)
|
|
}
|
|
|
|
func TestOptionsMerging_ComplexScenario(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
// Set up a provider
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
// Create global config with various options
|
|
globalConfig := Config{
|
|
Options: Options{
|
|
ContextPaths: []string{"global-context.md"},
|
|
DataDirectory: "global-data",
|
|
Debug: false,
|
|
DebugLSP: false,
|
|
DisableAutoSummarize: false,
|
|
TUI: TUIOptions{
|
|
CompactMode: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
// Create local config that partially overrides
|
|
localConfig := Config{
|
|
Options: Options{
|
|
ContextPaths: []string{"local-context.md"},
|
|
DebugLSP: true, // Override
|
|
DisableAutoSummarize: true, // Override
|
|
TUI: TUIOptions{
|
|
CompactMode: true, // Override
|
|
},
|
|
// DataDirectory and Debug not specified - should keep global values
|
|
},
|
|
}
|
|
|
|
localConfigPath := filepath.Join(cwdDir, "crush.json")
|
|
data, err = json.Marshal(localConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(localConfigPath, data, 0o644))
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Check merged results
|
|
expectedContextPaths := append(defaultContextPaths, "global-context.md", "local-context.md")
|
|
assert.Equal(t, expectedContextPaths, cfg.Options.ContextPaths)
|
|
assert.Equal(t, "global-data", cfg.Options.DataDirectory) // From global
|
|
assert.False(t, cfg.Options.Debug) // From global
|
|
assert.True(t, cfg.Options.DebugLSP) // From local
|
|
assert.True(t, cfg.Options.DisableAutoSummarize) // From local
|
|
assert.True(t, cfg.Options.TUI.CompactMode) // From local
|
|
}
|
|
|
|
// Model Selection Tests
|
|
|
|
func TestModelSelection_PreferredModelSelection(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
// Set up multiple providers to test selection logic
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-anthropic-key")
|
|
os.Setenv("OPENAI_API_KEY", "test-openai-key")
|
|
|
|
cfg, err := Init(cwdDir, false)
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, cfg.Providers, 2)
|
|
|
|
// Should have preferred models set
|
|
assert.NotEmpty(t, cfg.Models.Large.ModelID)
|
|
assert.NotEmpty(t, cfg.Models.Large.Provider)
|
|
assert.NotEmpty(t, cfg.Models.Small.ModelID)
|
|
assert.NotEmpty(t, cfg.Models.Small.Provider)
|
|
|
|
// Both should use the same provider (first available)
|
|
assert.Equal(t, cfg.Models.Large.Provider, cfg.Models.Small.Provider)
|
|
}
|
|
|
|
func TestValidation_InvalidModelReference(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
globalConfig := Config{
|
|
Providers: map[provider.InferenceProvider]ProviderConfig{
|
|
provider.InferenceProviderOpenAI: {
|
|
ID: provider.InferenceProviderOpenAI,
|
|
APIKey: "test-key",
|
|
ProviderType: provider.TypeOpenAI,
|
|
DefaultLargeModel: "non-existent-model",
|
|
DefaultSmallModel: "gpt-3.5-turbo",
|
|
Models: []Model{
|
|
{
|
|
ID: "gpt-3.5-turbo",
|
|
Name: "GPT-3.5 Turbo",
|
|
ContextWindow: 4096,
|
|
DefaultMaxTokens: 2048,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
_, err = Init(cwdDir, false)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestValidation_InvalidAgentModelType(t *testing.T) {
|
|
reset()
|
|
testConfigDir = t.TempDir()
|
|
cwdDir := t.TempDir()
|
|
|
|
os.Setenv("ANTHROPIC_API_KEY", "test-key")
|
|
|
|
globalConfig := Config{
|
|
Agents: map[AgentID]Agent{
|
|
AgentID("invalid-agent"): {
|
|
ID: AgentID("invalid-agent"),
|
|
Name: "Invalid Agent",
|
|
Model: ModelType("invalid"),
|
|
},
|
|
},
|
|
}
|
|
|
|
configPath := filepath.Join(testConfigDir, "crush.json")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))
|
|
data, err := json.Marshal(globalConfig)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, data, 0o644))
|
|
|
|
_, err = Init(cwdDir, false)
|
|
assert.Error(t, err)
|
|
}
|