@@ -0,0 +1,462 @@
6+ "github.com/charmbracelet/crush/internal/fur/provider"
7+ "github.com/stretchr/testify/assert"
8+ "github.com/stretchr/testify/require"
11+func TestConfig_Validate_ValidConfig(t *testing.T) {
13+ Models: PreferredModels{
14+ Large: PreferredModel{
16+ Provider: provider.InferenceProviderOpenAI,
18+ Small: PreferredModel{
19+ ModelID: "gpt-3.5-turbo",
20+ Provider: provider.InferenceProviderOpenAI,
23+ Providers: map[provider.InferenceProvider]ProviderConfig{
24+ provider.InferenceProviderOpenAI: {
25+ ID: provider.InferenceProviderOpenAI,
27+ ProviderType: provider.TypeOpenAI,
28+ DefaultLargeModel: "gpt-4",
29+ DefaultSmallModel: "gpt-3.5-turbo",
35+ DefaultMaxTokens: 4096,
41+ Name: "GPT-3.5 Turbo",
43+ DefaultMaxTokens: 2048,
50+ Agents: map[AgentID]Agent{
54+ Description: "An agent that helps with executing coding tasks.",
56+ ContextPaths: []string{"CRUSH.md"},
61+ Description: "An agent that helps with searching for context and finding implementation details.",
63+ ContextPaths: []string{"CRUSH.md"},
64+ AllowedTools: []string{"glob", "grep", "ls", "sourcegraph", "view"},
65+ AllowedMCP: map[string][]string{},
66+ AllowedLSP: []string{},
69+ MCP: map[string]MCP{},
70+ LSP: map[string]LSPConfig{},
72+ DataDirectory: ".crush",
73+ ContextPaths: []string{"CRUSH.md"},
77+ err := cfg.Validate()
78+ assert.NoError(t, err)
81+func TestConfig_Validate_MissingAPIKey(t *testing.T) {
83+ Providers: map[provider.InferenceProvider]ProviderConfig{
84+ provider.InferenceProviderOpenAI: {
85+ ID: provider.InferenceProviderOpenAI,
86+ ProviderType: provider.TypeOpenAI,
91+ DataDirectory: ".crush",
92+ ContextPaths: []string{"CRUSH.md"},
96+ err := cfg.Validate()
97+ require.Error(t, err)
98+ assert.Contains(t, err.Error(), "API key is required")
101+func TestConfig_Validate_InvalidProviderType(t *testing.T) {
103+ Providers: map[provider.InferenceProvider]ProviderConfig{
104+ provider.InferenceProviderOpenAI: {
105+ ID: provider.InferenceProviderOpenAI,
107+ ProviderType: provider.Type("invalid"),
111+ DataDirectory: ".crush",
112+ ContextPaths: []string{"CRUSH.md"},
116+ err := cfg.Validate()
117+ require.Error(t, err)
118+ assert.Contains(t, err.Error(), "invalid provider type")
121+func TestConfig_Validate_CustomProviderMissingBaseURL(t *testing.T) {
122+ customProvider := provider.InferenceProvider("custom-provider")
124+ Providers: map[provider.InferenceProvider]ProviderConfig{
128+ ProviderType: provider.TypeOpenAI,
129+ // Missing BaseURL for custom provider
133+ DataDirectory: ".crush",
134+ ContextPaths: []string{"CRUSH.md"},
138+ err := cfg.Validate()
139+ require.Error(t, err)
140+ assert.Contains(t, err.Error(), "BaseURL is required for custom providers")
143+func TestConfig_Validate_DuplicateModelIDs(t *testing.T) {
145+ Providers: map[provider.InferenceProvider]ProviderConfig{
146+ provider.InferenceProviderOpenAI: {
147+ ID: provider.InferenceProviderOpenAI,
149+ ProviderType: provider.TypeOpenAI,
154+ ContextWindow: 8192,
155+ DefaultMaxTokens: 4096,
158+ ID: "gpt-4", // Duplicate ID
159+ Name: "GPT-4 Duplicate",
160+ ContextWindow: 8192,
161+ DefaultMaxTokens: 4096,
167+ DataDirectory: ".crush",
168+ ContextPaths: []string{"CRUSH.md"},
172+ err := cfg.Validate()
173+ require.Error(t, err)
174+ assert.Contains(t, err.Error(), "duplicate model ID")
177+func TestConfig_Validate_InvalidModelFields(t *testing.T) {
179+ Providers: map[provider.InferenceProvider]ProviderConfig{
180+ provider.InferenceProviderOpenAI: {
181+ ID: provider.InferenceProviderOpenAI,
183+ ProviderType: provider.TypeOpenAI,
188+ ContextWindow: 0, // Invalid context window
189+ DefaultMaxTokens: -1, // Invalid max tokens
190+ CostPer1MIn: -5.0, // Negative cost
196+ DataDirectory: ".crush",
197+ ContextPaths: []string{"CRUSH.md"},
201+ err := cfg.Validate()
202+ require.Error(t, err)
203+ validationErr := err.(ValidationErrors)
204+ assert.True(t, len(validationErr) >= 4) // Should have multiple validation errors
207+func TestConfig_Validate_DefaultModelNotFound(t *testing.T) {
209+ Providers: map[provider.InferenceProvider]ProviderConfig{
210+ provider.InferenceProviderOpenAI: {
211+ ID: provider.InferenceProviderOpenAI,
213+ ProviderType: provider.TypeOpenAI,
214+ DefaultLargeModel: "nonexistent-model",
219+ ContextWindow: 8192,
220+ DefaultMaxTokens: 4096,
226+ DataDirectory: ".crush",
227+ ContextPaths: []string{"CRUSH.md"},
231+ err := cfg.Validate()
232+ require.Error(t, err)
233+ assert.Contains(t, err.Error(), "default large model 'nonexistent-model' not found")
236+func TestConfig_Validate_AgentIDMismatch(t *testing.T) {
238+ Agents: map[AgentID]Agent{
240+ ID: AgentTask, // Wrong ID
245+ DataDirectory: ".crush",
246+ ContextPaths: []string{"CRUSH.md"},
250+ err := cfg.Validate()
251+ require.Error(t, err)
252+ assert.Contains(t, err.Error(), "agent ID mismatch")
255+func TestConfig_Validate_InvalidAgentModelType(t *testing.T) {
257+ Agents: map[AgentID]Agent{
261+ Model: ModelType("invalid"),
265+ DataDirectory: ".crush",
266+ ContextPaths: []string{"CRUSH.md"},
270+ err := cfg.Validate()
271+ require.Error(t, err)
272+ assert.Contains(t, err.Error(), "invalid model type")
275+func TestConfig_Validate_UnknownTool(t *testing.T) {
277+ Agents: map[AgentID]Agent{
278+ AgentID("custom-agent"): {
279+ ID: AgentID("custom-agent"),
280+ Name: "Custom Agent",
282+ AllowedTools: []string{"unknown-tool"},
286+ DataDirectory: ".crush",
287+ ContextPaths: []string{"CRUSH.md"},
291+ err := cfg.Validate()
292+ require.Error(t, err)
293+ assert.Contains(t, err.Error(), "unknown tool")
296+func TestConfig_Validate_MCPReference(t *testing.T) {
298+ Agents: map[AgentID]Agent{
299+ AgentID("custom-agent"): {
300+ ID: AgentID("custom-agent"),
301+ Name: "Custom Agent",
303+ AllowedMCP: map[string][]string{"nonexistent-mcp": nil},
306+ MCP: map[string]MCP{}, // Empty MCP map
308+ DataDirectory: ".crush",
309+ ContextPaths: []string{"CRUSH.md"},
313+ err := cfg.Validate()
314+ require.Error(t, err)
315+ assert.Contains(t, err.Error(), "referenced MCP 'nonexistent-mcp' not found")
318+func TestConfig_Validate_InvalidMCPType(t *testing.T) {
320+ MCP: map[string]MCP{
322+ Type: MCPType("invalid"),
326+ DataDirectory: ".crush",
327+ ContextPaths: []string{"CRUSH.md"},
331+ err := cfg.Validate()
332+ require.Error(t, err)
333+ assert.Contains(t, err.Error(), "invalid MCP type")
336+func TestConfig_Validate_MCPMissingCommand(t *testing.T) {
338+ MCP: map[string]MCP{
345+ DataDirectory: ".crush",
346+ ContextPaths: []string{"CRUSH.md"},
350+ err := cfg.Validate()
351+ require.Error(t, err)
352+ assert.Contains(t, err.Error(), "command is required for stdio MCP")
355+func TestConfig_Validate_LSPMissingCommand(t *testing.T) {
357+ LSP: map[string]LSPConfig{
363+ DataDirectory: ".crush",
364+ ContextPaths: []string{"CRUSH.md"},
368+ err := cfg.Validate()
369+ require.Error(t, err)
370+ assert.Contains(t, err.Error(), "command is required for LSP")
373+func TestConfig_Validate_NoValidProviders(t *testing.T) {
375+ Providers: map[provider.InferenceProvider]ProviderConfig{
376+ provider.InferenceProviderOpenAI: {
377+ ID: provider.InferenceProviderOpenAI,
379+ ProviderType: provider.TypeOpenAI,
380+ Disabled: true, // Disabled
384+ DataDirectory: ".crush",
385+ ContextPaths: []string{"CRUSH.md"},
389+ err := cfg.Validate()
390+ require.Error(t, err)
391+ assert.Contains(t, err.Error(), "at least one non-disabled provider is required")
394+func TestConfig_Validate_MissingDefaultAgents(t *testing.T) {
396+ Providers: map[provider.InferenceProvider]ProviderConfig{
397+ provider.InferenceProviderOpenAI: {
398+ ID: provider.InferenceProviderOpenAI,
400+ ProviderType: provider.TypeOpenAI,
403+ Agents: map[AgentID]Agent{}, // Missing default agents
405+ DataDirectory: ".crush",
406+ ContextPaths: []string{"CRUSH.md"},
410+ err := cfg.Validate()
411+ require.Error(t, err)
412+ assert.Contains(t, err.Error(), "coder agent is required")
413+ assert.Contains(t, err.Error(), "task agent is required")
416+func TestConfig_Validate_KnownAgentProtection(t *testing.T) {
418+ Agents: map[AgentID]Agent{
421+ Name: "Modified Coder", // Should not be allowed
422+ Description: "Modified description", // Should not be allowed
427+ DataDirectory: ".crush",
428+ ContextPaths: []string{"CRUSH.md"},
432+ err := cfg.Validate()
433+ require.Error(t, err)
434+ assert.Contains(t, err.Error(), "coder agent name cannot be changed")
435+ assert.Contains(t, err.Error(), "coder agent description cannot be changed")
438+func TestConfig_Validate_EmptyDataDirectory(t *testing.T) {
441+ DataDirectory: "", // Empty
442+ ContextPaths: []string{"CRUSH.md"},
446+ err := cfg.Validate()
447+ require.Error(t, err)
448+ assert.Contains(t, err.Error(), "data directory is required")
451+func TestConfig_Validate_EmptyContextPath(t *testing.T) {
454+ DataDirectory: ".crush",
455+ ContextPaths: []string{""}, // Empty context path
459+ err := cfg.Validate()
460+ require.Error(t, err)
461+ assert.Contains(t, err.Error(), "context path cannot be empty")