Skip to content

Commit 4e77fb9

Browse files
committed
Allow architect mode to write md files
1 parent 0a32e24 commit 4e77fb9

File tree

9 files changed

+417
-137
lines changed

9 files changed

+417
-137
lines changed

src/core/config/CustomModesSchema.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,40 @@ import { TOOL_GROUPS, ToolGroup } from "../../shared/tool-groups"
55
// Create a schema for valid tool groups using the keys of TOOL_GROUPS
66
const ToolGroupSchema = z.enum(Object.keys(TOOL_GROUPS) as [ToolGroup, ...ToolGroup[]])
77

8+
// Schema for group options with regex validation
9+
const GroupOptionsSchema = z.object({
10+
fileRegex: z
11+
.string()
12+
.optional()
13+
.refine(
14+
(pattern) => {
15+
if (!pattern) return true // Optional, so empty is valid
16+
try {
17+
new RegExp(pattern)
18+
return true
19+
} catch {
20+
return false
21+
}
22+
},
23+
{ message: "Invalid regular expression pattern" },
24+
),
25+
})
26+
27+
// Schema for a group entry - either a tool group string or a tuple of [group, options]
28+
const GroupEntrySchema = z.union([ToolGroupSchema, z.tuple([ToolGroupSchema, GroupOptionsSchema])])
29+
830
// Schema for array of groups
931
const GroupsArraySchema = z
10-
.array(ToolGroupSchema)
32+
.array(GroupEntrySchema)
1133
.min(1, "At least one tool group is required")
1234
.refine(
1335
(groups) => {
1436
const seen = new Set()
1537
return groups.every((group) => {
16-
if (seen.has(group)) return false
17-
seen.add(group)
38+
// For tuples, check the group name (first element)
39+
const groupName = Array.isArray(group) ? group[0] : group
40+
if (seen.has(groupName)) return false
41+
seen.add(groupName)
1842
return true
1943
})
2044
},
Lines changed: 58 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,72 @@
1-
import { validateCustomMode } from "../CustomModesSchema"
2-
import { ModeConfig } from "../../../shared/modes"
3-
import { ZodError } from "zod"
4-
5-
describe("CustomModesSchema", () => {
6-
describe("validateCustomMode", () => {
7-
test("accepts valid mode configuration", () => {
8-
const validMode = {
9-
slug: "123e4567-e89b-12d3-a456-426614174000",
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: "123e4567-e89b-12d3-a456-426614174000",
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: "123e4567-e89b-12d3-a456-426614174000",
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-
]
50-
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 }
1+
import { CustomModeSchema } from "../CustomModesSchema"
2+
3+
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+
})
6314

64-
expect(() => validateCustomMode(invalidMode)).toThrow(ZodError)
65-
expect(() => validateCustomMode(invalidMode)).toThrow("Slug must contain only letters numbers and dashes")
66-
})
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+
}
6722

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
23+
expect(() => CustomModeSchema.parse(modeWithFileRestrictions)).not.toThrow()
24+
})
7525

76-
const emptyRoleMode = {
77-
slug: "123e4567-e89b-12d3-a456-426614174000",
78-
name: "Test Mode",
79-
roleDefinition: "",
80-
groups: ["read"] as const,
81-
} satisfies ModeConfig
26+
it("validates file regex patterns", () => {
27+
const validPatterns = ["\\.md$", ".*\\.txt$", "[a-z]+\\.js$"]
28+
const invalidPatterns = ["[", "(unclosed", "\\"]
8229

83-
expect(() => validateCustomMode(emptyNameMode)).toThrow("Name is required")
84-
expect(() => validateCustomMode(emptyRoleMode)).toThrow("Role definition is required")
30+
validPatterns.forEach((pattern) => {
31+
const mode = {
32+
slug: "test",
33+
name: "Test",
34+
roleDefinition: "Test",
35+
groups: ["read", ["edit", { fileRegex: pattern }]],
36+
}
37+
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
8538
})
8639

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,
40+
invalidPatterns.forEach((pattern) => {
41+
const mode = {
42+
slug: "test",
43+
name: "Test",
44+
roleDefinition: "Test",
45+
groups: ["read", ["edit", { fileRegex: pattern }]],
9346
}
94-
95-
expect(() => validateCustomMode(invalidGroupMode)).toThrow(ZodError)
47+
expect(() => CustomModeSchema.parse(mode)).toThrow()
9648
})
49+
})
9750

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-
})
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+
}
10858

109-
test("handles null and undefined gracefully", () => {
110-
expect(() => validateCustomMode(null)).toThrow(ZodError)
111-
expect(() => validateCustomMode(undefined)).toThrow(ZodError)
112-
})
59+
expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
60+
})
11361

114-
test("rejects non-object inputs", () => {
115-
const invalidInputs = [42, "string", true, [], () => {}]
62+
it("requires at least one group", () => {
63+
const modeWithNoGroups = {
64+
slug: "test",
65+
name: "Test",
66+
roleDefinition: "Test",
67+
groups: [],
68+
}
11669

117-
invalidInputs.forEach((input) => {
118-
expect(() => validateCustomMode(input)).toThrow(ZodError)
119-
})
120-
})
70+
expect(() => CustomModeSchema.parse(modeWithNoGroups)).toThrow(/At least one tool group is required/)
12171
})
12272
})

0 commit comments

Comments
 (0)