From 283f731c0d9bc93190713a1bb1d56df0e74cac4e Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Fri, 27 Jun 2025 17:45:14 +0200 Subject: [PATCH] fix: formatting in validation test --- internal/config/validation_test.go | 462 +++++++++++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 internal/config/validation_test.go diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0aef035ae7bddfc7532e9dde550ab0184ed180db --- /dev/null +++ b/internal/config/validation_test.go @@ -0,0 +1,462 @@ +package config + +import ( + "testing" + + "github.com/charmbracelet/crush/internal/fur/provider" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfig_Validate_ValidConfig(t *testing.T) { + cfg := &Config{ + Models: PreferredModels{ + Large: PreferredModel{ + ModelID: "gpt-4", + Provider: provider.InferenceProviderOpenAI, + }, + Small: PreferredModel{ + ModelID: "gpt-3.5-turbo", + Provider: provider.InferenceProviderOpenAI, + }, + }, + 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", + ContextWindow: 8192, + DefaultMaxTokens: 4096, + CostPer1MIn: 30.0, + CostPer1MOut: 60.0, + }, + { + ID: "gpt-3.5-turbo", + Name: "GPT-3.5 Turbo", + ContextWindow: 4096, + DefaultMaxTokens: 2048, + CostPer1MIn: 1.5, + CostPer1MOut: 2.0, + }, + }, + }, + }, + Agents: map[AgentID]Agent{ + AgentCoder: { + ID: AgentCoder, + Name: "Coder", + Description: "An agent that helps with executing coding tasks.", + Model: LargeModel, + ContextPaths: []string{"CRUSH.md"}, + }, + AgentTask: { + ID: AgentTask, + Name: "Task", + Description: "An agent that helps with searching for context and finding implementation details.", + Model: LargeModel, + ContextPaths: []string{"CRUSH.md"}, + AllowedTools: []string{"glob", "grep", "ls", "sourcegraph", "view"}, + AllowedMCP: map[string][]string{}, + AllowedLSP: []string{}, + }, + }, + MCP: map[string]MCP{}, + LSP: map[string]LSPConfig{}, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + assert.NoError(t, err) +} + +func TestConfig_Validate_MissingAPIKey(t *testing.T) { + cfg := &Config{ + Providers: map[provider.InferenceProvider]ProviderConfig{ + provider.InferenceProviderOpenAI: { + ID: provider.InferenceProviderOpenAI, + ProviderType: provider.TypeOpenAI, + // Missing APIKey + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "API key is required") +} + +func TestConfig_Validate_InvalidProviderType(t *testing.T) { + cfg := &Config{ + Providers: map[provider.InferenceProvider]ProviderConfig{ + provider.InferenceProviderOpenAI: { + ID: provider.InferenceProviderOpenAI, + APIKey: "test-key", + ProviderType: provider.Type("invalid"), + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid provider type") +} + +func TestConfig_Validate_CustomProviderMissingBaseURL(t *testing.T) { + customProvider := provider.InferenceProvider("custom-provider") + cfg := &Config{ + Providers: map[provider.InferenceProvider]ProviderConfig{ + customProvider: { + ID: customProvider, + APIKey: "test-key", + ProviderType: provider.TypeOpenAI, + // Missing BaseURL for custom provider + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "BaseURL is required for custom providers") +} + +func TestConfig_Validate_DuplicateModelIDs(t *testing.T) { + cfg := &Config{ + Providers: map[provider.InferenceProvider]ProviderConfig{ + provider.InferenceProviderOpenAI: { + ID: provider.InferenceProviderOpenAI, + APIKey: "test-key", + ProviderType: provider.TypeOpenAI, + Models: []Model{ + { + ID: "gpt-4", + Name: "GPT-4", + ContextWindow: 8192, + DefaultMaxTokens: 4096, + }, + { + ID: "gpt-4", // Duplicate ID + Name: "GPT-4 Duplicate", + ContextWindow: 8192, + DefaultMaxTokens: 4096, + }, + }, + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "duplicate model ID") +} + +func TestConfig_Validate_InvalidModelFields(t *testing.T) { + cfg := &Config{ + Providers: map[provider.InferenceProvider]ProviderConfig{ + provider.InferenceProviderOpenAI: { + ID: provider.InferenceProviderOpenAI, + APIKey: "test-key", + ProviderType: provider.TypeOpenAI, + Models: []Model{ + { + ID: "", // Empty ID + Name: "GPT-4", + ContextWindow: 0, // Invalid context window + DefaultMaxTokens: -1, // Invalid max tokens + CostPer1MIn: -5.0, // Negative cost + }, + }, + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + validationErr := err.(ValidationErrors) + assert.True(t, len(validationErr) >= 4) // Should have multiple validation errors +} + +func TestConfig_Validate_DefaultModelNotFound(t *testing.T) { + cfg := &Config{ + Providers: map[provider.InferenceProvider]ProviderConfig{ + provider.InferenceProviderOpenAI: { + ID: provider.InferenceProviderOpenAI, + APIKey: "test-key", + ProviderType: provider.TypeOpenAI, + DefaultLargeModel: "nonexistent-model", + Models: []Model{ + { + ID: "gpt-4", + Name: "GPT-4", + ContextWindow: 8192, + DefaultMaxTokens: 4096, + }, + }, + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "default large model 'nonexistent-model' not found") +} + +func TestConfig_Validate_AgentIDMismatch(t *testing.T) { + cfg := &Config{ + Agents: map[AgentID]Agent{ + AgentCoder: { + ID: AgentTask, // Wrong ID + Name: "Coder", + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "agent ID mismatch") +} + +func TestConfig_Validate_InvalidAgentModelType(t *testing.T) { + cfg := &Config{ + Agents: map[AgentID]Agent{ + AgentCoder: { + ID: AgentCoder, + Name: "Coder", + Model: ModelType("invalid"), + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid model type") +} + +func TestConfig_Validate_UnknownTool(t *testing.T) { + cfg := &Config{ + Agents: map[AgentID]Agent{ + AgentID("custom-agent"): { + ID: AgentID("custom-agent"), + Name: "Custom Agent", + Model: LargeModel, + AllowedTools: []string{"unknown-tool"}, + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown tool") +} + +func TestConfig_Validate_MCPReference(t *testing.T) { + cfg := &Config{ + Agents: map[AgentID]Agent{ + AgentID("custom-agent"): { + ID: AgentID("custom-agent"), + Name: "Custom Agent", + Model: LargeModel, + AllowedMCP: map[string][]string{"nonexistent-mcp": nil}, + }, + }, + MCP: map[string]MCP{}, // Empty MCP map + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "referenced MCP 'nonexistent-mcp' not found") +} + +func TestConfig_Validate_InvalidMCPType(t *testing.T) { + cfg := &Config{ + MCP: map[string]MCP{ + "test-mcp": { + Type: MCPType("invalid"), + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid MCP type") +} + +func TestConfig_Validate_MCPMissingCommand(t *testing.T) { + cfg := &Config{ + MCP: map[string]MCP{ + "test-mcp": { + Type: MCPStdio, + // Missing Command + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "command is required for stdio MCP") +} + +func TestConfig_Validate_LSPMissingCommand(t *testing.T) { + cfg := &Config{ + LSP: map[string]LSPConfig{ + "test-lsp": { + // Missing Command + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "command is required for LSP") +} + +func TestConfig_Validate_NoValidProviders(t *testing.T) { + cfg := &Config{ + Providers: map[provider.InferenceProvider]ProviderConfig{ + provider.InferenceProviderOpenAI: { + ID: provider.InferenceProviderOpenAI, + APIKey: "test-key", + ProviderType: provider.TypeOpenAI, + Disabled: true, // Disabled + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "at least one non-disabled provider is required") +} + +func TestConfig_Validate_MissingDefaultAgents(t *testing.T) { + cfg := &Config{ + Providers: map[provider.InferenceProvider]ProviderConfig{ + provider.InferenceProviderOpenAI: { + ID: provider.InferenceProviderOpenAI, + APIKey: "test-key", + ProviderType: provider.TypeOpenAI, + }, + }, + Agents: map[AgentID]Agent{}, // Missing default agents + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "coder agent is required") + assert.Contains(t, err.Error(), "task agent is required") +} + +func TestConfig_Validate_KnownAgentProtection(t *testing.T) { + cfg := &Config{ + Agents: map[AgentID]Agent{ + AgentCoder: { + ID: AgentCoder, + Name: "Modified Coder", // Should not be allowed + Description: "Modified description", // Should not be allowed + Model: LargeModel, + }, + }, + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "coder agent name cannot be changed") + assert.Contains(t, err.Error(), "coder agent description cannot be changed") +} + +func TestConfig_Validate_EmptyDataDirectory(t *testing.T) { + cfg := &Config{ + Options: Options{ + DataDirectory: "", // Empty + ContextPaths: []string{"CRUSH.md"}, + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "data directory is required") +} + +func TestConfig_Validate_EmptyContextPath(t *testing.T) { + cfg := &Config{ + Options: Options{ + DataDirectory: ".crush", + ContextPaths: []string{""}, // Empty context path + }, + } + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "context path cannot be empty") +}