validation_test.go 11 KB

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