mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Import settings bug fix / improvements (#3657)
This commit is contained in:
parent
f70e03e3b4
commit
dc694efdb2
21 changed files with 182 additions and 194 deletions
5
.changeset/young-dancers-join.md
Normal file
5
.changeset/young-dancers-join.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Fix settings import when global settings are omitted
|
||||
|
|
@ -11,7 +11,6 @@ import { ProviderSettingsManager } from "../ProviderSettingsManager"
|
|||
import { ContextProxy } from "../ContextProxy"
|
||||
import { CustomModesManager } from "../CustomModesManager"
|
||||
|
||||
// Mock VSCode modules
|
||||
jest.mock("vscode", () => ({
|
||||
window: {
|
||||
showOpenDialog: jest.fn(),
|
||||
|
|
@ -22,14 +21,12 @@ jest.mock("vscode", () => ({
|
|||
},
|
||||
}))
|
||||
|
||||
// Mock fs/promises
|
||||
jest.mock("fs/promises", () => ({
|
||||
readFile: jest.fn(),
|
||||
mkdir: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock os module
|
||||
jest.mock("os", () => ({
|
||||
homedir: jest.fn(() => "/mock/home"),
|
||||
}))
|
||||
|
|
@ -41,17 +38,14 @@ describe("importExport", () => {
|
|||
let mockCustomModesManager: jest.Mocked<CustomModesManager>
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Setup providerSettingsManager mock
|
||||
mockProviderSettingsManager = {
|
||||
export: jest.fn(),
|
||||
import: jest.fn(),
|
||||
listConfig: jest.fn(),
|
||||
} as unknown as jest.Mocked<ProviderSettingsManager>
|
||||
|
||||
// Setup contextProxy mock with properly typed export method
|
||||
mockContextProxy = {
|
||||
setValues: jest.fn(),
|
||||
setValue: jest.fn(),
|
||||
|
|
@ -59,10 +53,7 @@ describe("importExport", () => {
|
|||
setProviderSettings: jest.fn(),
|
||||
} as unknown as jest.Mocked<ContextProxy>
|
||||
|
||||
// Setup customModesManager mock
|
||||
mockCustomModesManager = {
|
||||
updateCustomMode: jest.fn(),
|
||||
} as unknown as jest.Mocked<CustomModesManager>
|
||||
mockCustomModesManager = { updateCustomMode: jest.fn() } as unknown as jest.Mocked<CustomModesManager>
|
||||
|
||||
const map = new Map<string, string>()
|
||||
|
||||
|
|
@ -76,7 +67,6 @@ describe("importExport", () => {
|
|||
|
||||
describe("importSettings", () => {
|
||||
it("should return success: false when user cancels file selection", async () => {
|
||||
// Mock user canceling file selection
|
||||
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue(undefined)
|
||||
|
||||
const result = await importSettings({
|
||||
|
|
@ -86,64 +76,118 @@ describe("importExport", () => {
|
|||
})
|
||||
|
||||
expect(result).toEqual({ success: false })
|
||||
|
||||
expect(vscode.window.showOpenDialog).toHaveBeenCalledWith({
|
||||
filters: { JSON: ["json"] },
|
||||
canSelectMany: false,
|
||||
})
|
||||
|
||||
expect(fs.readFile).not.toHaveBeenCalled()
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should import settings successfully from a valid file", async () => {
|
||||
// Mock successful file selection
|
||||
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
|
||||
|
||||
// Valid settings content
|
||||
const mockFileContent = JSON.stringify({
|
||||
providerProfiles: {
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: "openai" as ProviderName,
|
||||
apiKey: "test-key",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
globalSettings: {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } },
|
||||
},
|
||||
globalSettings: { mode: "code", autoApprovalEnabled: true },
|
||||
})
|
||||
|
||||
// Mock reading file
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue(mockFileContent)
|
||||
|
||||
// Mock export returning previous provider profiles
|
||||
const previousProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
apiProvider: "anthropic" as ProviderName,
|
||||
id: "default-id",
|
||||
},
|
||||
},
|
||||
apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
|
||||
}
|
||||
|
||||
mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
|
||||
|
||||
// Mock listConfig
|
||||
mockProviderSettingsManager.listConfig.mockResolvedValue([
|
||||
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
|
||||
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
|
||||
])
|
||||
|
||||
// Mock contextProxy.export
|
||||
mockContextProxy.export.mockResolvedValue({
|
||||
mode: "code",
|
||||
mockContextProxy.export.mockResolvedValue({ mode: "code" })
|
||||
|
||||
const result = await importSettings({
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
|
||||
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
|
||||
|
||||
expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({
|
||||
...previousProviderProfiles,
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } },
|
||||
})
|
||||
|
||||
expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true })
|
||||
expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "test")
|
||||
|
||||
expect(mockContextProxy.setValue).toHaveBeenCalledWith("listApiConfigMeta", [
|
||||
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
|
||||
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
|
||||
])
|
||||
})
|
||||
|
||||
it("should return success: false when file content is invalid", async () => {
|
||||
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
|
||||
|
||||
// Invalid content (missing required fields).
|
||||
const mockInvalidContent = JSON.stringify({
|
||||
providerProfiles: { apiConfigs: {} },
|
||||
globalSettings: {},
|
||||
})
|
||||
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue(mockInvalidContent)
|
||||
|
||||
const result = await importSettings({
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: false, error: "[providerProfiles.currentApiConfigName]: Required" })
|
||||
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should import settings successfully when globalSettings key is missing", async () => {
|
||||
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
|
||||
|
||||
const mockFileContent = JSON.stringify({
|
||||
providerProfiles: {
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } },
|
||||
},
|
||||
})
|
||||
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue(mockFileContent)
|
||||
|
||||
const previousProviderProfiles = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
|
||||
}
|
||||
|
||||
mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
|
||||
|
||||
mockProviderSettingsManager.listConfig.mockResolvedValue([
|
||||
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
|
||||
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
|
||||
])
|
||||
|
||||
mockContextProxy.export.mockResolvedValue({ mode: "code" })
|
||||
|
||||
const result = await importSettings({
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
|
|
@ -157,17 +201,12 @@ describe("importExport", () => {
|
|||
...previousProviderProfiles,
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: "openai" as ProviderName,
|
||||
apiKey: "test-key",
|
||||
id: "test-id",
|
||||
},
|
||||
test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" },
|
||||
},
|
||||
})
|
||||
expect(mockContextProxy.setValues).toHaveBeenCalledWith({
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
})
|
||||
|
||||
// Should call setValues with an empty object since globalSettings is missing.
|
||||
expect(mockContextProxy.setValues).toHaveBeenCalledWith({})
|
||||
expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "test")
|
||||
expect(mockContextProxy.setValue).toHaveBeenCalledWith("listApiConfigMeta", [
|
||||
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
|
||||
|
|
@ -175,39 +214,9 @@ describe("importExport", () => {
|
|||
])
|
||||
})
|
||||
|
||||
it("should return success: false when file content is invalid", async () => {
|
||||
// Mock successful file selection
|
||||
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
|
||||
|
||||
// Invalid content (missing required fields)
|
||||
const mockInvalidContent = JSON.stringify({
|
||||
providerProfiles: { apiConfigs: {} },
|
||||
globalSettings: {},
|
||||
})
|
||||
|
||||
// Mock reading file
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue(mockInvalidContent)
|
||||
|
||||
const result = await importSettings({
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: false })
|
||||
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should return success: false when file content is not valid JSON", async () => {
|
||||
// Mock successful file selection
|
||||
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
|
||||
|
||||
// Invalid JSON
|
||||
const mockInvalidJson = "{ this is not valid JSON }"
|
||||
|
||||
// Mock reading file
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue(mockInvalidJson)
|
||||
|
||||
const result = await importSettings({
|
||||
|
|
@ -216,17 +225,14 @@ describe("importExport", () => {
|
|||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: false })
|
||||
expect(result).toEqual({ success: false, error: "Expected property name or '}' in JSON at position 2" })
|
||||
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should return success: false when reading file fails", async () => {
|
||||
// Mock successful file selection
|
||||
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
|
||||
|
||||
// Mock file read error
|
||||
;(fs.readFile as jest.Mock).mockRejectedValue(new Error("File read error"))
|
||||
|
||||
const result = await importSettings({
|
||||
|
|
@ -235,7 +241,7 @@ describe("importExport", () => {
|
|||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: false })
|
||||
expect(result).toEqual({ success: false, error: "File read error" })
|
||||
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
|
||||
|
|
@ -277,43 +283,35 @@ describe("importExport", () => {
|
|||
|
||||
it("should call updateCustomMode for each custom mode in config", async () => {
|
||||
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
|
||||
|
||||
const customModes = [
|
||||
{
|
||||
slug: "mode1",
|
||||
name: "Mode One",
|
||||
roleDefinition: "Custom role one",
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
slug: "mode2",
|
||||
name: "Mode Two",
|
||||
roleDefinition: "Custom role two",
|
||||
groups: [],
|
||||
},
|
||||
{ slug: "mode1", name: "Mode One", roleDefinition: "Custom role one", groups: [] },
|
||||
{ slug: "mode2", name: "Mode Two", roleDefinition: "Custom role two", groups: [] },
|
||||
]
|
||||
|
||||
const mockFileContent = JSON.stringify({
|
||||
providerProfiles: {
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: {},
|
||||
},
|
||||
globalSettings: {
|
||||
mode: "code",
|
||||
customModes,
|
||||
},
|
||||
providerProfiles: { currentApiConfigName: "test", apiConfigs: {} },
|
||||
globalSettings: { mode: "code", customModes },
|
||||
})
|
||||
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue(mockFileContent)
|
||||
|
||||
mockProviderSettingsManager.export.mockResolvedValue({
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: {},
|
||||
})
|
||||
|
||||
mockProviderSettingsManager.listConfig.mockResolvedValue([])
|
||||
|
||||
const result = await importSettings({
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(mockCustomModesManager.updateCustomMode).toHaveBeenCalledTimes(customModes.length)
|
||||
|
||||
customModes.forEach((mode) => {
|
||||
expect(mockCustomModesManager.updateCustomMode).toHaveBeenCalledWith(mode.slug, mode)
|
||||
})
|
||||
|
|
@ -321,7 +319,6 @@ describe("importExport", () => {
|
|||
|
||||
describe("exportSettings", () => {
|
||||
it("should not export settings when user cancels file selection", async () => {
|
||||
// Mock user canceling file selection
|
||||
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue(undefined)
|
||||
|
||||
await exportSettings({
|
||||
|
|
@ -333,37 +330,25 @@ describe("importExport", () => {
|
|||
filters: { JSON: ["json"] },
|
||||
defaultUri: expect.anything(),
|
||||
})
|
||||
|
||||
expect(mockProviderSettingsManager.export).not.toHaveBeenCalled()
|
||||
expect(mockContextProxy.export).not.toHaveBeenCalled()
|
||||
expect(fs.writeFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should export settings to the selected file location", async () => {
|
||||
// Mock successful file location selection
|
||||
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue({
|
||||
fsPath: "/mock/path/roo-code-settings.json",
|
||||
})
|
||||
|
||||
// Mock providerProfiles data
|
||||
const mockProviderProfiles = {
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: "openai" as ProviderName,
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
rateLimitSecondsMigrated: false,
|
||||
},
|
||||
apiConfigs: { test: { apiProvider: "openai" as ProviderName, id: "test-id" } },
|
||||
migrations: { rateLimitSecondsMigrated: false },
|
||||
}
|
||||
mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
|
||||
|
||||
// Mock globalSettings data
|
||||
const mockGlobalSettings = {
|
||||
mode: "code",
|
||||
autoApprovalEnabled: true,
|
||||
}
|
||||
mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
|
||||
const mockGlobalSettings = { mode: "code", autoApprovalEnabled: true }
|
||||
mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
|
||||
|
||||
await exportSettings({
|
||||
|
|
@ -375,52 +360,32 @@ describe("importExport", () => {
|
|||
filters: { JSON: ["json"] },
|
||||
defaultUri: expect.anything(),
|
||||
})
|
||||
|
||||
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
|
||||
expect(mockContextProxy.export).toHaveBeenCalled()
|
||||
expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true })
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
"/mock/path/roo-code-settings.json",
|
||||
JSON.stringify(
|
||||
{
|
||||
providerProfiles: mockProviderProfiles,
|
||||
globalSettings: mockGlobalSettings,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
JSON.stringify({ providerProfiles: mockProviderProfiles, globalSettings: mockGlobalSettings }, null, 2),
|
||||
"utf-8",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle errors during the export process", async () => {
|
||||
// Mock successful file location selection
|
||||
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue({
|
||||
fsPath: "/mock/path/roo-code-settings.json",
|
||||
})
|
||||
|
||||
// Mock provider profiles
|
||||
mockProviderSettingsManager.export.mockResolvedValue({
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: "openai" as ProviderName,
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
rateLimitSecondsMigrated: false,
|
||||
},
|
||||
apiConfigs: { test: { apiProvider: "openai" as ProviderName, id: "test-id" } },
|
||||
migrations: { rateLimitSecondsMigrated: false },
|
||||
})
|
||||
|
||||
// Mock global settings
|
||||
mockContextProxy.export.mockResolvedValue({
|
||||
mode: "code",
|
||||
})
|
||||
|
||||
// Mock file write error
|
||||
mockContextProxy.export.mockResolvedValue({ mode: "code" })
|
||||
;(fs.writeFile as jest.Mock).mockRejectedValue(new Error("Write error"))
|
||||
|
||||
// The function catches errors internally and doesn't throw or return anything
|
||||
await exportSettings({
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
|
|
@ -431,38 +396,23 @@ describe("importExport", () => {
|
|||
expect(mockContextProxy.export).toHaveBeenCalled()
|
||||
expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true })
|
||||
expect(fs.writeFile).toHaveBeenCalled()
|
||||
// The error is caught and the function exits silently
|
||||
// The error is caught and the function exits silently.
|
||||
})
|
||||
|
||||
it("should handle errors during directory creation", async () => {
|
||||
// Mock successful file location selection
|
||||
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue({
|
||||
fsPath: "/mock/path/roo-code-settings.json",
|
||||
})
|
||||
|
||||
// Mock provider profiles
|
||||
mockProviderSettingsManager.export.mockResolvedValue({
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: "openai" as ProviderName,
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
rateLimitSecondsMigrated: false,
|
||||
},
|
||||
apiConfigs: { test: { apiProvider: "openai" as ProviderName, id: "test-id" } },
|
||||
migrations: { rateLimitSecondsMigrated: false },
|
||||
})
|
||||
|
||||
// Mock global settings
|
||||
mockContextProxy.export.mockResolvedValue({
|
||||
mode: "code",
|
||||
})
|
||||
|
||||
// Mock directory creation error
|
||||
mockContextProxy.export.mockResolvedValue({ mode: "code" })
|
||||
;(fs.mkdir as jest.Mock).mockRejectedValue(new Error("Directory creation error"))
|
||||
|
||||
// The function catches errors internally and doesn't throw or return anything
|
||||
await exportSettings({
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
|
|
@ -472,26 +422,22 @@ describe("importExport", () => {
|
|||
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
|
||||
expect(mockContextProxy.export).toHaveBeenCalled()
|
||||
expect(fs.mkdir).toHaveBeenCalled()
|
||||
expect(fs.writeFile).not.toHaveBeenCalled() // Should not be called since mkdir failed
|
||||
expect(fs.writeFile).not.toHaveBeenCalled() // Should not be called since mkdir failed.
|
||||
})
|
||||
|
||||
it("should use the correct default save location", async () => {
|
||||
// Mock user cancels to avoid full execution
|
||||
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue(undefined)
|
||||
|
||||
// Call the function
|
||||
await exportSettings({
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
})
|
||||
|
||||
// Verify the default save location
|
||||
expect(vscode.window.showSaveDialog).toHaveBeenCalledWith({
|
||||
filters: { JSON: ["json"] },
|
||||
defaultUri: expect.anything(),
|
||||
})
|
||||
|
||||
// Verify Uri.file was called with the correct path
|
||||
expect(vscode.Uri.file).toHaveBeenCalledWith(path.join("/mock/home", "Documents", "roo-code-settings.json"))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import * as path from "path"
|
|||
import fs from "fs/promises"
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
import { z, ZodError } from "zod"
|
||||
|
||||
import { globalSettingsSchema } from "../../schemas"
|
||||
|
||||
import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager"
|
||||
import { ContextProxy } from "./ContextProxy"
|
||||
import { CustomModesManager } from "./CustomModesManager"
|
||||
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
||||
|
||||
type ImportOptions = {
|
||||
providerSettingsManager: ProviderSettingsManager
|
||||
|
|
@ -34,15 +35,14 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu
|
|||
|
||||
const schema = z.object({
|
||||
providerProfiles: providerProfilesSchema,
|
||||
globalSettings: globalSettingsSchema,
|
||||
globalSettings: globalSettingsSchema.optional(),
|
||||
})
|
||||
|
||||
try {
|
||||
const previousProviderProfiles = await providerSettingsManager.export()
|
||||
|
||||
const { providerProfiles: newProviderProfiles, globalSettings } = schema.parse(
|
||||
JSON.parse(await fs.readFile(uris[0].fsPath, "utf-8")),
|
||||
)
|
||||
const data = JSON.parse(await fs.readFile(uris[0].fsPath, "utf-8"))
|
||||
const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse(data)
|
||||
|
||||
const providerProfiles = {
|
||||
currentApiConfigName: newProviderProfiles.currentApiConfigName,
|
||||
|
|
@ -79,7 +79,16 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu
|
|||
|
||||
return { providerProfiles, globalSettings, success: true }
|
||||
} catch (e) {
|
||||
return { success: false }
|
||||
let error = "Unknown error"
|
||||
|
||||
if (e instanceof ZodError) {
|
||||
error = e.issues.map((issue) => `[${issue.path.join(".")}]: ${issue.message}`).join("\n")
|
||||
telemetryService.captureSchemaValidationError({ schemaName: "ImportExport", error: e })
|
||||
} else if (e instanceof Error) {
|
||||
error = e.message
|
||||
}
|
||||
|
||||
return { success: false, error }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +106,14 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }:
|
|||
const providerProfiles = await providerSettingsManager.export()
|
||||
const globalSettings = await contextProxy.export()
|
||||
|
||||
// It's okay if there are no global settings, but if there are no
|
||||
// provider profile configured then don't export. If we wanted to
|
||||
// support this case then the `importSettings` function would need to
|
||||
// be updated to handle the case where there are no provider profiles.
|
||||
if (typeof providerProfiles === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
const dirname = path.dirname(uri.fsPath)
|
||||
await fs.mkdir(dirname, { recursive: true })
|
||||
await fs.writeFile(uri.fsPath, JSON.stringify({ providerProfiles, globalSettings }, null, 2), "utf-8")
|
||||
|
|
|
|||
|
|
@ -245,20 +245,23 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
|
|||
case "exportTaskWithId":
|
||||
provider.exportTaskWithId(message.text!)
|
||||
break
|
||||
case "importSettings":
|
||||
const { success } = await importSettings({
|
||||
case "importSettings": {
|
||||
const result = await importSettings({
|
||||
providerSettingsManager: provider.providerSettingsManager,
|
||||
contextProxy: provider.contextProxy,
|
||||
customModesManager: provider.customModesManager,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
if (result.success) {
|
||||
provider.settingsImportedAt = Date.now()
|
||||
await provider.postStateToWebview()
|
||||
await vscode.window.showInformationMessage(t("common:info.settings_imported"))
|
||||
} else if (result.error) {
|
||||
await vscode.window.showErrorMessage(t("common:errors.settings_import_failed", { error: result.error }))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case "exportSettings":
|
||||
await exportSettings({
|
||||
providerSettingsManager: provider.providerSettingsManager,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@
|
|||
"mcp_server_not_found": "Servidor \"{{serverName}}\" no trobat a la configuració",
|
||||
"custom_storage_path_set": "Ruta d'emmagatzematge personalitzada establerta: {{path}}",
|
||||
"default_storage_path": "S'ha reprès l'ús de la ruta d'emmagatzematge predeterminada",
|
||||
"settings_imported": "Configuració importada correctament."
|
||||
"settings_imported": "Configuració importada correctament.",
|
||||
"settings_import_failed": "Ha fallat la importació de la configuració: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Sí",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Server \"{{serverName}}\" nicht in der Konfiguration gefunden",
|
||||
"custom_storage_path_set": "Benutzerdefinierter Speicherpfad festgelegt: {{path}}",
|
||||
"default_storage_path": "Auf Standardspeicherpfad zurückgesetzt",
|
||||
"settings_imported": "Einstellungen erfolgreich importiert."
|
||||
"settings_imported": "Einstellungen erfolgreich importiert.",
|
||||
"settings_import_failed": "Fehler beim Importieren der Einstellungen: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Ja",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Server \"{{serverName}}\" not found in configuration",
|
||||
"custom_storage_path_set": "Custom storage path set: {{path}}",
|
||||
"default_storage_path": "Reverted to using default storage path",
|
||||
"settings_imported": "Settings imported successfully."
|
||||
"settings_imported": "Settings imported successfully.",
|
||||
"settings_import_failed": "Settings import failed: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Yes",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Servidor \"{{serverName}}\" no encontrado en la configuración",
|
||||
"custom_storage_path_set": "Ruta de almacenamiento personalizada establecida: {{path}}",
|
||||
"default_storage_path": "Se ha vuelto a usar la ruta de almacenamiento predeterminada",
|
||||
"settings_imported": "Configuración importada correctamente."
|
||||
"settings_imported": "Configuración importada correctamente.",
|
||||
"settings_import_failed": "Error al importar la configuración: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Sí",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Serveur \"{{serverName}}\" introuvable dans la configuration",
|
||||
"custom_storage_path_set": "Chemin de stockage personnalisé défini : {{path}}",
|
||||
"default_storage_path": "Retour au chemin de stockage par défaut",
|
||||
"settings_imported": "Paramètres importés avec succès."
|
||||
"settings_imported": "Paramètres importés avec succès.",
|
||||
"settings_import_failed": "Échec de l'importation des paramètres : {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Oui",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "सर्वर \"{{serverName}}\" कॉन्फ़िगरेशन में नहीं मिला",
|
||||
"custom_storage_path_set": "कस्टम स्टोरेज पाथ सेट किया गया: {{path}}",
|
||||
"default_storage_path": "डिफ़ॉल्ट स्टोरेज पाथ का उपयोग पुनः शुरू किया गया",
|
||||
"settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।"
|
||||
"settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।",
|
||||
"settings_import_failed": "सेटिंग्स इम्पोर्ट करने में विफल: {{error}}।"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "हां",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Server \"{{serverName}}\" non trovato nella configurazione",
|
||||
"custom_storage_path_set": "Percorso di archiviazione personalizzato impostato: {{path}}",
|
||||
"default_storage_path": "Tornato al percorso di archiviazione predefinito",
|
||||
"settings_imported": "Impostazioni importate con successo."
|
||||
"settings_imported": "Impostazioni importate con successo.",
|
||||
"settings_import_failed": "Importazione delle impostazioni fallita: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Sì",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "サーバー\"{{serverName}}\"が設定内に見つかりません",
|
||||
"custom_storage_path_set": "カスタムストレージパスが設定されました:{{path}}",
|
||||
"default_storage_path": "デフォルトのストレージパスに戻りました",
|
||||
"settings_imported": "設定が正常にインポートされました。"
|
||||
"settings_imported": "設定が正常にインポートされました。",
|
||||
"settings_import_failed": "設定のインポートに失敗しました:{{error}}。"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "はい",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "구성에서 서버 \"{{serverName}}\"을(를) 찾을 수 없습니다",
|
||||
"custom_storage_path_set": "사용자 지정 저장 경로 설정됨: {{path}}",
|
||||
"default_storage_path": "기본 저장 경로로 되돌아갔습니다",
|
||||
"settings_imported": "설정이 성공적으로 가져와졌습니다."
|
||||
"settings_imported": "설정이 성공적으로 가져와졌습니다.",
|
||||
"settings_import_failed": "설정 가져오기 실패: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "예",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Server \"{{serverName}}\" niet gevonden in configuratie",
|
||||
"custom_storage_path_set": "Aangepast opslagpad ingesteld: {{path}}",
|
||||
"default_storage_path": "Terug naar standaard opslagpad",
|
||||
"settings_imported": "Instellingen succesvol geïmporteerd."
|
||||
"settings_imported": "Instellingen succesvol geïmporteerd.",
|
||||
"settings_import_failed": "Importeren van instellingen mislukt: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Ja",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Serwer \"{{serverName}}\" nie znaleziony w konfiguracji",
|
||||
"custom_storage_path_set": "Ustawiono niestandardową ścieżkę przechowywania: {{path}}",
|
||||
"default_storage_path": "Wznowiono używanie domyślnej ścieżki przechowywania",
|
||||
"settings_imported": "Ustawienia zaimportowane pomyślnie."
|
||||
"settings_imported": "Ustawienia zaimportowane pomyślnie.",
|
||||
"settings_import_failed": "Nie udało się zaimportować ustawień: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Tak",
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@
|
|||
"mcp_server_not_found": "Servidor \"{{serverName}}\" não encontrado na configuração",
|
||||
"custom_storage_path_set": "Caminho de armazenamento personalizado definido: {{path}}",
|
||||
"default_storage_path": "Retornado ao caminho de armazenamento padrão",
|
||||
"settings_imported": "Configurações importadas com sucesso."
|
||||
"settings_imported": "Configurações importadas com sucesso.",
|
||||
"settings_import_failed": "Falha ao importar configurações: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Sim",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Сервер \"{{serverName}}\" не найден в конфигурации",
|
||||
"custom_storage_path_set": "Установлен пользовательский путь хранения: {{path}}",
|
||||
"default_storage_path": "Возвращено использование пути хранения по умолчанию",
|
||||
"settings_imported": "Настройки успешно импортированы."
|
||||
"settings_imported": "Настройки успешно импортированы.",
|
||||
"settings_import_failed": "Не удалось импортировать настройки: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Да",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Yapılandırmada \"{{serverName}}\" sunucusu bulunamadı",
|
||||
"custom_storage_path_set": "Özel depolama yolu ayarlandı: {{path}}",
|
||||
"default_storage_path": "Varsayılan depolama yoluna geri dönüldü",
|
||||
"settings_imported": "Ayarlar başarıyla içe aktarıldı."
|
||||
"settings_imported": "Ayarlar başarıyla içe aktarıldı.",
|
||||
"settings_import_failed": "Ayarlar içe aktarılamadı: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Evet",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "Không tìm thấy máy chủ \"{{serverName}}\" trong cấu hình",
|
||||
"custom_storage_path_set": "Đã thiết lập đường dẫn lưu trữ tùy chỉnh: {{path}}",
|
||||
"default_storage_path": "Đã quay lại sử dụng đường dẫn lưu trữ mặc định",
|
||||
"settings_imported": "Cài đặt đã được nhập thành công."
|
||||
"settings_imported": "Cài đặt đã được nhập thành công.",
|
||||
"settings_import_failed": "Nhập cài đặt thất bại: {{error}}."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Có",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "在配置中未找到服务器\"{{serverName}}\"",
|
||||
"custom_storage_path_set": "自定义存储路径已设置:{{path}}",
|
||||
"default_storage_path": "已恢复使用默认存储路径",
|
||||
"settings_imported": "设置已成功导入。"
|
||||
"settings_imported": "设置已成功导入。",
|
||||
"settings_import_failed": "设置导入失败:{{error}}。"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "是",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@
|
|||
"mcp_server_not_found": "在設定中沒有找到伺服器\"{{serverName}}\"",
|
||||
"custom_storage_path_set": "自訂儲存路徑已設定:{{path}}",
|
||||
"default_storage_path": "已恢復使用預設儲存路徑",
|
||||
"settings_imported": "設定已成功匯入。"
|
||||
"settings_imported": "設定已成功匯入。",
|
||||
"settings_import_failed": "設定匯入失敗:{{error}}。"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "是",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue