فهرست منبع

Remove redundant zod schemas (#2051)

Add changeset
Chris Estreich 9 ماه پیش
والد
کامیت
d4c7493674

+ 5 - 0
.changeset/great-mice-turn.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Remove redundant zod schemas

+ 4 - 3
src/core/config/CustomModesManager.ts

@@ -1,7 +1,7 @@
 import * as vscode from "vscode"
 import * as path from "path"
 import * as fs from "fs/promises"
-import { CustomModesSettingsSchema } from "./CustomModesSchema"
+import { customModesSettingsSchema } from "../../schemas"
 import { ModeConfig } from "../../shared/modes"
 import { fileExistsAtPath } from "../../utils/fs"
 import { arePathsEqual, getWorkspacePath } from "../../utils/path"
@@ -62,7 +62,7 @@ export class CustomModesManager {
 		try {
 			const content = await fs.readFile(filePath, "utf-8")
 			const settings = JSON.parse(content)
-			const result = CustomModesSettingsSchema.safeParse(settings)
+			const result = customModesSettingsSchema.safeParse(settings)
 			if (!result.success) {
 				return []
 			}
@@ -144,7 +144,8 @@ export class CustomModesManager {
 						return
 					}
 
-					const result = CustomModesSettingsSchema.safeParse(config)
+					const result = customModesSettingsSchema.safeParse(config)
+
 					if (!result.success) {
 						vscode.window.showErrorMessage(errorMessage)
 						return

+ 0 - 82
src/core/config/CustomModesSchema.ts

@@ -1,82 +0,0 @@
-import { z } from "zod"
-import { ModeConfig } from "../../shared/modes"
-import { TOOL_GROUPS, ToolGroup } from "../../shared/tool-groups"
-
-// Create a schema for valid tool groups using the keys of TOOL_GROUPS
-const ToolGroupSchema = z.enum(Object.keys(TOOL_GROUPS) as [ToolGroup, ...ToolGroup[]])
-
-// Schema for group options with regex validation
-const GroupOptionsSchema = z.object({
-	fileRegex: z
-		.string()
-		.optional()
-		.refine(
-			(pattern) => {
-				if (!pattern) return true // Optional, so empty is valid
-				try {
-					new RegExp(pattern)
-					return true
-				} catch {
-					return false
-				}
-			},
-			{ message: "Invalid regular expression pattern" },
-		),
-	description: z.string().optional(),
-})
-
-// Schema for a group entry - either a tool group string or a tuple of [group, options]
-const GroupEntrySchema = z.union([ToolGroupSchema, z.tuple([ToolGroupSchema, GroupOptionsSchema])])
-
-// Schema for array of groups
-const GroupsArraySchema = z.array(GroupEntrySchema).refine(
-	(groups) => {
-		const seen = new Set()
-		return groups.every((group) => {
-			// For tuples, check the group name (first element)
-			const groupName = Array.isArray(group) ? group[0] : group
-			if (seen.has(groupName)) return false
-			seen.add(groupName)
-			return true
-		})
-	},
-	{ message: "Duplicate groups are not allowed" },
-)
-
-// Schema for mode configuration
-export const CustomModeSchema = z.object({
-	slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
-	name: z.string().min(1, "Name is required"),
-	roleDefinition: z.string().min(1, "Role definition is required"),
-	customInstructions: z.string().optional(),
-	groups: GroupsArraySchema,
-}) satisfies z.ZodType<ModeConfig>
-
-// Schema for the entire custom modes settings file
-export const CustomModesSettingsSchema = z.object({
-	customModes: z.array(CustomModeSchema).refine(
-		(modes) => {
-			const slugs = new Set()
-			return modes.every((mode) => {
-				if (slugs.has(mode.slug)) {
-					return false
-				}
-				slugs.add(mode.slug)
-				return true
-			})
-		},
-		{
-			message: "Duplicate mode slugs are not allowed",
-		},
-	),
-})
-
-export type CustomModesSettings = z.infer<typeof CustomModesSettingsSchema>
-
-/**
- * Validates a custom mode configuration against the schema
- * @throws {z.ZodError} if validation fails
- */
-export function validateCustomMode(mode: unknown): asserts mode is ModeConfig {
-	CustomModeSchema.parse(mode)
-}

+ 15 - 13
src/core/config/__tests__/CustomModesSettings.test.ts

@@ -1,4 +1,6 @@
-import { CustomModesSettingsSchema } from "../CustomModesSchema"
+// npx jest src/core/config/__tests__/CustomModesSettings.test.ts
+
+import { customModesSettingsSchema } from "../../../schemas"
 import { ModeConfig } from "../../../shared/modes"
 import { ZodError } from "zod"
 
@@ -17,7 +19,7 @@ describe("CustomModesSettings", () => {
 			}
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(validSettings)
+				customModesSettingsSchema.parse(validSettings)
 			}).not.toThrow()
 		})
 
@@ -27,7 +29,7 @@ describe("CustomModesSettings", () => {
 			}
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(validSettings)
+				customModesSettingsSchema.parse(validSettings)
 			}).not.toThrow()
 		})
 
@@ -44,7 +46,7 @@ describe("CustomModesSettings", () => {
 			}
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(validSettings)
+				customModesSettingsSchema.parse(validSettings)
 			}).not.toThrow()
 		})
 
@@ -52,7 +54,7 @@ describe("CustomModesSettings", () => {
 			const invalidSettings = {} as any
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(invalidSettings)
+				customModesSettingsSchema.parse(invalidSettings)
 			}).toThrow(ZodError)
 		})
 
@@ -68,10 +70,10 @@ describe("CustomModesSettings", () => {
 			}
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(invalidSettings)
+				customModesSettingsSchema.parse(invalidSettings)
 			}).toThrow(ZodError)
 			expect(() => {
-				CustomModesSettingsSchema.parse(invalidSettings)
+				customModesSettingsSchema.parse(invalidSettings)
 			}).toThrow("Slug must contain only letters numbers and dashes")
 		})
 
@@ -81,17 +83,17 @@ describe("CustomModesSettings", () => {
 			}
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(invalidSettings)
+				customModesSettingsSchema.parse(invalidSettings)
 			}).toThrow(ZodError)
 		})
 
 		test("rejects null or undefined", () => {
 			expect(() => {
-				CustomModesSettingsSchema.parse(null)
+				customModesSettingsSchema.parse(null)
 			}).toThrow(ZodError)
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(undefined)
+				customModesSettingsSchema.parse(undefined)
 			}).toThrow(ZodError)
 		})
 
@@ -104,7 +106,7 @@ describe("CustomModesSettings", () => {
 			}
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(duplicateSettings)
+				customModesSettingsSchema.parse(duplicateSettings)
 			}).toThrow("Duplicate mode slugs are not allowed")
 		})
 
@@ -119,7 +121,7 @@ describe("CustomModesSettings", () => {
 			}
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(invalidSettings)
+				customModesSettingsSchema.parse(invalidSettings)
 			}).toThrow(ZodError)
 		})
 
@@ -134,7 +136,7 @@ describe("CustomModesSettings", () => {
 			}
 
 			expect(() => {
-				CustomModesSettingsSchema.parse(validSettings)
+				customModesSettingsSchema.parse(validSettings)
 			}).not.toThrow()
 		})
 	})

+ 0 - 81
src/core/config/__tests__/GroupConfigSchema.test.ts

@@ -1,81 +0,0 @@
-import { CustomModeSchema } from "../CustomModesSchema"
-import { ModeConfig } from "../../../shared/modes"
-
-describe("GroupConfigSchema", () => {
-	const validBaseMode = {
-		slug: "123e4567-e89b-12d3-a456-426614174000",
-		name: "Test Mode",
-		roleDefinition: "Test role definition",
-	}
-
-	describe("group format validation", () => {
-		test("accepts single group", () => {
-			const mode = {
-				...validBaseMode,
-				groups: ["read"] as const,
-			} satisfies ModeConfig
-
-			expect(() => CustomModeSchema.parse(mode)).not.toThrow()
-		})
-
-		test("accepts multiple groups", () => {
-			const mode = {
-				...validBaseMode,
-				groups: ["read", "edit", "browser"] as const,
-			} satisfies ModeConfig
-
-			expect(() => CustomModeSchema.parse(mode)).not.toThrow()
-		})
-
-		test("accepts all available groups", () => {
-			const mode = {
-				...validBaseMode,
-				groups: ["read", "edit", "browser", "command", "mcp"] as const,
-			} satisfies ModeConfig
-
-			expect(() => CustomModeSchema.parse(mode)).not.toThrow()
-		})
-
-		test("rejects non-array group format", () => {
-			const mode = {
-				...validBaseMode,
-				groups: "not-an-array" as any,
-			}
-
-			expect(() => CustomModeSchema.parse(mode)).toThrow()
-		})
-
-		test("rejects invalid group names", () => {
-			const mode = {
-				...validBaseMode,
-				groups: ["invalid_group"] as any,
-			}
-
-			expect(() => CustomModeSchema.parse(mode)).toThrow()
-		})
-
-		test("rejects duplicate groups", () => {
-			const mode = {
-				...validBaseMode,
-				groups: ["read", "read"] as any,
-			}
-
-			expect(() => CustomModeSchema.parse(mode)).toThrow("Duplicate groups are not allowed")
-		})
-
-		test("rejects null or undefined groups", () => {
-			const modeWithNull = {
-				...validBaseMode,
-				groups: null as any,
-			}
-
-			const modeWithUndefined = {
-				...validBaseMode,
-				groups: undefined as any,
-			}
-
-			expect(() => CustomModeSchema.parse(modeWithNull)).toThrow()
-			expect(() => CustomModeSchema.parse(modeWithUndefined)).toThrow()
-		})
-	})
-})

+ 90 - 6
src/core/config/__tests__/CustomModesSchema.test.ts → src/core/config/__tests__/ModeConfig.test.ts

@@ -1,7 +1,14 @@
+// npx jest src/core/config/__tests__/ModeConfig.test.ts
+
 import { ZodError } from "zod"
-import { CustomModeSchema, validateCustomMode } from "../CustomModesSchema"
+
+import { modeConfigSchema } from "../../../schemas"
 import { ModeConfig } from "../../../shared/modes"
 
+function validateCustomMode(mode: unknown): asserts mode is ModeConfig {
+	modeConfigSchema.parse(mode)
+}
+
 describe("CustomModeSchema", () => {
 	describe("validateCustomMode", () => {
 		test("accepts valid mode configuration", () => {
@@ -129,8 +136,8 @@ describe("CustomModeSchema", () => {
 				],
 			}
 
-			expect(() => CustomModeSchema.parse(modeWithJustRegex)).not.toThrow()
-			expect(() => CustomModeSchema.parse(modeWithDescription)).not.toThrow()
+			expect(() => modeConfigSchema.parse(modeWithJustRegex)).not.toThrow()
+			expect(() => modeConfigSchema.parse(modeWithDescription)).not.toThrow()
 		})
 
 		it("validates file regex patterns", () => {
@@ -144,7 +151,7 @@ describe("CustomModeSchema", () => {
 					roleDefinition: "Test",
 					groups: ["read", ["edit", { fileRegex: pattern }]],
 				}
-				expect(() => CustomModeSchema.parse(mode)).not.toThrow()
+				expect(() => modeConfigSchema.parse(mode)).not.toThrow()
 			})
 
 			invalidPatterns.forEach((pattern) => {
@@ -154,7 +161,7 @@ describe("CustomModeSchema", () => {
 					roleDefinition: "Test",
 					groups: ["read", ["edit", { fileRegex: pattern }]],
 				}
-				expect(() => CustomModeSchema.parse(mode)).toThrow()
+				expect(() => modeConfigSchema.parse(mode)).toThrow()
 			})
 		})
 
@@ -166,7 +173,84 @@ describe("CustomModeSchema", () => {
 				groups: ["read", "read", ["edit", { fileRegex: "\\.md$" }], ["edit", { fileRegex: "\\.txt$" }]],
 			}
 
-			expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
+			expect(() => modeConfigSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
+		})
+	})
+
+	const validBaseMode = {
+		slug: "123e4567-e89b-12d3-a456-426614174000",
+		name: "Test Mode",
+		roleDefinition: "Test role definition",
+	}
+
+	describe("group format validation", () => {
+		test("accepts single group", () => {
+			const mode = {
+				...validBaseMode,
+				groups: ["read"] as const,
+			} satisfies ModeConfig
+
+			expect(() => modeConfigSchema.parse(mode)).not.toThrow()
+		})
+
+		test("accepts multiple groups", () => {
+			const mode = {
+				...validBaseMode,
+				groups: ["read", "edit", "browser"] as const,
+			} satisfies ModeConfig
+
+			expect(() => modeConfigSchema.parse(mode)).not.toThrow()
+		})
+
+		test("accepts all available groups", () => {
+			const mode = {
+				...validBaseMode,
+				groups: ["read", "edit", "browser", "command", "mcp"] as const,
+			} satisfies ModeConfig
+
+			expect(() => modeConfigSchema.parse(mode)).not.toThrow()
+		})
+
+		test("rejects non-array group format", () => {
+			const mode = {
+				...validBaseMode,
+				groups: "not-an-array" as any,
+			}
+
+			expect(() => modeConfigSchema.parse(mode)).toThrow()
+		})
+
+		test("rejects invalid group names", () => {
+			const mode = {
+				...validBaseMode,
+				groups: ["invalid_group"] as any,
+			}
+
+			expect(() => modeConfigSchema.parse(mode)).toThrow()
+		})
+
+		test("rejects duplicate groups", () => {
+			const mode = {
+				...validBaseMode,
+				groups: ["read", "read"] as any,
+			}
+
+			expect(() => modeConfigSchema.parse(mode)).toThrow("Duplicate groups are not allowed")
+		})
+
+		test("rejects null or undefined groups", () => {
+			const modeWithNull = {
+				...validBaseMode,
+				groups: null as any,
+			}
+
+			const modeWithUndefined = {
+				...validBaseMode,
+				groups: undefined as any,
+			}
+
+			expect(() => modeConfigSchema.parse(modeWithNull)).toThrow()
+			expect(() => modeConfigSchema.parse(modeWithUndefined)).toThrow()
 		})
 	})
 })

+ 67 - 5
src/schemas/index.ts

@@ -152,7 +152,24 @@ export type HistoryItem = z.infer<typeof historyItemSchema>
  */
 
 export const groupOptionsSchema = z.object({
-	fileRegex: z.string().optional(),
+	fileRegex: z
+		.string()
+		.optional()
+		.refine(
+			(pattern) => {
+				if (!pattern) {
+					return true // Optional, so empty is valid.
+				}
+
+				try {
+					new RegExp(pattern)
+					return true
+				} catch {
+					return false
+				}
+			},
+			{ message: "Invalid regular expression pattern" },
+		),
 	description: z.string().optional(),
 })
 
@@ -170,17 +187,62 @@ export type GroupEntry = z.infer<typeof groupEntrySchema>
  * ModeConfig
  */
 
+const groupEntryArraySchema = z.array(groupEntrySchema).refine(
+	(groups) => {
+		const seen = new Set()
+
+		return groups.every((group) => {
+			// For tuples, check the group name (first element).
+			const groupName = Array.isArray(group) ? group[0] : group
+
+			if (seen.has(groupName)) {
+				return false
+			}
+
+			seen.add(groupName)
+			return true
+		})
+	},
+	{ message: "Duplicate groups are not allowed" },
+)
+
 export const modeConfigSchema = z.object({
-	slug: z.string(),
-	name: z.string(),
-	roleDefinition: z.string(),
+	slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
+	name: z.string().min(1, "Name is required"),
+	roleDefinition: z.string().min(1, "Role definition is required"),
 	customInstructions: z.string().optional(),
-	groups: z.array(groupEntrySchema),
+	groups: groupEntryArraySchema,
 	source: z.enum(["global", "project"]).optional(),
 })
 
 export type ModeConfig = z.infer<typeof modeConfigSchema>
 
+/**
+ * CustomModesSettings
+ */
+
+export const customModesSettingsSchema = z.object({
+	customModes: z.array(modeConfigSchema).refine(
+		(modes) => {
+			const slugs = new Set()
+
+			return modes.every((mode) => {
+				if (slugs.has(mode.slug)) {
+					return false
+				}
+
+				slugs.add(mode.slug)
+				return true
+			})
+		},
+		{
+			message: "Duplicate mode slugs are not allowed",
+		},
+	),
+})
+
+export type CustomModesSettings = z.infer<typeof customModesSettingsSchema>
+
 /**
  * PromptComponent
  */

+ 3 - 2
webview-ui/src/components/prompts/PromptsView.tsx

@@ -19,7 +19,7 @@ import {
 	ModeConfig,
 	GroupEntry,
 } from "../../../../src/shared/modes"
-import { CustomModeSchema } from "../../../../src/core/config/CustomModesSchema"
+import { modeConfigSchema } from "../../../../src/schemas"
 import { supportPrompt, SupportPromptType } from "../../../../src/shared/support-prompt"
 
 import { TOOL_GROUPS, ToolGroup } from "../../../../src/shared/tool-groups"
@@ -223,7 +223,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 		}
 
 		// Validate the mode against the schema
-		const result = CustomModeSchema.safeParse(newMode)
+		const result = modeConfigSchema.safeParse(newMode)
+
 		if (!result.success) {
 			// Map Zod errors to specific fields
 			result.error.errors.forEach((error) => {