Skip to content

Commit 6c3c216

Browse files
committed
Add test to confirm this works with custom modes
1 parent 7413d64 commit 6c3c216

File tree

4 files changed

+231
-53
lines changed

4 files changed

+231
-53
lines changed

src/core/config/CustomModesSchema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const GroupOptionsSchema = z.object({
2222
},
2323
{ message: "Invalid regular expression pattern" },
2424
),
25+
fileRegexDescription: z.string().optional(),
2526
})
2627

2728
// Schema for a group entry - either a tool group string or a tuple of [group, options]
Lines changed: 174 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,194 @@
1-
import { CustomModeSchema } from "../CustomModesSchema"
1+
import { ZodError } from "zod"
2+
import { CustomModeSchema, validateCustomMode } from "../CustomModesSchema"
3+
import { ModeConfig } from "../../../shared/modes"
24

35
describe("CustomModeSchema", () => {
4-
it("validates a basic mode configuration", () => {
5-
const validMode = {
6-
slug: "test-mode",
7-
name: "Test Mode",
8-
roleDefinition: "Test role definition",
9-
groups: ["read", "browser"],
10-
}
11-
12-
expect(() => CustomModeSchema.parse(validMode)).not.toThrow()
13-
})
6+
describe("validateCustomMode", () => {
7+
test("accepts valid mode configuration", () => {
8+
const validMode = {
9+
slug: "test",
10+
name: "Test Mode",
11+
roleDefinition: "Test role definition",
12+
groups: ["read"] as const,
13+
} satisfies ModeConfig
14+
15+
expect(() => validateCustomMode(validMode)).not.toThrow()
16+
})
17+
18+
test("accepts mode with multiple groups", () => {
19+
const validMode = {
20+
slug: "test",
21+
name: "Test Mode",
22+
roleDefinition: "Test role definition",
23+
groups: ["read", "edit", "browser"] as const,
24+
} satisfies ModeConfig
25+
26+
expect(() => validateCustomMode(validMode)).not.toThrow()
27+
})
28+
29+
test("accepts mode with optional customInstructions", () => {
30+
const validMode = {
31+
slug: "test",
32+
name: "Test Mode",
33+
roleDefinition: "Test role definition",
34+
customInstructions: "Custom instructions",
35+
groups: ["read"] as const,
36+
} satisfies ModeConfig
37+
38+
expect(() => validateCustomMode(validMode)).not.toThrow()
39+
})
40+
41+
test("rejects missing required fields", () => {
42+
const invalidModes = [
43+
{}, // All fields missing
44+
{ name: "Test" }, // Missing most fields
45+
{
46+
name: "Test",
47+
roleDefinition: "Role",
48+
}, // Missing slug and groups
49+
]
1450

15-
it("validates a mode with file restrictions", () => {
16-
const modeWithFileRestrictions = {
17-
slug: "markdown-editor",
18-
name: "Markdown Editor",
19-
roleDefinition: "Markdown editing mode",
20-
groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
21-
}
51+
invalidModes.forEach((invalidMode) => {
52+
expect(() => validateCustomMode(invalidMode)).toThrow(ZodError)
53+
})
54+
})
55+
56+
test("rejects invalid slug format", () => {
57+
const invalidMode = {
58+
slug: "not@a@valid@slug",
59+
name: "Test Mode",
60+
roleDefinition: "Test role definition",
61+
groups: ["read"] as const,
62+
} satisfies Omit<ModeConfig, "slug"> & { slug: string }
63+
64+
expect(() => validateCustomMode(invalidMode)).toThrow(ZodError)
65+
expect(() => validateCustomMode(invalidMode)).toThrow("Slug must contain only letters numbers and dashes")
66+
})
67+
68+
test("rejects empty strings in required fields", () => {
69+
const emptyNameMode = {
70+
slug: "123e4567-e89b-12d3-a456-426614174000",
71+
name: "",
72+
roleDefinition: "Test role definition",
73+
groups: ["read"] as const,
74+
} satisfies ModeConfig
75+
76+
const emptyRoleMode = {
77+
slug: "123e4567-e89b-12d3-a456-426614174000",
78+
name: "Test Mode",
79+
roleDefinition: "",
80+
groups: ["read"] as const,
81+
} satisfies ModeConfig
82+
83+
expect(() => validateCustomMode(emptyNameMode)).toThrow("Name is required")
84+
expect(() => validateCustomMode(emptyRoleMode)).toThrow("Role definition is required")
85+
})
86+
87+
test("rejects invalid group configurations", () => {
88+
const invalidGroupMode = {
89+
slug: "123e4567-e89b-12d3-a456-426614174000",
90+
name: "Test Mode",
91+
roleDefinition: "Test role definition",
92+
groups: ["not-a-valid-group"] as any,
93+
}
94+
95+
expect(() => validateCustomMode(invalidGroupMode)).toThrow(ZodError)
96+
})
2297

23-
expect(() => CustomModeSchema.parse(modeWithFileRestrictions)).not.toThrow()
98+
test("rejects empty groups array", () => {
99+
const invalidMode = {
100+
slug: "123e4567-e89b-12d3-a456-426614174000",
101+
name: "Test Mode",
102+
roleDefinition: "Test role definition",
103+
groups: [] as const,
104+
} satisfies ModeConfig
105+
106+
expect(() => validateCustomMode(invalidMode)).toThrow("At least one tool group is required")
107+
})
108+
109+
test("handles null and undefined gracefully", () => {
110+
expect(() => validateCustomMode(null)).toThrow(ZodError)
111+
expect(() => validateCustomMode(undefined)).toThrow(ZodError)
112+
})
113+
114+
test("rejects non-object inputs", () => {
115+
const invalidInputs = [42, "string", true, [], () => {}]
116+
117+
invalidInputs.forEach((input) => {
118+
expect(() => validateCustomMode(input)).toThrow(ZodError)
119+
})
120+
})
24121
})
25122

26-
it("validates file regex patterns", () => {
27-
const validPatterns = ["\\.md$", ".*\\.txt$", "[a-z]+\\.js$"]
28-
const invalidPatterns = ["[", "(unclosed", "\\"]
123+
describe("fileRegex", () => {
124+
it("validates a mode with file restrictions and descriptions", () => {
125+
const modeWithJustRegex = {
126+
slug: "markdown-editor",
127+
name: "Markdown Editor",
128+
roleDefinition: "Markdown editing mode",
129+
groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
130+
}
131+
132+
const modeWithDescription = {
133+
slug: "docs-editor",
134+
name: "Documentation Editor",
135+
roleDefinition: "Documentation editing mode",
136+
groups: [
137+
"read",
138+
["edit", { fileRegex: "\\.(md|txt)$", fileRegexDescription: "Documentation files only" }],
139+
"browser",
140+
],
141+
}
142+
143+
expect(() => CustomModeSchema.parse(modeWithJustRegex)).not.toThrow()
144+
expect(() => CustomModeSchema.parse(modeWithDescription)).not.toThrow()
145+
})
146+
147+
it("validates file regex patterns", () => {
148+
const validPatterns = ["\\.md$", ".*\\.txt$", "[a-z]+\\.js$"]
149+
const invalidPatterns = ["[", "(unclosed", "\\"]
150+
151+
validPatterns.forEach((pattern) => {
152+
const mode = {
153+
slug: "test",
154+
name: "Test",
155+
roleDefinition: "Test",
156+
groups: ["read", ["edit", { fileRegex: pattern }]],
157+
}
158+
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
159+
})
160+
161+
invalidPatterns.forEach((pattern) => {
162+
const mode = {
163+
slug: "test",
164+
name: "Test",
165+
roleDefinition: "Test",
166+
groups: ["read", ["edit", { fileRegex: pattern }]],
167+
}
168+
expect(() => CustomModeSchema.parse(mode)).toThrow()
169+
})
170+
})
29171

30-
validPatterns.forEach((pattern) => {
31-
const mode = {
172+
it("prevents duplicate groups", () => {
173+
const modeWithDuplicates = {
32174
slug: "test",
33175
name: "Test",
34176
roleDefinition: "Test",
35-
groups: ["read", ["edit", { fileRegex: pattern }]],
177+
groups: ["read", "read", ["edit", { fileRegex: "\\.md$" }], ["edit", { fileRegex: "\\.txt$" }]],
36178
}
37-
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
179+
180+
expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
38181
})
39182

40-
invalidPatterns.forEach((pattern) => {
41-
const mode = {
183+
it("requires at least one group", () => {
184+
const modeWithNoGroups = {
42185
slug: "test",
43186
name: "Test",
44187
roleDefinition: "Test",
45-
groups: ["read", ["edit", { fileRegex: pattern }]],
188+
groups: [],
46189
}
47-
expect(() => CustomModeSchema.parse(mode)).toThrow()
48-
})
49-
})
50-
51-
it("prevents duplicate groups", () => {
52-
const modeWithDuplicates = {
53-
slug: "test",
54-
name: "Test",
55-
roleDefinition: "Test",
56-
groups: ["read", "read", ["edit", { fileRegex: "\\.md$" }], ["edit", { fileRegex: "\\.txt$" }]],
57-
}
58-
59-
expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
60-
})
61-
62-
it("requires at least one group", () => {
63-
const modeWithNoGroups = {
64-
slug: "test",
65-
name: "Test",
66-
roleDefinition: "Test",
67-
groups: [],
68-
}
69190

70-
expect(() => CustomModeSchema.parse(modeWithNoGroups)).toThrow(/At least one tool group is required/)
191+
expect(() => CustomModeSchema.parse(modeWithNoGroups)).toThrow(/At least one tool group is required/)
192+
})
71193
})
72194
})

src/core/prompts/sections/rules.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export function getRulesSection(
88
supportsComputerUse: boolean,
99
diffStrategy?: DiffStrategy,
1010
context?: vscode.ExtensionContext,
11-
diffEnabled?: boolean,
1211
): string {
1312
const settingsDir = context ? path.join(context.globalStorageUri.fsPath, "settings") : "<settings directory>"
1413
const customModesPath = path.join(settingsDir, "cline_custom_modes.json")

src/shared/__tests__/modes.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,62 @@ describe("isToolAllowedForMode", () => {
8484
expect(diffError).toBeInstanceOf(FileRestrictionError)
8585
})
8686

87+
it("uses description in file restriction error for custom modes", () => {
88+
const customModesWithDescription: ModeConfig[] = [
89+
{
90+
slug: "docs-editor",
91+
name: "Documentation Editor",
92+
roleDefinition: "You are a documentation editor",
93+
groups: [
94+
"read",
95+
["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }],
96+
"browser",
97+
],
98+
},
99+
]
100+
101+
// Test write_to_file with non-matching file
102+
const writeError = isToolAllowedForMode(
103+
"write_to_file",
104+
"docs-editor",
105+
customModesWithDescription,
106+
undefined,
107+
"test.js",
108+
)
109+
expect(writeError).toBeInstanceOf(FileRestrictionError)
110+
expect((writeError as FileRestrictionError).message).toContain("Documentation files only")
111+
112+
// Test apply_diff with non-matching file
113+
const diffError = isToolAllowedForMode(
114+
"apply_diff",
115+
"docs-editor",
116+
customModesWithDescription,
117+
undefined,
118+
"test.js",
119+
)
120+
expect(diffError).toBeInstanceOf(FileRestrictionError)
121+
expect((diffError as FileRestrictionError).message).toContain("Documentation files only")
122+
123+
// Test that matching files are allowed
124+
const mdResult = isToolAllowedForMode(
125+
"write_to_file",
126+
"docs-editor",
127+
customModesWithDescription,
128+
undefined,
129+
"test.md",
130+
)
131+
expect(mdResult).toBe(true)
132+
133+
const txtResult = isToolAllowedForMode(
134+
"write_to_file",
135+
"docs-editor",
136+
customModesWithDescription,
137+
undefined,
138+
"test.txt",
139+
)
140+
expect(txtResult).toBe(true)
141+
})
142+
87143
it("allows ask mode to edit markdown files only", () => {
88144
// Should allow editing markdown files
89145
const mdResult = isToolAllowedForMode("write_to_file", "ask", [], undefined, "test.md")

0 commit comments

Comments
 (0)