mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
253 lines
9.1 KiB
TypeScript
253 lines
9.1 KiB
TypeScript
import * as vscode from "vscode"
|
|
import * as path from "path"
|
|
import * as fs from "fs/promises"
|
|
import { fileExistsAtPath } from "../utils/fs"
|
|
import { GlobalFileNames } from "../shared/globalFileNames"
|
|
import { migrateSettings } from "../utils/migrateSettings"
|
|
|
|
// Mock dependencies
|
|
vitest.mock("vscode")
|
|
vitest.mock("fs/promises", () => ({
|
|
mkdir: vitest.fn().mockResolvedValue(undefined),
|
|
readFile: vitest.fn(),
|
|
writeFile: vitest.fn().mockResolvedValue(undefined),
|
|
rename: vitest.fn().mockResolvedValue(undefined),
|
|
unlink: vitest.fn().mockResolvedValue(undefined),
|
|
}))
|
|
vitest.mock("fs")
|
|
vitest.mock("../utils/fs")
|
|
|
|
describe("Settings Migration", () => {
|
|
let mockContext: vscode.ExtensionContext
|
|
let mockOutputChannel: vscode.OutputChannel
|
|
const mockStoragePath = "/mock/storage"
|
|
const mockSettingsDir = path.join(mockStoragePath, "settings")
|
|
|
|
// Legacy file names
|
|
const legacyCustomModesJson = path.join(mockSettingsDir, "custom_modes.json")
|
|
const legacyClineCustomModesPath = path.join(mockSettingsDir, "cline_custom_modes.json")
|
|
const legacyMcpSettingsPath = path.join(mockSettingsDir, "cline_mcp_settings.json")
|
|
|
|
// New file names
|
|
const newCustomModesYaml = path.join(mockSettingsDir, GlobalFileNames.customModes)
|
|
const newMcpSettingsPath = path.join(mockSettingsDir, GlobalFileNames.mcpSettings)
|
|
|
|
beforeEach(() => {
|
|
vitest.clearAllMocks()
|
|
|
|
// Mock output channel
|
|
mockOutputChannel = {
|
|
appendLine: vitest.fn(),
|
|
append: vitest.fn(),
|
|
clear: vitest.fn(),
|
|
show: vitest.fn(),
|
|
hide: vitest.fn(),
|
|
dispose: vitest.fn(),
|
|
} as unknown as vscode.OutputChannel
|
|
|
|
// Mock extension context
|
|
mockContext = {
|
|
globalStorageUri: { fsPath: mockStoragePath },
|
|
} as unknown as vscode.ExtensionContext
|
|
|
|
// Set global outputChannel for all tests
|
|
;(global as any).outputChannel = mockOutputChannel
|
|
})
|
|
|
|
it("should migrate custom modes file if old file exists and new file doesn't", async () => {
|
|
// Clear all previous mocks to ensure clean test environment
|
|
vitest.clearAllMocks()
|
|
|
|
// Setup mock for rename function
|
|
const mockRename = vitest.mocked(fs.rename).mockResolvedValue(undefined)
|
|
|
|
// Mock file existence checks - only return true for paths we want to exist
|
|
vitest.mocked(fileExistsAtPath).mockImplementation(async (path: string) => {
|
|
if (path === mockSettingsDir) return true
|
|
if (path === legacyClineCustomModesPath) return true
|
|
return false // All other paths don't exist, including destination files
|
|
})
|
|
|
|
// Run the migration
|
|
await migrateSettings(mockContext, mockOutputChannel)
|
|
|
|
// Verify expected rename call - cline_custom_modes.json should be renamed to custom_modes.json
|
|
expect(mockRename).toHaveBeenCalledWith(legacyClineCustomModesPath, legacyCustomModesJson)
|
|
})
|
|
|
|
it("should migrate MCP settings file if old file exists and new file doesn't", async () => {
|
|
// Clear all previous mocks to ensure clean test environment
|
|
vitest.clearAllMocks()
|
|
|
|
// Setup mock for rename function
|
|
const mockRename = vitest.mocked(fs.rename).mockResolvedValue(undefined)
|
|
|
|
// Ensure the other files don't interfere with this test
|
|
vitest.mocked(fileExistsAtPath).mockImplementation(async (path: string) => {
|
|
if (path === mockSettingsDir) return true
|
|
if (path === legacyMcpSettingsPath) return true
|
|
if (path === legacyClineCustomModesPath) return false // Ensure this file doesn't exist
|
|
if (path === legacyCustomModesJson) return false // Ensure this file doesn't exist
|
|
return false // All other paths don't exist, including destination files
|
|
})
|
|
|
|
// Run the migration
|
|
await migrateSettings(mockContext, mockOutputChannel)
|
|
|
|
// Verify expected rename call
|
|
expect(mockRename).toHaveBeenCalledWith(legacyMcpSettingsPath, newMcpSettingsPath)
|
|
})
|
|
|
|
it("should not migrate if new file already exists", async () => {
|
|
// Clear all previous mocks to ensure clean test environment
|
|
vitest.clearAllMocks()
|
|
|
|
// Setup mock for rename function
|
|
const mockRename = vitest.mocked(fs.rename).mockResolvedValue(undefined)
|
|
|
|
// Mock file existence checks - both source and destination exist
|
|
vitest.mocked(fileExistsAtPath).mockImplementation(async (path: string) => {
|
|
if (path === mockSettingsDir) return true
|
|
if (path === legacyClineCustomModesPath) return true
|
|
if (path === legacyCustomModesJson) return true // Destination already exists
|
|
if (path === legacyMcpSettingsPath) return true
|
|
if (path === newMcpSettingsPath) return true
|
|
return false
|
|
})
|
|
|
|
await migrateSettings(mockContext, mockOutputChannel)
|
|
|
|
// Verify rename was not called since destination files exist
|
|
expect(mockRename).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("should handle errors gracefully", async () => {
|
|
// Clear mocks
|
|
vitest.clearAllMocks()
|
|
|
|
// Mock file existence to throw error
|
|
vitest.mocked(fileExistsAtPath).mockRejectedValue(new Error("Test error"))
|
|
|
|
await migrateSettings(mockContext, mockOutputChannel)
|
|
|
|
// Verify error was logged
|
|
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
|
expect.stringContaining("Error migrating settings files"),
|
|
)
|
|
})
|
|
|
|
it("should convert custom_modes.json to YAML format", async () => {
|
|
// Clear all previous mocks to ensure clean test environment
|
|
vitest.clearAllMocks()
|
|
|
|
const testJsonContent = JSON.stringify({ customModes: [{ slug: "test-mode", name: "Test Mode" }] })
|
|
|
|
// Setup mock functions
|
|
const mockWrite = vitest.mocked(fs.writeFile).mockResolvedValue(undefined)
|
|
const mockUnlink = vitest.mocked(fs.unlink).mockResolvedValue(undefined)
|
|
|
|
// Mock file read to return JSON content
|
|
vitest.mocked(fs.readFile).mockImplementation(async (path: any) => {
|
|
if (path === legacyCustomModesJson) {
|
|
return testJsonContent
|
|
}
|
|
throw new Error("File not found: " + path)
|
|
})
|
|
|
|
// Isolate this test by making sure only the specific JSON file exists
|
|
vitest.mocked(fileExistsAtPath).mockImplementation(async (path: string) => {
|
|
if (path === mockSettingsDir) return true
|
|
if (path === legacyCustomModesJson) return true
|
|
if (path === legacyClineCustomModesPath) return false
|
|
if (path === legacyMcpSettingsPath) return false
|
|
return false
|
|
})
|
|
|
|
await migrateSettings(mockContext, mockOutputChannel)
|
|
|
|
// Verify file operations
|
|
expect(mockWrite).toHaveBeenCalledWith(newCustomModesYaml, expect.any(String), "utf-8")
|
|
// We don't delete the original JSON file to allow for rollback
|
|
expect(mockUnlink).not.toHaveBeenCalled()
|
|
|
|
// Verify log message mentions preservation of original file
|
|
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
|
expect.stringContaining("original JSON file preserved for rollback purposes"),
|
|
)
|
|
})
|
|
|
|
it("should handle corrupt JSON gracefully", async () => {
|
|
// Clear all previous mocks to ensure clean test environment
|
|
vitest.clearAllMocks()
|
|
|
|
// Setup mock functions
|
|
const mockWrite = vitest.mocked(fs.writeFile).mockResolvedValue(undefined)
|
|
const mockUnlink = vitest.mocked(fs.unlink).mockResolvedValue(undefined)
|
|
|
|
// Mock file read to return corrupt JSON
|
|
vitest.mocked(fs.readFile).mockImplementation(async (path: any) => {
|
|
if (path === legacyCustomModesJson) {
|
|
return "{ invalid json content" // This will cause an error when parsed
|
|
}
|
|
throw new Error("File not found: " + path)
|
|
})
|
|
|
|
// Isolate this test
|
|
vitest.mocked(fileExistsAtPath).mockImplementation(async (path: string) => {
|
|
if (path === mockSettingsDir) return true
|
|
if (path === legacyCustomModesJson) return true
|
|
if (path === legacyClineCustomModesPath) return false
|
|
if (path === legacyMcpSettingsPath) return false
|
|
return false
|
|
})
|
|
|
|
await migrateSettings(mockContext, mockOutputChannel)
|
|
|
|
// Verify error was logged
|
|
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
|
expect.stringContaining("Error parsing custom_modes.json"),
|
|
)
|
|
|
|
// Verify no write/unlink operations were performed
|
|
expect(mockWrite).not.toHaveBeenCalled()
|
|
expect(mockUnlink).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("should skip migration when YAML file already exists", async () => {
|
|
// Clear all previous mocks to ensure clean test environment
|
|
vitest.clearAllMocks()
|
|
|
|
// Setup mock functions
|
|
const mockWrite = vitest.mocked(fs.writeFile).mockResolvedValue(undefined)
|
|
const mockUnlink = vitest.mocked(fs.unlink).mockResolvedValue(undefined)
|
|
|
|
// Mock file read
|
|
vitest.mocked(fs.readFile).mockImplementation(async (path: any) => {
|
|
if (path === legacyCustomModesJson) {
|
|
return JSON.stringify({ customModes: [] })
|
|
}
|
|
throw new Error("File not found: " + path)
|
|
})
|
|
|
|
// Mock file existence checks - both source and yaml destination exist
|
|
vitest.mocked(fileExistsAtPath).mockImplementation(async (path: string) => {
|
|
if (path === mockSettingsDir) return true
|
|
if (path === legacyCustomModesJson) return true
|
|
if (path === newCustomModesYaml) return true // YAML already exists
|
|
if (path === legacyClineCustomModesPath) return false
|
|
if (path === legacyMcpSettingsPath) return false
|
|
return false
|
|
})
|
|
|
|
await migrateSettings(mockContext, mockOutputChannel)
|
|
|
|
// Verify skip message was logged
|
|
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
|
"custom_modes.yaml already exists, skipping migration",
|
|
)
|
|
|
|
// Verify no file operations occurred
|
|
expect(mockWrite).not.toHaveBeenCalled()
|
|
expect(mockUnlink).not.toHaveBeenCalled()
|
|
})
|
|
})
|