Remove redundant zod schemas (#2051)

Add changeset
This commit is contained in:
Chris Estreich 2025-03-27 21:59:51 -07:00 committed by GitHub
parent 3d0fc3ccc5
commit d4c7493674
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 184 additions and 192 deletions

View file

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

View file

@ -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

View file

@ -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)
}

View file

@ -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()
})
})

View file

@ -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()
})
})
})

View file

@ -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()
})
})
})

View file

@ -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
*/

View file

@ -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) => {