refactor: move schema tests and ajv dep to packages/types, extract shared schema builder

- Remove ajv from src/package.json (extension workspace) since it was only
  used for schema validation tests unrelated to extension runtime code
- Add ajv as devDependency to packages/types/package.json where the schema
  generation already lives
- Move AJV validation tests from src/utils/__tests__/ to
  packages/types/src/__tests__/ alongside the sync test
- Extract shared schema-building logic into packages/types/src/roomodes-schema.ts
  to eliminate duplication between the generator script and the sync test
- Regenerate schemas/roomodes.json via the updated generator
This commit is contained in:
Roo Code 2026-04-08 22:03:34 +00:00
parent 3fd2ca47c8
commit 8725668865
7 changed files with 74 additions and 101 deletions

View file

@ -32,6 +32,7 @@
"@types/node": "^24.1.0",
"globals": "^16.3.0",
"tsup": "^8.4.0",
"ajv": "^8.18.0",
"vitest": "^3.2.3",
"zod-to-json-schema": "^3.25.1"
}

View file

@ -11,69 +11,11 @@
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from "url"
import { zodToJsonSchema } from "zod-to-json-schema"
import { z } from "zod"
import { toolGroups, deprecatedToolGroups } from "../src/tool.js"
import { groupOptionsSchema, modeConfigSchema } from "../src/mode.js"
import { generateRoomodesJsonSchema } from "../src/roomodes-schema.js"
// ---------------------------------------------------------------------------
// 1. Build a ToolGroup enum that includes deprecated groups so existing
// configs still validate.
// ---------------------------------------------------------------------------
const allToolGroups = [...toolGroups, ...deprecatedToolGroups] as [string, ...string[]]
const allToolGroupsSchema = z.enum(allToolGroups)
const jsonSchema = generateRoomodesJsonSchema()
// ---------------------------------------------------------------------------
// 2. Build a GroupEntry schema that uses the extended tool group list.
// ---------------------------------------------------------------------------
const groupEntrySchema = z.union([allToolGroupsSchema, z.tuple([allToolGroupsSchema, groupOptionsSchema])])
// ---------------------------------------------------------------------------
// 3. Build the RuleFile schema (used during import/export but not part of
// the core Zod types).
// ---------------------------------------------------------------------------
const ruleFileSchema = z.object({
relativePath: z.string(),
content: z.string().optional(),
})
// ---------------------------------------------------------------------------
// 4. Build an extended ModeConfig schema that includes rulesFiles and uses
// the extended groups (with deprecated entries).
// ---------------------------------------------------------------------------
const exportedModeConfigSchema = modeConfigSchema.omit({ groups: true }).extend({
groups: z.array(groupEntrySchema),
rulesFiles: z.array(ruleFileSchema).optional(),
})
// ---------------------------------------------------------------------------
// 5. Build the top-level .roomodes schema.
// ---------------------------------------------------------------------------
const roomodesSchema = z
.object({
customModes: z.array(exportedModeConfigSchema),
})
.strict()
// ---------------------------------------------------------------------------
// 6. Convert to JSON Schema (draft-07).
// ---------------------------------------------------------------------------
const jsonSchema = zodToJsonSchema(roomodesSchema, {
$refStrategy: "none",
target: "jsonSchema7",
}) as Record<string, unknown>
// ---------------------------------------------------------------------------
// 7. Add metadata.
// ---------------------------------------------------------------------------
jsonSchema["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json"
jsonSchema["title"] = "Roo Code Custom Modes"
jsonSchema["description"] = "Schema for .roomodes configuration files used by Roo Code to define custom modes."
// ---------------------------------------------------------------------------
// 8. Write to disk.
// ---------------------------------------------------------------------------
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(__dirname, "../../..")
const outPath = path.join(repoRoot, "schemas", "roomodes.json")

View file

@ -2,11 +2,8 @@ import { describe, it, expect } from "vitest"
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from "url"
import { zodToJsonSchema } from "zod-to-json-schema"
import { z } from "zod"
import { toolGroups, deprecatedToolGroups } from "../tool.js"
import { groupOptionsSchema, modeConfigSchema } from "../mode.js"
import { generateRoomodesJsonSchema } from "../roomodes-schema.js"
/**
* This test verifies that the checked-in schemas/roomodes.json matches what
@ -22,32 +19,7 @@ describe("roomodes schema sync", () => {
const schemaPath = path.resolve(__dirname, "../../../../schemas/roomodes.json")
const checkedIn = JSON.parse(fs.readFileSync(schemaPath, "utf-8"))
// Reproduce the same generation logic as scripts/generate-roomodes-schema.ts
const allToolGroups = [...toolGroups, ...deprecatedToolGroups] as [string, ...string[]]
const allToolGroupsSchema = z.enum(allToolGroups)
const groupEntrySchema = z.union([allToolGroupsSchema, z.tuple([allToolGroupsSchema, groupOptionsSchema])])
const ruleFileSchema = z.object({
relativePath: z.string(),
content: z.string().optional(),
})
const exportedModeConfigSchema = modeConfigSchema.omit({ groups: true }).extend({
groups: z.array(groupEntrySchema),
rulesFiles: z.array(ruleFileSchema).optional(),
})
const roomodesSchema = z
.object({
customModes: z.array(exportedModeConfigSchema),
})
.strict()
const generated = zodToJsonSchema(roomodesSchema, {
$refStrategy: "none",
target: "jsonSchema7",
}) as Record<string, unknown>
generated["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json"
generated["title"] = "Roo Code Custom Modes"
generated["description"] = "Schema for .roomodes configuration files used by Roo Code to define custom modes."
const generated = generateRoomodesJsonSchema()
expect(checkedIn).toEqual(generated)
})

View file

@ -3,23 +3,24 @@
* using AJV. The schema itself is dynamically generated from the Zod types in
* packages/types/src/mode.ts -- see packages/types/scripts/generate-roomodes-schema.ts.
*
* A separate drift-detection test in packages/types ensures the checked-in
* schema stays in sync with the Zod source of truth.
* A separate drift-detection test (roomodes-schema-sync.spec.ts) ensures the
* checked-in schema stays in sync with the Zod source of truth.
*/
import { describe, it, expect, beforeAll } from "vitest"
import Ajv from "ajv"
import Ajv, { type ValidateFunction } from "ajv"
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from "url"
describe("roomodes JSON schema", () => {
let ajv: Ajv
let schema: Record<string, unknown>
let validate: ReturnType<Ajv["compile"]>
let validate: ValidateFunction
beforeAll(() => {
const schemaPath = path.resolve(__dirname, "../../../schemas/roomodes.json")
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const schemaPath = path.resolve(__dirname, "../../../../schemas/roomodes.json")
schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"))
ajv = new Ajv({ strict: false })
const ajv = new Ajv.default({ strict: false })
validate = ajv.compile(schema)
})

View file

@ -0,0 +1,58 @@
/**
* Builds the Zod schema for .roomodes configuration files and converts it
* to JSON Schema (draft-07). This module is the single source of truth for
* both the generator script (scripts/generate-roomodes-schema.ts) and the
* drift-detection test.
*/
import { z } from "zod"
import { zodToJsonSchema } from "zod-to-json-schema"
import { toolGroups, deprecatedToolGroups } from "./tool.js"
import { groupOptionsSchema, modeConfigSchema } from "./mode.js"
// Build a ToolGroup enum that includes deprecated groups so existing configs
// still validate.
const allToolGroups = [...toolGroups, ...deprecatedToolGroups] as [string, ...string[]]
const allToolGroupsSchema = z.enum(allToolGroups)
// Build a GroupEntry schema that uses the extended tool group list.
const groupEntrySchema = z.union([allToolGroupsSchema, z.tuple([allToolGroupsSchema, groupOptionsSchema])])
// Build the RuleFile schema (used during import/export but not part of the
// core Zod types).
const ruleFileSchema = z.object({
relativePath: z.string(),
content: z.string().optional(),
})
// Build an extended ModeConfig schema that includes rulesFiles and uses the
// extended groups (with deprecated entries).
const exportedModeConfigSchema = modeConfigSchema.omit({ groups: true }).extend({
groups: z.array(groupEntrySchema),
rulesFiles: z.array(ruleFileSchema).optional(),
})
// Build the top-level .roomodes schema.
const roomodesZodSchema = z
.object({
customModes: z.array(exportedModeConfigSchema),
})
.strict()
/**
* Generates the JSON Schema object for .roomodes configuration files.
* Includes metadata fields ($id, title, description).
*/
export function generateRoomodesJsonSchema(): Record<string, unknown> {
const jsonSchema = zodToJsonSchema(roomodesZodSchema, {
$refStrategy: "none",
target: "jsonSchema7",
}) as Record<string, unknown>
jsonSchema["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json"
jsonSchema["title"] = "Roo Code Custom Modes"
jsonSchema["description"] = "Schema for .roomodes configuration files used by Roo Code to define custom modes."
return jsonSchema
}

6
pnpm-lock.yaml generated
View file

@ -722,6 +722,9 @@ importers:
'@types/node':
specifier: ^24.1.0
version: 24.2.1
ajv:
specifier: ^8.18.0
version: 8.18.0
globals:
specifier: ^16.3.0
version: 16.3.0
@ -1104,9 +1107,6 @@ importers:
ai:
specifier: ^6.0.75
version: 6.0.77(zod@3.25.76)
ajv:
specifier: ^8.18.0
version: 8.18.0
esbuild-wasm:
specifier: ^0.25.0
version: 0.25.12

View file

@ -567,7 +567,6 @@
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "3.3.2",
"ai": "^6.0.75",
"ajv": "^8.18.0",
"esbuild-wasm": "^0.25.0",
"execa": "^9.5.2",
"glob": "^11.1.0",