Revert "feat: enable loading Roo modes from multiple files in .roo/roo_modes directory" (#7332)

Revert "feat: enable loading Roo modes from multiple files in .roo/roo_modes …"

This reverts commit faece9e26f.
This commit is contained in:
Hannes Rudolph 2025-08-22 12:44:15 -06:00 committed by GitHub
parent b1f2d39faf
commit 7137c1902f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 163 additions and 407 deletions

View file

@ -10,14 +10,13 @@ import { type ModeConfig, type PromptComponent, customModesSettingsSchema, modeC
import { fileExistsAtPath } from "../../utils/fs"
import { getWorkspacePath } from "../../utils/path"
import { getGlobalRooDirectory, getRooDirectoriesForCwd } from "../../services/roo-config"
import { getGlobalRooDirectory } from "../../services/roo-config"
import { logger } from "../../utils/logging"
import { GlobalFileNames } from "../../shared/globalFileNames"
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
import { t } from "../../i18n"
const ROOMODES_FILENAME = ".roomodes"
const ROO_MODES_DIR = "modes"
// Type definitions for import/export functionality
interface RuleFile {
@ -180,7 +179,7 @@ export class CustomModesManager {
}
}
private async loadModesFromFile(filePath: string, source?: "global" | "project"): Promise<ModeConfig[]> {
private async loadModesFromFile(filePath: string): Promise<ModeConfig[]> {
try {
const content = await fs.readFile(filePath, "utf-8")
const settings = this.parseYamlSafely(content, filePath)
@ -207,19 +206,12 @@ export class CustomModesManager {
return []
}
// Determine source based on file path if not provided
if (!source) {
const isRoomodes = filePath.endsWith(ROOMODES_FILENAME)
const isInRooModesDir = filePath.includes(ROO_MODES_DIR)
source = isRoomodes || isInRooModesDir ? ("project" as const) : ("global" as const)
}
// Determine source based on file path
const isRoomodes = filePath.endsWith(ROOMODES_FILENAME)
const source = isRoomodes ? ("project" as const) : ("global" as const)
// Add source and sourceFile to each mode
return result.data.customModes.map((mode) => ({
...mode,
source,
sourceFile: filePath,
}))
// Add source to each mode
return result.data.customModes.map((mode) => ({ ...mode, source }))
} catch (error) {
// Only log if the error wasn't already handled in parseYamlSafely
if (!(error as any).alreadyHandled) {
@ -230,37 +222,6 @@ export class CustomModesManager {
}
}
/**
* Load modes from all YAML files in a directory
*/
private async loadModesFromDirectory(dirPath: string, source: "global" | "project"): Promise<ModeConfig[]> {
const modes: ModeConfig[] = []
try {
// Check if directory exists
const dirExists = await fileExistsAtPath(dirPath)
if (!dirExists) {
return modes
}
// Read all files in the directory
const entries = await fs.readdir(dirPath, { withFileTypes: true })
// Process each YAML file
for (const entry of entries) {
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
const filePath = path.join(dirPath, entry.name)
const fileModes = await this.loadModesFromFile(filePath, source)
modes.push(...fileModes)
}
}
} catch (error) {
console.error(`[CustomModesManager] Error loading modes from directory ${dirPath}:`, error)
}
return modes
}
private async mergeCustomModes(projectModes: ModeConfig[], globalModes: ModeConfig[]): Promise<ModeConfig[]> {
const slugs = new Set<string>()
const merged: ModeConfig[] = []
@ -304,55 +265,90 @@ export class CustomModesManager {
const settingsPath = await this.getCustomModesFilePath()
// Common handler for any mode file change
const handleModeFileChange = async () => {
// Watch settings file
const settingsWatcher = vscode.workspace.createFileSystemWatcher(settingsPath)
const handleSettingsChange = async () => {
try {
// Reload all modes using the same logic as getCustomModes
const modes = await this.getCustomModes()
// Ensure that the settings file exists (especially important for delete events)
await this.getCustomModesFilePath()
const content = await fs.readFile(settingsPath, "utf-8")
const errorMessage = t("common:customModes.errors.invalidFormat")
let config: any
try {
config = this.parseYamlSafely(content, settingsPath)
} catch (error) {
console.error(error)
vscode.window.showErrorMessage(errorMessage)
return
}
const result = customModesSettingsSchema.safeParse(config)
if (!result.success) {
vscode.window.showErrorMessage(errorMessage)
return
}
// Get modes from .roomodes if it exists (takes precedence)
const roomodesPath = await this.getWorkspaceRoomodes()
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
// Merge modes from both sources (.roomodes takes precedence)
const mergedModes = await this.mergeCustomModes(roomodesModes, result.data.customModes)
await this.context.globalState.update("customModes", mergedModes)
this.clearCache()
await this.onUpdate()
} catch (error) {
console.error(`[CustomModesManager] Error handling mode file change:`, error)
console.error(`[CustomModesManager] Error handling settings file change:`, error)
}
}
// Watch settings file
const settingsWatcher = vscode.workspace.createFileSystemWatcher(settingsPath)
this.disposables.push(settingsWatcher.onDidChange(handleModeFileChange))
this.disposables.push(settingsWatcher.onDidCreate(handleModeFileChange))
this.disposables.push(settingsWatcher.onDidDelete(handleModeFileChange))
this.disposables.push(settingsWatcher.onDidChange(handleSettingsChange))
this.disposables.push(settingsWatcher.onDidCreate(handleSettingsChange))
this.disposables.push(settingsWatcher.onDidDelete(handleSettingsChange))
this.disposables.push(settingsWatcher)
// Watch global .roo/modes directory
const globalRooModesDir = path.join(getGlobalRooDirectory(), ROO_MODES_DIR)
const globalRooModesPattern = path.join(globalRooModesDir, "*.{yaml,yml}")
const globalRooModesWatcher = vscode.workspace.createFileSystemWatcher(globalRooModesPattern)
this.disposables.push(globalRooModesWatcher.onDidChange(handleModeFileChange))
this.disposables.push(globalRooModesWatcher.onDidCreate(handleModeFileChange))
this.disposables.push(globalRooModesWatcher.onDidDelete(handleModeFileChange))
this.disposables.push(globalRooModesWatcher)
// Watch .roomodes file and project .roo/modes directory if workspace exists
// Watch .roomodes file - watch the path even if it doesn't exist yet
const workspaceFolders = vscode.workspace.workspaceFolders
if (workspaceFolders && workspaceFolders.length > 0) {
const workspaceRoot = getWorkspacePath()
// Watch .roomodes file
const roomodesPath = path.join(workspaceRoot, ROOMODES_FILENAME)
const roomodesWatcher = vscode.workspace.createFileSystemWatcher(roomodesPath)
this.disposables.push(roomodesWatcher.onDidChange(handleModeFileChange))
this.disposables.push(roomodesWatcher.onDidCreate(handleModeFileChange))
this.disposables.push(roomodesWatcher.onDidDelete(handleModeFileChange))
this.disposables.push(roomodesWatcher)
// Watch project .roo/modes directory
const projectRooModesDir = path.join(workspaceRoot, ".roo", ROO_MODES_DIR)
const projectRooModesPattern = path.join(projectRooModesDir, "*.{yaml,yml}")
const projectRooModesWatcher = vscode.workspace.createFileSystemWatcher(projectRooModesPattern)
this.disposables.push(projectRooModesWatcher.onDidChange(handleModeFileChange))
this.disposables.push(projectRooModesWatcher.onDidCreate(handleModeFileChange))
this.disposables.push(projectRooModesWatcher.onDidDelete(handleModeFileChange))
this.disposables.push(projectRooModesWatcher)
const handleRoomodesChange = async () => {
try {
const settingsModes = await this.loadModesFromFile(settingsPath)
const roomodesModes = await this.loadModesFromFile(roomodesPath)
// .roomodes takes precedence
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
await this.context.globalState.update("customModes", mergedModes)
this.clearCache()
await this.onUpdate()
} catch (error) {
console.error(`[CustomModesManager] Error handling .roomodes file change:`, error)
}
}
this.disposables.push(roomodesWatcher.onDidChange(handleRoomodesChange))
this.disposables.push(roomodesWatcher.onDidCreate(handleRoomodesChange))
this.disposables.push(
roomodesWatcher.onDidDelete(async () => {
// When .roomodes is deleted, refresh with only settings modes
try {
const settingsModes = await this.loadModesFromFile(settingsPath)
await this.context.globalState.update("customModes", settingsModes)
this.clearCache()
await this.onUpdate()
} catch (error) {
console.error(`[CustomModesManager] Error handling .roomodes file deletion:`, error)
}
}),
)
this.disposables.push(roomodesWatcher)
}
}
@ -366,59 +362,35 @@ export class CustomModesManager {
// Get modes from settings file.
const settingsPath = await this.getCustomModesFilePath()
const settingsModes = await this.loadModesFromFile(settingsPath, "global")
// Get modes from .roo/modes directories (both global and project)
const allRooModesDirModes: ModeConfig[] = []
// Load from global .roo/modes
const globalRooModesDir = path.join(getGlobalRooDirectory(), ROO_MODES_DIR)
const globalRooModesDirModes = await this.loadModesFromDirectory(globalRooModesDir, "global")
allRooModesDirModes.push(...globalRooModesDirModes)
// Load from project .roo/modes if workspace exists
const workspacePath = getWorkspacePath()
if (workspacePath) {
const projectRooModesDir = path.join(workspacePath, ".roo", ROO_MODES_DIR)
const projectRooModesDirModes = await this.loadModesFromDirectory(projectRooModesDir, "project")
allRooModesDirModes.push(...projectRooModesDirModes)
}
const settingsModes = await this.loadModesFromFile(settingsPath)
// Get modes from .roomodes if it exists.
const roomodesPath = await this.getWorkspaceRoomodes()
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath, "project") : []
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
// Create a map to store modes with proper precedence
// Precedence order (highest to lowest):
// 1. .roo/modes (project)
// 2. .roomodes (project)
// 3. .roo/modes (global)
// 4. settings file (global)
const modesMap = new Map<string, ModeConfig>()
// Create maps to store modes by source.
const projectModes = new Map<string, ModeConfig>()
const globalModes = new Map<string, ModeConfig>()
// Add in reverse precedence order (lowest to highest) so higher precedence overwrites
// 4. Global settings file
for (const mode of settingsModes) {
modesMap.set(mode.slug, mode)
}
// 3. Global .roo/modes
for (const mode of globalRooModesDirModes) {
modesMap.set(mode.slug, mode)
}
// 2. Project .roomodes
// Add project modes (they take precedence).
for (const mode of roomodesModes) {
modesMap.set(mode.slug, mode)
projectModes.set(mode.slug, { ...mode, source: "project" as const })
}
// 1. Project .roo/modes (highest precedence)
for (const mode of allRooModesDirModes.filter((m) => m.source === "project")) {
modesMap.set(mode.slug, mode)
// Add global modes.
for (const mode of settingsModes) {
if (!projectModes.has(mode.slug)) {
globalModes.set(mode.slug, { ...mode, source: "global" as const })
}
}
// Convert map to array
const mergedModes = Array.from(modesMap.values())
// Combine modes in the correct order: project modes first, then global modes.
const mergedModes = [
...roomodesModes.map((mode) => ({ ...mode, source: "project" as const })),
...settingsModes
.filter((mode) => !projectModes.has(mode.slug))
.map((mode) => ({ ...mode, source: "global" as const })),
]
await this.context.globalState.update("customModes", mergedModes)
@ -442,50 +414,34 @@ export class CustomModesManager {
return
}
// Check if we're updating an existing mode and preserve its source file
const existingModes = await this.getCustomModes()
const existingMode = existingModes.find((m) => m.slug === slug)
const isProjectMode = config.source === "project"
let targetPath: string
// If mode exists and has a sourceFile, update it in the same file
if (existingMode && (existingMode as any).sourceFile) {
targetPath = (existingMode as any).sourceFile
logger.info(`Updating mode in original file: ${targetPath}`, { slug })
} else {
// For new modes or modes without sourceFile, determine target based on source
const isProjectMode = config.source === "project"
if (isProjectMode) {
const workspaceFolders = vscode.workspace.workspaceFolders
if (isProjectMode) {
const workspaceFolders = vscode.workspace.workspaceFolders
if (!workspaceFolders || workspaceFolders.length === 0) {
logger.error("Failed to update project mode: No workspace folder found", { slug })
throw new Error(t("common:customModes.errors.noWorkspaceForProject"))
}
const workspaceRoot = getWorkspacePath()
targetPath = path.join(workspaceRoot, ROOMODES_FILENAME)
const exists = await fileExistsAtPath(targetPath)
logger.info(`${exists ? "Updating" : "Creating"} project mode in ${ROOMODES_FILENAME}`, {
slug,
workspace: workspaceRoot,
})
} else {
targetPath = await this.getCustomModesFilePath()
if (!workspaceFolders || workspaceFolders.length === 0) {
logger.error("Failed to update project mode: No workspace folder found", { slug })
throw new Error(t("common:customModes.errors.noWorkspaceForProject"))
}
const workspaceRoot = getWorkspacePath()
targetPath = path.join(workspaceRoot, ROOMODES_FILENAME)
const exists = await fileExistsAtPath(targetPath)
logger.info(`${exists ? "Updating" : "Creating"} project mode in ${ROOMODES_FILENAME}`, {
slug,
workspace: workspaceRoot,
})
} else {
targetPath = await this.getCustomModesFilePath()
}
await this.queueWrite(async () => {
// Determine source based on target path
const isProjectFile =
targetPath.includes(ROOMODES_FILENAME) ||
(targetPath.includes(ROO_MODES_DIR) && !targetPath.includes(getGlobalRooDirectory()))
// Ensure source is set correctly based on target file.
const modeWithSource = {
...config,
source: isProjectFile ? ("project" as const) : ("global" as const),
source: isProjectMode ? ("project" as const) : ("global" as const),
}
await this.updateModesInFile(targetPath, (modes) => {
@ -536,81 +492,54 @@ export class CustomModesManager {
}
private async refreshMergedState(): Promise<void> {
// Use the same logic as getCustomModes to ensure consistency
const modes = await this.getCustomModes()
const settingsPath = await this.getCustomModesFilePath()
const roomodesPath = await this.getWorkspaceRoomodes()
const settingsModes = await this.loadModesFromFile(settingsPath)
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
await this.context.globalState.update("customModes", mergedModes)
this.clearCache()
await this.onUpdate()
}
public async deleteCustomMode(slug: string, fromMarketplace = false): Promise<void> {
try {
// Get all modes to find where this mode is stored
const allModes = await this.getCustomModes()
const modeToDelete = allModes.find((m) => m.slug === slug)
const settingsPath = await this.getCustomModesFilePath()
const roomodesPath = await this.getWorkspaceRoomodes()
if (!modeToDelete) {
const settingsModes = await this.loadModesFromFile(settingsPath)
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
// Find the mode in either file
const projectMode = roomodesModes.find((m) => m.slug === slug)
const globalMode = settingsModes.find((m) => m.slug === slug)
if (!projectMode && !globalMode) {
throw new Error(t("common:customModes.errors.modeNotFound"))
}
// Determine which mode to use for rules folder path calculation
const modeToDelete = projectMode || globalMode
await this.queueWrite(async () => {
// Delete from the source file if it has one
if ((modeToDelete as any).sourceFile) {
const sourceFile = (modeToDelete as any).sourceFile
await this.updateModesInFile(sourceFile, (modes) => modes.filter((m) => m.slug !== slug))
} else {
// Fallback to checking all possible locations
const settingsPath = await this.getCustomModesFilePath()
const roomodesPath = await this.getWorkspaceRoomodes()
// Delete from project first if it exists there
if (projectMode && roomodesPath) {
await this.updateModesInFile(roomodesPath, (modes) => modes.filter((m) => m.slug !== slug))
}
// Try to delete from settings file
try {
const settingsModes = await this.loadModesFromFile(settingsPath)
if (settingsModes.find((m) => m.slug === slug)) {
await this.updateModesInFile(settingsPath, (modes) => modes.filter((m) => m.slug !== slug))
}
} catch (error) {
// Ignore if file doesn't exist
}
// Try to delete from .roomodes
if (roomodesPath) {
try {
const roomodesModes = await this.loadModesFromFile(roomodesPath)
if (roomodesModes.find((m) => m.slug === slug)) {
await this.updateModesInFile(roomodesPath, (modes) =>
modes.filter((m) => m.slug !== slug),
)
}
} catch (error) {
// Ignore if file doesn't exist
}
}
// Check and delete from .roo/modes directories
const rooDirectories = getRooDirectoriesForCwd(getWorkspacePath() || process.cwd())
for (const rooDir of rooDirectories) {
const rooModesDir = path.join(rooDir, ROO_MODES_DIR)
try {
const entries = await fs.readdir(rooModesDir, { withFileTypes: true })
for (const entry of entries) {
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
const filePath = path.join(rooModesDir, entry.name)
const fileModes = await this.loadModesFromFile(filePath)
if (fileModes.find((m) => m.slug === slug)) {
await this.updateModesInFile(filePath, (modes) =>
modes.filter((m) => m.slug !== slug),
)
}
}
}
} catch (error) {
// Directory might not exist
}
}
// Delete from global settings if it exists there
if (globalMode) {
await this.updateModesInFile(settingsPath, (modes) => modes.filter((m) => m.slug !== slug))
}
// Delete associated rules folder
await this.deleteRulesFolder(slug, modeToDelete, fromMarketplace)
if (modeToDelete) {
await this.deleteRulesFolder(slug, modeToDelete, fromMarketplace)
}
// Clear cache when modes are deleted
this.clearCache()
@ -878,11 +807,6 @@ export class CustomModesManager {
source: "project" as const,
}
// Normalize sourceFile path to use forward slashes if it exists
if ((exportMode as any).sourceFile) {
;(exportMode as any).sourceFile = (exportMode as any).sourceFile.replace(/\\/g, "/")
}
// Merge custom prompts if provided
if (customPrompts) {
if (customPrompts.roleDefinition) exportMode.roleDefinition = customPrompts.roleDefinition

View file

@ -114,176 +114,6 @@ describe("CustomModesManager", () => {
expect(modes).toHaveLength(2)
})
it("should load modes from .roo/modes directory", async () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
const rooModesMode1 = { slug: "mode2", name: "Mode 2", roleDefinition: "Role 2", groups: ["read"] }
const rooModesMode2 = { slug: "mode3", name: "Mode 3", roleDefinition: "Role 3", groups: ["edit"] }
;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => {
// Return true for settings path and modes directories
return path === mockSettingsPath || path.includes("modes") || path === mockRoomodes
})
;(fs.readdir as Mock).mockImplementation(async (path: string) => {
if (path.includes("modes")) {
return [
{ name: "mode1.yaml", isFile: () => true },
{ name: "mode2.yml", isFile: () => true },
{ name: "readme.txt", isFile: () => true }, // Should be ignored
]
}
return []
})
;(fs.readFile as Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return yaml.stringify({ customModes: settingsModes })
}
if (path.includes("mode1.yaml")) {
return yaml.stringify({ customModes: [rooModesMode1] })
}
if (path.includes("mode2.yml")) {
return yaml.stringify({ customModes: [rooModesMode2] })
}
throw new Error("File not found")
})
const modes = await manager.getCustomModes()
// Should have all 3 modes
expect(modes).toHaveLength(3)
expect(modes.map((m) => m.slug)).toContain("mode1")
expect(modes.map((m) => m.slug)).toContain("mode2")
expect(modes.map((m) => m.slug)).toContain("mode3")
})
it("should respect precedence: project .roo/modes > .roomodes > global .roo/modes > settings", async () => {
const settingsMode = { slug: "test", name: "Settings", roleDefinition: "Settings Role", groups: ["read"] }
const globalRooMode = {
slug: "test",
name: "Global Roo",
roleDefinition: "Global Roo Role",
groups: ["read"],
}
const roomodesMode = { slug: "test", name: "Roomodes", roleDefinition: "Roomodes Role", groups: ["read"] }
const projectRooMode = {
slug: "test",
name: "Project Roo",
roleDefinition: "Project Roo Role",
groups: ["read"],
}
;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => {
return true // All paths exist
})
;(fs.readdir as Mock).mockImplementation(async (path: string) => {
if (path.includes("modes")) {
return [{ name: "test.yaml", isFile: () => true }]
}
return []
})
;(fs.readFile as Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return yaml.stringify({ customModes: [settingsMode] })
}
if (path === mockRoomodes) {
return yaml.stringify({ customModes: [roomodesMode] })
}
// Global .roo/modes
if (path.includes("modes") && !path.includes(mockWorkspacePath)) {
return yaml.stringify({ customModes: [globalRooMode] })
}
// Project .roo/modes
if (path.includes("modes") && path.includes(mockWorkspacePath)) {
return yaml.stringify({ customModes: [projectRooMode] })
}
throw new Error("File not found")
})
const modes = await manager.getCustomModes()
// Should have only one mode with the slug "test"
expect(modes).toHaveLength(1)
// Project .roo/modes should take precedence
expect(modes[0].name).toBe("Project Roo")
expect(modes[0].roleDefinition).toBe("Project Roo Role")
})
it("should handle empty .roo/modes directory", async () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => {
return path === mockSettingsPath || path.includes("modes")
})
;(fs.readdir as Mock).mockImplementation(async () => {
return [] // Empty directory
})
;(fs.readFile as Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
const modes = await manager.getCustomModes()
// Should only have the settings mode
expect(modes).toHaveLength(1)
expect(modes[0].slug).toBe("mode1")
})
it("should handle non-existent .roo/modes directory", async () => {
const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }]
;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => {
// modes directories don't exist
if (path.includes("modes")) {
return false
}
return path === mockSettingsPath
})
;(fs.readFile as Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return yaml.stringify({ customModes: settingsModes })
}
throw new Error("File not found")
})
const modes = await manager.getCustomModes()
// Should only have the settings mode
expect(modes).toHaveLength(1)
expect(modes[0].slug).toBe("mode1")
})
it("should preserve sourceFile property for modes loaded from .roo/modes", async () => {
const rooModesMode = { slug: "test", name: "Test Mode", roleDefinition: "Test Role", groups: ["read"] }
;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => {
return path === mockSettingsPath || path.includes("modes")
})
;(fs.readdir as Mock).mockImplementation(async (path: string) => {
if (path.includes("modes")) {
return [{ name: "test.yaml", isFile: () => true }]
}
return []
})
;(fs.readFile as Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return yaml.stringify({ customModes: [] })
}
if (path.includes("test.yaml")) {
return yaml.stringify({ customModes: [rooModesMode] })
}
throw new Error("File not found")
})
const modes = await manager.getCustomModes()
expect(modes).toHaveLength(1)
expect(modes[0].slug).toBe("test")
// Check that sourceFile is preserved
expect((modes[0] as any).sourceFile).toContain("test.yaml")
})
it("should merge modes with .roomodes taking precedence", async () => {
const settingsModes = [
{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] },
@ -309,10 +139,7 @@ describe("CustomModesManager", () => {
// Should contain 3 modes (mode1 from settings, mode2 and mode3 from roomodes)
expect(modes).toHaveLength(3)
// The order is now: settings modes first, then roomodes overrides
expect(modes.map((m) => m.slug)).toContain("mode1")
expect(modes.map((m) => m.slug)).toContain("mode2")
expect(modes.map((m) => m.slug)).toContain("mode3")
expect(modes.map((m) => m.slug)).toEqual(["mode2", "mode3", "mode1"])
// mode2 should come from .roomodes since it takes precedence
const mode2 = modes.find((m) => m.slug === "mode2")
@ -628,7 +455,6 @@ describe("CustomModesManager", () => {
roleDefinition: "Role 1",
groups: ["read"],
source: "project",
sourceFile: mockRoomodes, // Add sourceFile to simulate existing mode
},
]
@ -639,9 +465,6 @@ describe("CustomModesManager", () => {
let settingsContent = { customModes: existingModes }
let roomodesContent = { customModes: roomodesModes }
// Mock getCustomModes to return the roomodes mode with sourceFile
const getCustomModesSpy = vi.spyOn(manager, "getCustomModes")
getCustomModesSpy.mockResolvedValueOnce(roomodesModes as any)
;(fs.readFile as Mock).mockImplementation(async (path: string) => {
if (path === mockRoomodes) {
return yaml.stringify(roomodesContent)
@ -663,8 +486,8 @@ describe("CustomModesManager", () => {
await manager.updateCustomMode("mode1", newMode)
// Since mode1 exists in .roomodes with sourceFile, it should update there
expect(fs.writeFile).toHaveBeenCalledWith(mockRoomodes, expect.any(String), "utf-8")
// Should write to settings file
expect(fs.writeFile).toHaveBeenCalledWith(mockSettingsPath, expect.any(String), "utf-8")
// Verify the content of the write
const writeCall = (fs.writeFile as Mock).mock.calls[0]
@ -674,15 +497,24 @@ describe("CustomModesManager", () => {
slug: "mode1",
name: "Updated Mode 1",
roleDefinition: "Updated Role 1",
source: "project", // Should be project since it's in .roomodes
source: "global",
}),
)
// Should update global state with merged modes where .roomodes takes precedence
expect(mockContext.globalState.update).toHaveBeenCalledWith(
"customModes",
expect.arrayContaining([
expect.objectContaining({
slug: "mode1",
name: "Roomodes Mode 1", // .roomodes version should take precedence
source: "project",
}),
]),
)
// Should trigger onUpdate
expect(mockOnUpdate).toHaveBeenCalled()
// Restore the spy
getCustomModesSpy.mockRestore()
})
it("creates .roomodes file when adding project-specific mode", async () => {