on main help sign in
fix: formatting in validation test
commit953863834943239385e14d69615ab46fab94f42c authorKujtim Hoxha <kujtimii.h@gmail.com> dateFri 27 Jun 2025, 17:45 (1 year ago) parent400bf3f
1 file changed+462
internal/config/validation_test.go+462
internal/config/validation_test.gonew file+462
@@ -0,0 +1,462 @@
1+package config
2+
3+import (
4+ "testing"
5+
6+ "github.com/charmbracelet/crush/internal/fur/provider"
7+ "github.com/stretchr/testify/assert"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestConfig_Validate_ValidConfig(t *testing.T) {
12+ cfg := &Config{
13+ Models: PreferredModels{
14+ Large: PreferredModel{
15+ ModelID: "gpt-4",
16+ Provider: provider.InferenceProviderOpenAI,
17+ },
18+ Small: PreferredModel{
19+ ModelID: "gpt-3.5-turbo",
20+ Provider: provider.InferenceProviderOpenAI,
21+ },
22+ },
23+ Providers: map[provider.InferenceProvider]ProviderConfig{
24+ provider.InferenceProviderOpenAI: {
25+ ID: provider.InferenceProviderOpenAI,
26+ APIKey: "test-key",
27+ ProviderType: provider.TypeOpenAI,
28+ DefaultLargeModel: "gpt-4",
29+ DefaultSmallModel: "gpt-3.5-turbo",
30+ Models: []Model{
31+ {
32+ ID: "gpt-4",
33+ Name: "GPT-4",
34+ ContextWindow: 8192,
35+ DefaultMaxTokens: 4096,
36+ CostPer1MIn: 30.0,
37+ CostPer1MOut: 60.0,
38+ },
39+ {
40+ ID: "gpt-3.5-turbo",
41+ Name: "GPT-3.5 Turbo",
42+ ContextWindow: 4096,
43+ DefaultMaxTokens: 2048,
44+ CostPer1MIn: 1.5,
45+ CostPer1MOut: 2.0,
46+ },
47+ },
48+ },
49+ },
50+ Agents: map[AgentID]Agent{
51+ AgentCoder: {
52+ ID: AgentCoder,
53+ Name: "Coder",
54+ Description: "An agent that helps with executing coding tasks.",
55+ Model: LargeModel,
56+ ContextPaths: []string{"CRUSH.md"},
57+ },
58+ AgentTask: {
59+ ID: AgentTask,
60+ Name: "Task",
61+ Description: "An agent that helps with searching for context and finding implementation details.",
62+ Model: LargeModel,
63+ ContextPaths: []string{"CRUSH.md"},
64+ AllowedTools: []string{"glob", "grep", "ls", "sourcegraph", "view"},
65+ AllowedMCP: map[string][]string{},
66+ AllowedLSP: []string{},
67+ },
68+ },
69+ MCP: map[string]MCP{},
70+ LSP: map[string]LSPConfig{},
71+ Options: Options{
72+ DataDirectory: ".crush",
73+ ContextPaths: []string{"CRUSH.md"},
74+ },
75+ }
76+
77+ err := cfg.Validate()
78+ assert.NoError(t, err)
79+}
80+
81+func TestConfig_Validate_MissingAPIKey(t *testing.T) {
82+ cfg := &Config{
83+ Providers: map[provider.InferenceProvider]ProviderConfig{
84+ provider.InferenceProviderOpenAI: {
85+ ID: provider.InferenceProviderOpenAI,
86+ ProviderType: provider.TypeOpenAI,
87+ // Missing APIKey
88+ },
89+ },
90+ Options: Options{
91+ DataDirectory: ".crush",
92+ ContextPaths: []string{"CRUSH.md"},
93+ },
94+ }
95+
96+ err := cfg.Validate()
97+ require.Error(t, err)
98+ assert.Contains(t, err.Error(), "API key is required")
99+}
100+
101+func TestConfig_Validate_InvalidProviderType(t *testing.T) {
102+ cfg := &Config{
103+ Providers: map[provider.InferenceProvider]ProviderConfig{
104+ provider.InferenceProviderOpenAI: {
105+ ID: provider.InferenceProviderOpenAI,
106+ APIKey: "test-key",
107+ ProviderType: provider.Type("invalid"),
108+ },
109+ },
110+ Options: Options{
111+ DataDirectory: ".crush",
112+ ContextPaths: []string{"CRUSH.md"},
113+ },
114+ }
115+
116+ err := cfg.Validate()
117+ require.Error(t, err)
118+ assert.Contains(t, err.Error(), "invalid provider type")
119+}
120+
121+func TestConfig_Validate_CustomProviderMissingBaseURL(t *testing.T) {
122+ customProvider := provider.InferenceProvider("custom-provider")
123+ cfg := &Config{
124+ Providers: map[provider.InferenceProvider]ProviderConfig{
125+ customProvider: {
126+ ID: customProvider,
127+ APIKey: "test-key",
128+ ProviderType: provider.TypeOpenAI,
129+ // Missing BaseURL for custom provider
130+ },
131+ },
132+ Options: Options{
133+ DataDirectory: ".crush",
134+ ContextPaths: []string{"CRUSH.md"},
135+ },
136+ }
137+
138+ err := cfg.Validate()
139+ require.Error(t, err)
140+ assert.Contains(t, err.Error(), "BaseURL is required for custom providers")
141+}
142+
143+func TestConfig_Validate_DuplicateModelIDs(t *testing.T) {
144+ cfg := &Config{
145+ Providers: map[provider.InferenceProvider]ProviderConfig{
146+ provider.InferenceProviderOpenAI: {
147+ ID: provider.InferenceProviderOpenAI,
148+ APIKey: "test-key",
149+ ProviderType: provider.TypeOpenAI,
150+ Models: []Model{
151+ {
152+ ID: "gpt-4",
153+ Name: "GPT-4",
154+ ContextWindow: 8192,
155+ DefaultMaxTokens: 4096,
156+ },
157+ {
158+ ID: "gpt-4", // Duplicate ID
159+ Name: "GPT-4 Duplicate",
160+ ContextWindow: 8192,
161+ DefaultMaxTokens: 4096,
162+ },
163+ },
164+ },
165+ },
166+ Options: Options{
167+ DataDirectory: ".crush",
168+ ContextPaths: []string{"CRUSH.md"},
169+ },
170+ }
171+
172+ err := cfg.Validate()
173+ require.Error(t, err)
174+ assert.Contains(t, err.Error(), "duplicate model ID")
175+}
176+
177+func TestConfig_Validate_InvalidModelFields(t *testing.T) {
178+ cfg := &Config{
179+ Providers: map[provider.InferenceProvider]ProviderConfig{
180+ provider.InferenceProviderOpenAI: {
181+ ID: provider.InferenceProviderOpenAI,
182+ APIKey: "test-key",
183+ ProviderType: provider.TypeOpenAI,
184+ Models: []Model{
185+ {
186+ ID: "", // Empty ID
187+ Name: "GPT-4",
188+ ContextWindow: 0, // Invalid context window
189+ DefaultMaxTokens: -1, // Invalid max tokens
190+ CostPer1MIn: -5.0, // Negative cost
191+ },
192+ },
193+ },
194+ },
195+ Options: Options{
196+ DataDirectory: ".crush",
197+ ContextPaths: []string{"CRUSH.md"},
198+ },
199+ }
200+
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
205+}
206+
207+func TestConfig_Validate_DefaultModelNotFound(t *testing.T) {
208+ cfg := &Config{
209+ Providers: map[provider.InferenceProvider]ProviderConfig{
210+ provider.InferenceProviderOpenAI: {
211+ ID: provider.InferenceProviderOpenAI,
212+ APIKey: "test-key",
213+ ProviderType: provider.TypeOpenAI,
214+ DefaultLargeModel: "nonexistent-model",
215+ Models: []Model{
216+ {
217+ ID: "gpt-4",
218+ Name: "GPT-4",
219+ ContextWindow: 8192,
220+ DefaultMaxTokens: 4096,
221+ },
222+ },
223+ },
224+ },
225+ Options: Options{
226+ DataDirectory: ".crush",
227+ ContextPaths: []string{"CRUSH.md"},
228+ },
229+ }
230+
231+ err := cfg.Validate()
232+ require.Error(t, err)
233+ assert.Contains(t, err.Error(), "default large model 'nonexistent-model' not found")
234+}
235+
236+func TestConfig_Validate_AgentIDMismatch(t *testing.T) {
237+ cfg := &Config{
238+ Agents: map[AgentID]Agent{
239+ AgentCoder: {
240+ ID: AgentTask, // Wrong ID
241+ Name: "Coder",
242+ },
243+ },
244+ Options: Options{
245+ DataDirectory: ".crush",
246+ ContextPaths: []string{"CRUSH.md"},
247+ },
248+ }
249+
250+ err := cfg.Validate()
251+ require.Error(t, err)
252+ assert.Contains(t, err.Error(), "agent ID mismatch")
253+}
254+
255+func TestConfig_Validate_InvalidAgentModelType(t *testing.T) {
256+ cfg := &Config{
257+ Agents: map[AgentID]Agent{
258+ AgentCoder: {
259+ ID: AgentCoder,
260+ Name: "Coder",
261+ Model: ModelType("invalid"),
262+ },
263+ },
264+ Options: Options{
265+ DataDirectory: ".crush",
266+ ContextPaths: []string{"CRUSH.md"},
267+ },
268+ }
269+
270+ err := cfg.Validate()
271+ require.Error(t, err)
272+ assert.Contains(t, err.Error(), "invalid model type")
273+}
274+
275+func TestConfig_Validate_UnknownTool(t *testing.T) {
276+ cfg := &Config{
277+ Agents: map[AgentID]Agent{
278+ AgentID("custom-agent"): {
279+ ID: AgentID("custom-agent"),
280+ Name: "Custom Agent",
281+ Model: LargeModel,
282+ AllowedTools: []string{"unknown-tool"},
283+ },
284+ },
285+ Options: Options{
286+ DataDirectory: ".crush",
287+ ContextPaths: []string{"CRUSH.md"},
288+ },
289+ }
290+
291+ err := cfg.Validate()
292+ require.Error(t, err)
293+ assert.Contains(t, err.Error(), "unknown tool")
294+}
295+
296+func TestConfig_Validate_MCPReference(t *testing.T) {
297+ cfg := &Config{
298+ Agents: map[AgentID]Agent{
299+ AgentID("custom-agent"): {
300+ ID: AgentID("custom-agent"),
301+ Name: "Custom Agent",
302+ Model: LargeModel,
303+ AllowedMCP: map[string][]string{"nonexistent-mcp": nil},
304+ },
305+ },
306+ MCP: map[string]MCP{}, // Empty MCP map
307+ Options: Options{
308+ DataDirectory: ".crush",
309+ ContextPaths: []string{"CRUSH.md"},
310+ },
311+ }
312+
313+ err := cfg.Validate()
314+ require.Error(t, err)
315+ assert.Contains(t, err.Error(), "referenced MCP 'nonexistent-mcp' not found")
316+}
317+
318+func TestConfig_Validate_InvalidMCPType(t *testing.T) {
319+ cfg := &Config{
320+ MCP: map[string]MCP{
321+ "test-mcp": {
322+ Type: MCPType("invalid"),
323+ },
324+ },
325+ Options: Options{
326+ DataDirectory: ".crush",
327+ ContextPaths: []string{"CRUSH.md"},
328+ },
329+ }
330+
331+ err := cfg.Validate()
332+ require.Error(t, err)
333+ assert.Contains(t, err.Error(), "invalid MCP type")
334+}
335+
336+func TestConfig_Validate_MCPMissingCommand(t *testing.T) {
337+ cfg := &Config{
338+ MCP: map[string]MCP{
339+ "test-mcp": {
340+ Type: MCPStdio,
341+ // Missing Command
342+ },
343+ },
344+ Options: Options{
345+ DataDirectory: ".crush",
346+ ContextPaths: []string{"CRUSH.md"},
347+ },
348+ }
349+
350+ err := cfg.Validate()
351+ require.Error(t, err)
352+ assert.Contains(t, err.Error(), "command is required for stdio MCP")
353+}
354+
355+func TestConfig_Validate_LSPMissingCommand(t *testing.T) {
356+ cfg := &Config{
357+ LSP: map[string]LSPConfig{
358+ "test-lsp": {
359+ // Missing Command
360+ },
361+ },
362+ Options: Options{
363+ DataDirectory: ".crush",
364+ ContextPaths: []string{"CRUSH.md"},
365+ },
366+ }
367+
368+ err := cfg.Validate()
369+ require.Error(t, err)
370+ assert.Contains(t, err.Error(), "command is required for LSP")
371+}
372+
373+func TestConfig_Validate_NoValidProviders(t *testing.T) {
374+ cfg := &Config{
375+ Providers: map[provider.InferenceProvider]ProviderConfig{
376+ provider.InferenceProviderOpenAI: {
377+ ID: provider.InferenceProviderOpenAI,
378+ APIKey: "test-key",
379+ ProviderType: provider.TypeOpenAI,
380+ Disabled: true, // Disabled
381+ },
382+ },
383+ Options: Options{
384+ DataDirectory: ".crush",
385+ ContextPaths: []string{"CRUSH.md"},
386+ },
387+ }
388+
389+ err := cfg.Validate()
390+ require.Error(t, err)
391+ assert.Contains(t, err.Error(), "at least one non-disabled provider is required")
392+}
393+
394+func TestConfig_Validate_MissingDefaultAgents(t *testing.T) {
395+ cfg := &Config{
396+ Providers: map[provider.InferenceProvider]ProviderConfig{
397+ provider.InferenceProviderOpenAI: {
398+ ID: provider.InferenceProviderOpenAI,
399+ APIKey: "test-key",
400+ ProviderType: provider.TypeOpenAI,
401+ },
402+ },
403+ Agents: map[AgentID]Agent{}, // Missing default agents
404+ Options: Options{
405+ DataDirectory: ".crush",
406+ ContextPaths: []string{"CRUSH.md"},
407+ },
408+ }
409+
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")
414+}
415+
416+func TestConfig_Validate_KnownAgentProtection(t *testing.T) {
417+ cfg := &Config{
418+ Agents: map[AgentID]Agent{
419+ AgentCoder: {
420+ ID: AgentCoder,
421+ Name: "Modified Coder", // Should not be allowed
422+ Description: "Modified description", // Should not be allowed
423+ Model: LargeModel,
424+ },
425+ },
426+ Options: Options{
427+ DataDirectory: ".crush",
428+ ContextPaths: []string{"CRUSH.md"},
429+ },
430+ }
431+
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")
436+}
437+
438+func TestConfig_Validate_EmptyDataDirectory(t *testing.T) {
439+ cfg := &Config{
440+ Options: Options{
441+ DataDirectory: "", // Empty
442+ ContextPaths: []string{"CRUSH.md"},
443+ },
444+ }
445+
446+ err := cfg.Validate()
447+ require.Error(t, err)
448+ assert.Contains(t, err.Error(), "data directory is required")
449+}
450+
451+func TestConfig_Validate_EmptyContextPath(t *testing.T) {
452+ cfg := &Config{
453+ Options: Options{
454+ DataDirectory: ".crush",
455+ ContextPaths: []string{""}, // Empty context path
456+ },
457+ }
458+
459+ err := cfg.Validate()
460+ require.Error(t, err)
461+ assert.Contains(t, err.Error(), "context path cannot be empty")
462+}