Roo-Code/src/shared/__tests__/modes.test.ts

435 lines
13 KiB
TypeScript

// Mock setup must come before imports
jest.mock("vscode")
const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions")
jest.mock("../../core/prompts/sections/custom-instructions", () => ({
addCustomInstructions: mockAddCustomInstructions,
}))
import { isToolAllowedForMode, FileRestrictionError, ModeConfig, getFullModeDetails, modes } from "../modes"
import * as vscode from "vscode"
import { addCustomInstructions } from "../../core/prompts/sections/custom-instructions"
describe("isToolAllowedForMode", () => {
const customModes: ModeConfig[] = [
{
slug: "markdown-editor",
name: "Markdown Editor",
roleDefinition: "You are a markdown editor",
groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
},
{
slug: "css-editor",
name: "CSS Editor",
roleDefinition: "You are a CSS editor",
groups: ["read", ["edit", { fileRegex: "\\.css$" }], "browser"],
},
{
slug: "test-exp-mode",
name: "Test Exp Mode",
roleDefinition: "You are an experimental tester",
groups: ["read", "edit", "browser"],
},
]
it("allows always available tools", () => {
expect(isToolAllowedForMode("ask_followup_question", "markdown-editor", customModes)).toBe(true)
expect(isToolAllowedForMode("attempt_completion", "markdown-editor", customModes)).toBe(true)
})
it("allows unrestricted tools", () => {
expect(isToolAllowedForMode("read_file", "markdown-editor", customModes)).toBe(true)
expect(isToolAllowedForMode("browser_action", "markdown-editor", customModes)).toBe(true)
})
describe("file restrictions", () => {
it("allows editing matching files", () => {
// Test markdown editor mode
const mdResult = isToolAllowedForMode("create_file", "markdown-editor", customModes, undefined, {
path: "test.md",
content: "# Test",
})
expect(mdResult).toBe(true)
// Test CSS editor mode
const cssResult = isToolAllowedForMode("create_file", "css-editor", customModes, undefined, {
path: "styles.css",
content: ".test { color: red; }",
})
expect(cssResult).toBe(true)
})
it("rejects editing non-matching files", () => {
// Test markdown editor mode with non-markdown file
expect(() =>
isToolAllowedForMode("create_file", "markdown-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(FileRestrictionError)
expect(() =>
isToolAllowedForMode("create_file", "markdown-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(/\\.md\$/)
// Test CSS editor mode with non-CSS file
expect(() =>
isToolAllowedForMode("create_file", "css-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(FileRestrictionError)
expect(() =>
isToolAllowedForMode("create_file", "css-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(/\\.css\$/)
})
it("handles partial streaming cases (path only, no content/diff)", () => {
// Should allow path-only for matching files (no validation yet since content/diff not provided)
expect(
isToolAllowedForMode("create_file", "markdown-editor", customModes, undefined, {
path: "test.js",
}),
).toBe(true)
expect(
isToolAllowedForMode("edit_file", "markdown-editor", customModes, undefined, {
path: "test.js",
}),
).toBe(true)
// Should allow path-only for architect mode too
expect(
isToolAllowedForMode("create_file", "architect", [], undefined, {
path: "test.js",
}),
).toBe(true)
})
it("applies restrictions to both create_file and edit_file", () => {
// Test create_file
const writeResult = isToolAllowedForMode("create_file", "markdown-editor", customModes, undefined, {
path: "test.md",
content: "# Test",
})
expect(writeResult).toBe(true)
// Test edit_file
const diffResult = isToolAllowedForMode("edit_file", "markdown-editor", customModes, undefined, {
path: "test.md",
diff: "- old\n+ new",
})
expect(diffResult).toBe(true)
// Test both with non-matching file
expect(() =>
isToolAllowedForMode("create_file", "markdown-editor", customModes, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(FileRestrictionError)
expect(() =>
isToolAllowedForMode("edit_file", "markdown-editor", customModes, undefined, {
path: "test.js",
diff: "- old\n+ new",
}),
).toThrow(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 create_file with non-matching file
expect(() =>
isToolAllowedForMode("create_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(FileRestrictionError)
expect(() =>
isToolAllowedForMode("create_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(/Documentation files only/)
// Test edit_file with non-matching file
expect(() =>
isToolAllowedForMode("edit_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.js",
diff: "- old\n+ new",
}),
).toThrow(FileRestrictionError)
expect(() =>
isToolAllowedForMode("edit_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.js",
diff: "- old\n+ new",
}),
).toThrow(/Documentation files only/)
// Test that matching files are allowed
expect(
isToolAllowedForMode("create_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.md",
content: "# Test",
}),
).toBe(true)
expect(
isToolAllowedForMode("create_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.txt",
content: "Test content",
}),
).toBe(true)
// Test partial streaming cases
expect(
isToolAllowedForMode("create_file", "docs-editor", customModesWithDescription, undefined, {
path: "test.js",
}),
).toBe(true)
})
it("allows architect mode to edit markdown files only", () => {
// Should allow editing markdown files
expect(
isToolAllowedForMode("create_file", "architect", [], undefined, {
path: "test.md",
content: "# Test",
}),
).toBe(true)
// Should allow applying diffs to markdown files
expect(
isToolAllowedForMode("edit_file", "architect", [], undefined, {
path: "readme.md",
diff: "- old\n+ new",
}),
).toBe(true)
// Should reject non-markdown files
expect(() =>
isToolAllowedForMode("create_file", "architect", [], undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(FileRestrictionError)
expect(() =>
isToolAllowedForMode("create_file", "architect", [], undefined, {
path: "test.js",
content: "console.log('test')",
}),
).toThrow(/Markdown files only/)
// Should maintain read capabilities
expect(isToolAllowedForMode("read_file", "architect", [])).toBe(true)
expect(isToolAllowedForMode("browser_action", "architect", [])).toBe(true)
expect(isToolAllowedForMode("use_mcp_tool", "architect", [])).toBe(true)
})
})
it("handles non-existent modes", () => {
expect(isToolAllowedForMode("create_file", "non-existent", customModes)).toBe(false)
})
it("respects tool requirements", () => {
const toolRequirements = {
create_file: false,
}
expect(isToolAllowedForMode("create_file", "markdown-editor", customModes, toolRequirements)).toBe(false)
})
describe("experimental tools", () => {
it("disables tools when experiment is disabled", () => {
const experiments = {
search_and_replace: false,
insert_content: false,
}
expect(
isToolAllowedForMode(
"search_and_replace",
"test-exp-mode",
customModes,
undefined,
undefined,
experiments,
),
).toBe(false)
expect(
isToolAllowedForMode("insert_content", "test-exp-mode", customModes, undefined, undefined, experiments),
).toBe(false)
})
it("allows tools when experiment is enabled", () => {
const experiments = {
search_and_replace: true,
insert_content: true,
}
expect(
isToolAllowedForMode(
"search_and_replace",
"test-exp-mode",
customModes,
undefined,
undefined,
experiments,
),
).toBe(true)
expect(
isToolAllowedForMode("insert_content", "test-exp-mode", customModes, undefined, undefined, experiments),
).toBe(true)
})
it("allows non-experimental tools when experiments are disabled", () => {
const experiments = {
search_and_replace: false,
insert_content: false,
}
expect(
isToolAllowedForMode("read_file", "markdown-editor", customModes, undefined, undefined, experiments),
).toBe(true)
expect(
isToolAllowedForMode(
"create_file",
"markdown-editor",
customModes,
undefined,
{ path: "test.md" },
experiments,
),
).toBe(true)
})
})
})
describe("FileRestrictionError", () => {
it("formats error message with pattern when no description provided", () => {
const error = new FileRestrictionError("Markdown Editor", "\\.md$", undefined, "test.js")
expect(error.message).toBe(
"This mode (Markdown Editor) can only edit files matching pattern: \\.md$. Got: test.js",
)
expect(error.name).toBe("FileRestrictionError")
})
describe("debug mode", () => {
it("is configured correctly", () => {
const debugMode = modes.find((mode) => mode.slug === "debug")
expect(debugMode).toBeDefined()
expect(debugMode).toMatchObject({
slug: "debug",
name: "Debug",
roleDefinition:
"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
groups: ["read", "edit", "browser", "command", "mcp"],
})
expect(debugMode?.customInstructions).toContain(
"Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.",
)
})
})
describe("getFullModeDetails", () => {
beforeEach(() => {
jest.clearAllMocks()
;(addCustomInstructions as jest.Mock).mockResolvedValue("Combined instructions")
})
it("returns base mode when no overrides exist", async () => {
const result = await getFullModeDetails("debug")
expect(result).toMatchObject({
slug: "debug",
name: "Debug",
roleDefinition:
"You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
})
})
it("applies custom mode overrides", async () => {
const customModes = [
{
slug: "debug",
name: "Custom Debug",
roleDefinition: "Custom debug role",
groups: ["read"],
},
]
const result = await getFullModeDetails("debug", customModes)
expect(result).toMatchObject({
slug: "debug",
name: "Custom Debug",
roleDefinition: "Custom debug role",
groups: ["read"],
})
})
it("applies prompt component overrides", async () => {
const customModePrompts = {
debug: {
roleDefinition: "Overridden role",
customInstructions: "Overridden instructions",
},
}
const result = await getFullModeDetails("debug", undefined, customModePrompts)
expect(result.roleDefinition).toBe("Overridden role")
expect(result.customInstructions).toBe("Overridden instructions")
})
it("combines custom instructions when cwd provided", async () => {
const options = {
cwd: "/test/path",
globalCustomInstructions: "Global instructions",
preferredLanguage: "en",
}
await getFullModeDetails("debug", undefined, undefined, options)
expect(addCustomInstructions).toHaveBeenCalledWith(
expect.any(String),
"Global instructions",
"/test/path",
"debug",
{ preferredLanguage: "en" },
)
})
it("falls back to first mode for non-existent mode", async () => {
const result = await getFullModeDetails("non-existent")
expect(result).toMatchObject({
...modes[0],
customInstructions: "",
})
})
})
it("formats error message with description when provided", () => {
const error = new FileRestrictionError("Markdown Editor", "\\.md$", "Markdown files only", "test.js")
expect(error.message).toBe(
"This mode (Markdown Editor) can only edit files matching pattern: \\.md$ (Markdown files only). Got: test.js",
)
expect(error.name).toBe("FileRestrictionError")
})
})