Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,33 @@ import type { Mock } from "vitest"

// Mock dependencies - must come before imports
vi.mock("../../../api/providers/fetchers/modelCache")
vi.mock("../../../utils/fs")
vi.mock("../../../utils/path")
vi.mock("../../../i18n", () => ({
t: vi.fn((key: string) => key), // Return the key as-is for testing
}))
vi.mock("vscode", () => ({
window: {
showInformationMessage: vi.fn(),
showErrorMessage: vi.fn(),
},
workspace: {
workspaceFolders: [],
},
}))

import { webviewMessageHandler } from "../webviewMessageHandler"
import type { ClineProvider } from "../ClineProvider"
import { getModels } from "../../../api/providers/fetchers/modelCache"
import type { ModelRecord } from "../../../shared/api"
import { fileExistsAtPath } from "../../../utils/fs"
import { getWorkspacePath } from "../../../utils/path"
import * as vscode from "vscode"

const mockFileExistsAtPath = fileExistsAtPath as Mock<typeof fileExistsAtPath>
const mockGetWorkspacePath = getWorkspacePath as Mock<typeof getWorkspacePath>
const mockShowInformationMessage = vscode.window.showInformationMessage as Mock
const mockShowErrorMessage = vscode.window.showErrorMessage as Mock

const mockGetModels = getModels as Mock<typeof getModels>

Expand Down Expand Up @@ -275,6 +297,199 @@ describe("webviewMessageHandler - requestRouterModels", () => {
})
})

describe("webviewMessageHandler - deleteCustomMode", () => {
const mockCustomModesManager = {
getCustomModes: vi.fn(),
deleteCustomMode: vi.fn(),
}

const mockContextProxy = {
setValue: vi.fn(),
}

const mockProvider = {
...mockClineProvider,
customModesManager: mockCustomModesManager,
contextProxy: mockContextProxy,
postStateToWebview: vi.fn(),
} as unknown as ClineProvider

beforeEach(() => {
vi.clearAllMocks()
mockGetWorkspacePath.mockReturnValue("/test/workspace")
})

it("shows enhanced warning when rules folder exists", async () => {
const testMode = {
slug: "test-mode",
name: "Test Mode",
source: "project" as const,
}

mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
mockFileExistsAtPath.mockResolvedValue(true)
mockShowInformationMessage.mockResolvedValue("common:answers.yes")

await webviewMessageHandler(mockProvider, {
type: "deleteCustomMode",
slug: "test-mode",
})

// Verify rules folder check was performed
expect(mockFileExistsAtPath).toHaveBeenCalledWith("/test/workspace/.roo/rules-test-mode")

// Verify enhanced warning message was shown
expect(mockShowInformationMessage).toHaveBeenCalledWith(
"common:confirmation.delete_custom_mode_with_rules",
{ modal: true },
"common:answers.yes",
)

// Verify deletion proceeded
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-mode")
})

it("shows standard warning when rules folder does not exist", async () => {
const testMode = {
slug: "test-mode",
name: "Test Mode",
source: "global" as const,
}

mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
mockFileExistsAtPath.mockResolvedValue(false)
mockShowInformationMessage.mockResolvedValue("common:answers.yes")

await webviewMessageHandler(mockProvider, {
type: "deleteCustomMode",
slug: "test-mode",
})

// Verify standard warning message was shown
expect(mockShowInformationMessage).toHaveBeenCalledWith(
"common:confirmation.delete_custom_mode",
{ modal: true },
"common:answers.yes",
)

// Verify deletion proceeded
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-mode")
})

it("shows standard warning when workspace path is not available", async () => {
const testMode = {
slug: "test-mode",
name: "Test Mode",
source: "project" as const,
}

mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
mockGetWorkspacePath.mockReturnValue("")
mockShowInformationMessage.mockResolvedValue("common:answers.yes")

await webviewMessageHandler(mockProvider, {
type: "deleteCustomMode",
slug: "test-mode",
})

// Verify file check was not performed
expect(mockFileExistsAtPath).not.toHaveBeenCalled()

// Verify standard warning message was shown
expect(mockShowInformationMessage).toHaveBeenCalledWith(
"common:confirmation.delete_custom_mode",
{ modal: true },
"common:answers.yes",
)
})

it("shows standard warning when file check fails", async () => {
const testMode = {
slug: "test-mode",
name: "Test Mode",
source: "project" as const,
}

mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
mockFileExistsAtPath.mockRejectedValue(new Error("File system error"))
mockShowInformationMessage.mockResolvedValue("common:answers.yes")

await webviewMessageHandler(mockProvider, {
type: "deleteCustomMode",
slug: "test-mode",
})

// Verify standard warning message was shown (fallback)
expect(mockShowInformationMessage).toHaveBeenCalledWith(
"common:confirmation.delete_custom_mode",
{ modal: true },
"common:answers.yes",
)

// Verify deletion still proceeded
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-mode")
})

it("does not delete when user cancels", async () => {
const testMode = {
slug: "test-mode",
name: "Test Mode",
source: "project" as const,
}

mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
mockFileExistsAtPath.mockResolvedValue(true)
mockShowInformationMessage.mockResolvedValue(undefined) // User cancelled

await webviewMessageHandler(mockProvider, {
type: "deleteCustomMode",
slug: "test-mode",
})

// Verify deletion was not performed
expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled()
})

it("shows error when mode is not found", async () => {
mockCustomModesManager.getCustomModes.mockResolvedValue([])

await webviewMessageHandler(mockProvider, {
type: "deleteCustomMode",
slug: "nonexistent-mode",
})

// Verify error message was shown
expect(mockShowErrorMessage).toHaveBeenCalledWith("common:customModes.errors.modeNotFound")

// Verify deletion was not attempted
expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled()
})

it("correctly identifies GLOBAL mode source", async () => {
const testMode = {
slug: "global-mode",
name: "Global Mode",
source: "global" as const,
}

mockCustomModesManager.getCustomModes.mockResolvedValue([testMode])
mockFileExistsAtPath.mockResolvedValue(true)
mockShowInformationMessage.mockResolvedValue("common:answers.yes")

await webviewMessageHandler(mockProvider, {
type: "deleteCustomMode",
slug: "global-mode",
})

// Verify enhanced warning message shows GLOBAL
expect(mockShowInformationMessage).toHaveBeenCalledWith(
"common:confirmation.delete_custom_mode_with_rules",
{ modal: true },
"common:answers.yes",
)
})
})

it("prefers config values over message values for LiteLLM", async () => {
const mockModels: ModelRecord = {}
mockGetModels.mockResolvedValue(mockModels)
Expand Down
38 changes: 37 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1485,8 +1485,44 @@ export const webviewMessageHandler = async (
break
case "deleteCustomMode":
if (message.slug) {
// Get the mode details to determine source and name
const customModes = await provider.customModesManager.getCustomModes()
const modeToDelete = customModes.find((mode) => mode.slug === message.slug)

if (!modeToDelete) {
vscode.window.showErrorMessage(t("common:customModes.errors.modeNotFound"))
break
}

// Check if rules folder exists
const workspacePath = getWorkspacePath()
let hasRulesFolder = false

if (workspacePath) {
const rulesFolderPath = path.join(workspacePath, ".roo", `rules-${message.slug}`)
try {
hasRulesFolder = await fileExistsAtPath(rulesFolderPath)
} catch (error) {
// If we can't check the folder, fall back to standard confirmation
hasRulesFolder = false
}
}

// Show appropriate confirmation dialog
let confirmationMessage: string
if (hasRulesFolder) {
const sourceText = modeToDelete.source === "project" ? "PROJECT" : "GLOBAL"
confirmationMessage = t("common:confirmation.delete_custom_mode_with_rules", {
source: sourceText,
modeName: modeToDelete.name,
slug: message.slug,
})
} else {
confirmationMessage = t("common:confirmation.delete_custom_mode")
}

const answer = await vscode.window.showInformationMessage(
t("common:confirmation.delete_custom_mode"),
confirmationMessage,
{ modal: true },
t("common:answers.yes"),
)
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.",
"delete_config_profile": "Are you sure you want to delete this configuration profile?",
"delete_custom_mode": "Are you sure you want to delete this custom mode?",
"delete_custom_mode_with_rules": "Are you sure you want to delete this {{source}} mode '{{modeName}}'?\n\nNote: The associated rules folder at .roo/rules-{{slug}} will remain and must be deleted manually.",
"delete_message": "What would you like to delete?",
"just_this_message": "Just this message",
"this_and_subsequent": "This and all subsequent messages"
Expand Down
Loading