From 6266e46ad20bcb2a8882389a860cdf7f485e2e5c Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Fri, 22 Aug 2025 12:34:16 -0600 Subject: [PATCH] =?UTF-8?q?Revert=20"feat:=20enable=20loading=20Roo=20mode?= =?UTF-8?q?s=20from=20multiple=20files=20in=20.roo/roo=5Fmodes=20=E2=80=A6?= =?UTF-8?q?"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit faece9e26fe8b8d29f24ae04cc145c9b9a192765. --- src/core/config/CustomModesManager.ts | 368 +++++++----------- .../__tests__/CustomModesManager.spec.ts | 200 +--------- 2 files changed, 162 insertions(+), 406 deletions(-) diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index 160dc8a286..a9a2e6a6b5 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -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 { + private async loadModesFromFile(filePath: string): Promise { 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 { - 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 { const slugs = new Set() 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") : [] - - // 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() - - // 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) - } + const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : [] - // 3. Global .roo/modes - for (const mode of globalRooModesDirModes) { - modesMap.set(mode.slug, mode) - } + // Create maps to store modes by source. + const projectModes = new Map() + const globalModes = new Map() - // 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")) - } + 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) + 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() - } + 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 { - // 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 { 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() + + 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 (!modeToDelete) { + if (!projectMode && !globalMode) { throw new Error(t("common:customModes.errors.modeNotFound")) } - 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() - - // 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 - } + // Determine which mode to use for rules folder path calculation + const modeToDelete = projectMode || globalMode - // 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 - } - } + await this.queueWrite(async () => { + // Delete from project first if it exists there + if (projectMode && roomodesPath) { + await this.updateModesInFile(roomodesPath, (modes) => modes.filter((m) => m.slug !== slug)) + } - // 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 diff --git a/src/core/config/__tests__/CustomModesManager.spec.ts b/src/core/config/__tests__/CustomModesManager.spec.ts index bc69951d36..b48ea7b65b 100644 --- a/src/core/config/__tests__/CustomModesManager.spec.ts +++ b/src/core/config/__tests__/CustomModesManager.spec.ts @@ -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 () => {