diff --git a/.changeset/great-mice-turn.md b/.changeset/great-mice-turn.md new file mode 100644 index 0000000000..0f8f9aea07 --- /dev/null +++ b/.changeset/great-mice-turn.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Remove redundant zod schemas diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index c701f38f91..cfbe29dcb9 100644 --- a/src/core/config/CustomModesManager.ts +++ b/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 diff --git a/src/core/config/CustomModesSchema.ts b/src/core/config/CustomModesSchema.ts deleted file mode 100644 index 4cf85cbe45..0000000000 --- a/src/core/config/CustomModesSchema.ts +++ /dev/null @@ -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 - -// 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 - -/** - * 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) -} diff --git a/src/core/config/__tests__/CustomModesSettings.test.ts b/src/core/config/__tests__/CustomModesSettings.test.ts index 322cd839e7..247bced8b3 100644 --- a/src/core/config/__tests__/CustomModesSettings.test.ts +++ b/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() }) }) diff --git a/src/core/config/__tests__/GroupConfigSchema.test.ts b/src/core/config/__tests__/GroupConfigSchema.test.ts deleted file mode 100644 index 68d0f6b69a..0000000000 --- a/src/core/config/__tests__/GroupConfigSchema.test.ts +++ /dev/null @@ -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() - }) - }) -}) diff --git a/src/core/config/__tests__/CustomModesSchema.test.ts b/src/core/config/__tests__/ModeConfig.test.ts similarity index 66% rename from src/core/config/__tests__/CustomModesSchema.test.ts rename to src/core/config/__tests__/ModeConfig.test.ts index f46bac2c7e..e246a7ec4b 100644 --- a/src/core/config/__tests__/CustomModesSchema.test.ts +++ b/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() }) }) }) diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 68390d795f..932fd490c9 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -152,7 +152,24 @@ export type HistoryItem = z.infer */ 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 * 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 +/** + * 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 + /** * PromptComponent */ diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index d1b0a9d732..62982b827c 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/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) => {