mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add test to confirm this works with custom modes
This commit is contained in:
@@ -22,6 +22,7 @@ const GroupOptionsSchema = z.object({
|
|||||||
},
|
},
|
||||||
{ message: "Invalid regular expression pattern" },
|
{ message: "Invalid regular expression pattern" },
|
||||||
),
|
),
|
||||||
|
fileRegexDescription: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Schema for a group entry - either a tool group string or a tuple of [group, options]
|
// Schema for a group entry - either a tool group string or a tuple of [group, options]
|
||||||
|
|||||||
@@ -1,72 +1,194 @@
|
|||||||
import { CustomModeSchema } from "../CustomModesSchema"
|
import { ZodError } from "zod"
|
||||||
|
import { CustomModeSchema, validateCustomMode } from "../CustomModesSchema"
|
||||||
|
import { ModeConfig } from "../../../shared/modes"
|
||||||
|
|
||||||
describe("CustomModeSchema", () => {
|
describe("CustomModeSchema", () => {
|
||||||
it("validates a basic mode configuration", () => {
|
describe("validateCustomMode", () => {
|
||||||
const validMode = {
|
test("accepts valid mode configuration", () => {
|
||||||
slug: "test-mode",
|
const validMode = {
|
||||||
name: "Test Mode",
|
|
||||||
roleDefinition: "Test role definition",
|
|
||||||
groups: ["read", "browser"],
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(() => CustomModeSchema.parse(validMode)).not.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("validates a mode with file restrictions", () => {
|
|
||||||
const modeWithFileRestrictions = {
|
|
||||||
slug: "markdown-editor",
|
|
||||||
name: "Markdown Editor",
|
|
||||||
roleDefinition: "Markdown editing mode",
|
|
||||||
groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(() => CustomModeSchema.parse(modeWithFileRestrictions)).not.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("validates file regex patterns", () => {
|
|
||||||
const validPatterns = ["\\.md$", ".*\\.txt$", "[a-z]+\\.js$"]
|
|
||||||
const invalidPatterns = ["[", "(unclosed", "\\"]
|
|
||||||
|
|
||||||
validPatterns.forEach((pattern) => {
|
|
||||||
const mode = {
|
|
||||||
slug: "test",
|
slug: "test",
|
||||||
name: "Test",
|
name: "Test Mode",
|
||||||
roleDefinition: "Test",
|
roleDefinition: "Test role definition",
|
||||||
groups: ["read", ["edit", { fileRegex: pattern }]],
|
groups: ["read"] as const,
|
||||||
}
|
} satisfies ModeConfig
|
||||||
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
|
||||||
|
expect(() => validateCustomMode(validMode)).not.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
invalidPatterns.forEach((pattern) => {
|
test("accepts mode with multiple groups", () => {
|
||||||
const mode = {
|
const validMode = {
|
||||||
slug: "test",
|
slug: "test",
|
||||||
name: "Test",
|
name: "Test Mode",
|
||||||
roleDefinition: "Test",
|
roleDefinition: "Test role definition",
|
||||||
groups: ["read", ["edit", { fileRegex: pattern }]],
|
groups: ["read", "edit", "browser"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(validMode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts mode with optional customInstructions", () => {
|
||||||
|
const validMode = {
|
||||||
|
slug: "test",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
customInstructions: "Custom instructions",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(validMode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects missing required fields", () => {
|
||||||
|
const invalidModes = [
|
||||||
|
{}, // All fields missing
|
||||||
|
{ name: "Test" }, // Missing most fields
|
||||||
|
{
|
||||||
|
name: "Test",
|
||||||
|
roleDefinition: "Role",
|
||||||
|
}, // Missing slug and groups
|
||||||
|
]
|
||||||
|
|
||||||
|
invalidModes.forEach((invalidMode) => {
|
||||||
|
expect(() => validateCustomMode(invalidMode)).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects invalid slug format", () => {
|
||||||
|
const invalidMode = {
|
||||||
|
slug: "not@a@valid@slug",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies Omit<ModeConfig, "slug"> & { slug: string }
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(invalidMode)).toThrow(ZodError)
|
||||||
|
expect(() => validateCustomMode(invalidMode)).toThrow("Slug must contain only letters numbers and dashes")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects empty strings in required fields", () => {
|
||||||
|
const emptyNameMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
const emptyRoleMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(emptyNameMode)).toThrow("Name is required")
|
||||||
|
expect(() => validateCustomMode(emptyRoleMode)).toThrow("Role definition is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects invalid group configurations", () => {
|
||||||
|
const invalidGroupMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["not-a-valid-group"] as any,
|
||||||
}
|
}
|
||||||
expect(() => CustomModeSchema.parse(mode)).toThrow()
|
|
||||||
|
expect(() => validateCustomMode(invalidGroupMode)).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects empty groups array", () => {
|
||||||
|
const invalidMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: [] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(invalidMode)).toThrow("At least one tool group is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("handles null and undefined gracefully", () => {
|
||||||
|
expect(() => validateCustomMode(null)).toThrow(ZodError)
|
||||||
|
expect(() => validateCustomMode(undefined)).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects non-object inputs", () => {
|
||||||
|
const invalidInputs = [42, "string", true, [], () => {}]
|
||||||
|
|
||||||
|
invalidInputs.forEach((input) => {
|
||||||
|
expect(() => validateCustomMode(input)).toThrow(ZodError)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("prevents duplicate groups", () => {
|
describe("fileRegex", () => {
|
||||||
const modeWithDuplicates = {
|
it("validates a mode with file restrictions and descriptions", () => {
|
||||||
slug: "test",
|
const modeWithJustRegex = {
|
||||||
name: "Test",
|
slug: "markdown-editor",
|
||||||
roleDefinition: "Test",
|
name: "Markdown Editor",
|
||||||
groups: ["read", "read", ["edit", { fileRegex: "\\.md$" }], ["edit", { fileRegex: "\\.txt$" }]],
|
roleDefinition: "Markdown editing mode",
|
||||||
}
|
groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
|
||||||
|
}
|
||||||
|
|
||||||
expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
|
const modeWithDescription = {
|
||||||
})
|
slug: "docs-editor",
|
||||||
|
name: "Documentation Editor",
|
||||||
|
roleDefinition: "Documentation editing mode",
|
||||||
|
groups: [
|
||||||
|
"read",
|
||||||
|
["edit", { fileRegex: "\\.(md|txt)$", fileRegexDescription: "Documentation files only" }],
|
||||||
|
"browser",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
it("requires at least one group", () => {
|
expect(() => CustomModeSchema.parse(modeWithJustRegex)).not.toThrow()
|
||||||
const modeWithNoGroups = {
|
expect(() => CustomModeSchema.parse(modeWithDescription)).not.toThrow()
|
||||||
slug: "test",
|
})
|
||||||
name: "Test",
|
|
||||||
roleDefinition: "Test",
|
|
||||||
groups: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(() => CustomModeSchema.parse(modeWithNoGroups)).toThrow(/At least one tool group is required/)
|
it("validates file regex patterns", () => {
|
||||||
|
const validPatterns = ["\\.md$", ".*\\.txt$", "[a-z]+\\.js$"]
|
||||||
|
const invalidPatterns = ["[", "(unclosed", "\\"]
|
||||||
|
|
||||||
|
validPatterns.forEach((pattern) => {
|
||||||
|
const mode = {
|
||||||
|
slug: "test",
|
||||||
|
name: "Test",
|
||||||
|
roleDefinition: "Test",
|
||||||
|
groups: ["read", ["edit", { fileRegex: pattern }]],
|
||||||
|
}
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
invalidPatterns.forEach((pattern) => {
|
||||||
|
const mode = {
|
||||||
|
slug: "test",
|
||||||
|
name: "Test",
|
||||||
|
roleDefinition: "Test",
|
||||||
|
groups: ["read", ["edit", { fileRegex: pattern }]],
|
||||||
|
}
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("prevents duplicate groups", () => {
|
||||||
|
const modeWithDuplicates = {
|
||||||
|
slug: "test",
|
||||||
|
name: "Test",
|
||||||
|
roleDefinition: "Test",
|
||||||
|
groups: ["read", "read", ["edit", { fileRegex: "\\.md$" }], ["edit", { fileRegex: "\\.txt$" }]],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("requires at least one group", () => {
|
||||||
|
const modeWithNoGroups = {
|
||||||
|
slug: "test",
|
||||||
|
name: "Test",
|
||||||
|
roleDefinition: "Test",
|
||||||
|
groups: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(modeWithNoGroups)).toThrow(/At least one tool group is required/)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export function getRulesSection(
|
|||||||
supportsComputerUse: boolean,
|
supportsComputerUse: boolean,
|
||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
context?: vscode.ExtensionContext,
|
context?: vscode.ExtensionContext,
|
||||||
diffEnabled?: boolean,
|
|
||||||
): string {
|
): string {
|
||||||
const settingsDir = context ? path.join(context.globalStorageUri.fsPath, "settings") : "<settings directory>"
|
const settingsDir = context ? path.join(context.globalStorageUri.fsPath, "settings") : "<settings directory>"
|
||||||
const customModesPath = path.join(settingsDir, "cline_custom_modes.json")
|
const customModesPath = path.join(settingsDir, "cline_custom_modes.json")
|
||||||
|
|||||||
@@ -84,6 +84,62 @@ describe("isToolAllowedForMode", () => {
|
|||||||
expect(diffError).toBeInstanceOf(FileRestrictionError)
|
expect(diffError).toBeInstanceOf(FileRestrictionError)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("uses description in file restriction error for custom modes", () => {
|
||||||
|
const customModesWithDescription: ModeConfig[] = [
|
||||||
|
{
|
||||||
|
slug: "docs-editor",
|
||||||
|
name: "Documentation Editor",
|
||||||
|
roleDefinition: "You are a documentation editor",
|
||||||
|
groups: [
|
||||||
|
"read",
|
||||||
|
["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }],
|
||||||
|
"browser",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Test write_to_file with non-matching file
|
||||||
|
const writeError = isToolAllowedForMode(
|
||||||
|
"write_to_file",
|
||||||
|
"docs-editor",
|
||||||
|
customModesWithDescription,
|
||||||
|
undefined,
|
||||||
|
"test.js",
|
||||||
|
)
|
||||||
|
expect(writeError).toBeInstanceOf(FileRestrictionError)
|
||||||
|
expect((writeError as FileRestrictionError).message).toContain("Documentation files only")
|
||||||
|
|
||||||
|
// Test apply_diff with non-matching file
|
||||||
|
const diffError = isToolAllowedForMode(
|
||||||
|
"apply_diff",
|
||||||
|
"docs-editor",
|
||||||
|
customModesWithDescription,
|
||||||
|
undefined,
|
||||||
|
"test.js",
|
||||||
|
)
|
||||||
|
expect(diffError).toBeInstanceOf(FileRestrictionError)
|
||||||
|
expect((diffError as FileRestrictionError).message).toContain("Documentation files only")
|
||||||
|
|
||||||
|
// Test that matching files are allowed
|
||||||
|
const mdResult = isToolAllowedForMode(
|
||||||
|
"write_to_file",
|
||||||
|
"docs-editor",
|
||||||
|
customModesWithDescription,
|
||||||
|
undefined,
|
||||||
|
"test.md",
|
||||||
|
)
|
||||||
|
expect(mdResult).toBe(true)
|
||||||
|
|
||||||
|
const txtResult = isToolAllowedForMode(
|
||||||
|
"write_to_file",
|
||||||
|
"docs-editor",
|
||||||
|
customModesWithDescription,
|
||||||
|
undefined,
|
||||||
|
"test.txt",
|
||||||
|
)
|
||||||
|
expect(txtResult).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it("allows ask mode to edit markdown files only", () => {
|
it("allows ask mode to edit markdown files only", () => {
|
||||||
// Should allow editing markdown files
|
// Should allow editing markdown files
|
||||||
const mdResult = isToolAllowedForMode("write_to_file", "ask", [], undefined, "test.md")
|
const mdResult = isToolAllowedForMode("write_to_file", "ask", [], undefined, "test.md")
|
||||||
|
|||||||
Reference in New Issue
Block a user