Roo-Code/src/__tests__/migrateSettings.spec.ts
Chris Estreich 62c3914034
Farewell jest (#4607)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-06-16 21:39:45 -07:00

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()
})
})