Skip to content

Commit c383c77

Browse files
committed
feat: implement settings import from local file functionality
- Add importSettingsFromContent function to import settings from string content - Refactor importSettingsFromPath to reuse import logic - Improve import error handling and type validation - Add ImportSource type to support file path or content - Update related components and message handling to support new import methods - Enhance test cases to cover new import functionality
1 parent 2263d86 commit c383c77

File tree

8 files changed

+371
-74
lines changed

8 files changed

+371
-74
lines changed

src/activate/registerCommands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
190190
customModesManager: visibleProvider.customModesManager,
191191
provider: visibleProvider,
192192
},
193-
filePath,
193+
filePath ? { filePath } : undefined,
194194
)
195195
},
196196
focusInput: async () => {

src/core/config/__tests__/importExport.spec.ts

Lines changed: 184 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// npx vitest src/core/config/__tests__/importExport.spec.ts
22

3+
import { describe, it, expect, vi, beforeEach } from "vitest"
34
import fs from "fs/promises"
45
import * as path from "path"
56

@@ -8,7 +9,13 @@ import * as vscode from "vscode"
89
import type { ProviderName } from "@roo-code/types"
910
import { TelemetryService } from "@roo-code/telemetry"
1011

11-
import { importSettings, importSettingsFromFile, importSettingsWithFeedback, exportSettings } from "../importExport"
12+
import {
13+
importSettings,
14+
importSettingsFromFile,
15+
importSettingsFromContent,
16+
importSettingsWithFeedback,
17+
exportSettings,
18+
} from "../importExport"
1219
import { ProviderSettingsManager } from "../ProviderSettingsManager"
1320
import { ContextProxy } from "../ContextProxy"
1421
import { CustomModesManager } from "../CustomModesManager"
@@ -263,7 +270,9 @@ describe("importExport", () => {
263270
})
264271

265272
expect(result.success).toBe(false)
266-
expect(result.error).toMatch(/^Expected property name or '}' in JSON at position 2/)
273+
if (!result.success) {
274+
expect(result.error).toMatch(/^Expected property name or '}' in JSON at position 2/)
275+
}
267276
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
268277
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
269278
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
@@ -426,7 +435,7 @@ describe("importExport", () => {
426435
customModesManager: mockCustomModesManager,
427436
provider: mockProvider,
428437
},
429-
filePath,
438+
{ filePath },
430439
)
431440

432441
expect(vscode.window.showOpenDialog).not.toHaveBeenCalled()
@@ -436,6 +445,178 @@ describe("importExport", () => {
436445

437446
showErrorMessageSpy.mockRestore()
438447
})
448+
449+
it("should import settings successfully from provided fileContents", async () => {
450+
const mockFileContent = JSON.stringify({
451+
providerProfiles: {
452+
currentApiConfigName: "test",
453+
apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } },
454+
},
455+
globalSettings: { mode: "code", autoApprovalEnabled: true },
456+
})
457+
458+
const previousProviderProfiles = {
459+
currentApiConfigName: "default",
460+
apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
461+
}
462+
463+
mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
464+
mockProviderSettingsManager.listConfig.mockResolvedValue([
465+
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
466+
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
467+
])
468+
mockContextProxy.export.mockResolvedValue({ mode: "code" })
469+
470+
const result = await importSettingsFromContent(mockFileContent, {
471+
providerSettingsManager: mockProviderSettingsManager,
472+
contextProxy: mockContextProxy,
473+
customModesManager: mockCustomModesManager,
474+
})
475+
476+
expect(result.success).toBe(true)
477+
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
478+
expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({
479+
currentApiConfigName: "test",
480+
apiConfigs: {
481+
default: { apiProvider: "anthropic" as ProviderName, id: "default-id" },
482+
test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" },
483+
},
484+
modeApiConfigs: {},
485+
})
486+
expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true })
487+
expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "test")
488+
expect(mockContextProxy.setValue).toHaveBeenCalledWith("listApiConfigMeta", [
489+
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
490+
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
491+
])
492+
})
493+
494+
it("should return success: false when fileContents is not valid JSON", async () => {
495+
const mockInvalidJson = "{ this is not valid JSON }"
496+
497+
const result = await importSettingsFromContent(mockInvalidJson, {
498+
providerSettingsManager: mockProviderSettingsManager,
499+
contextProxy: mockContextProxy,
500+
customModesManager: mockCustomModesManager,
501+
})
502+
503+
expect(result.success).toBe(false)
504+
if (!result.success) {
505+
expect(result.error).toMatch(/^Expected property name or '}' in JSON at position 2/)
506+
}
507+
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
508+
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
509+
})
510+
511+
it("should return success: false when fileContents is missing required fields", async () => {
512+
// Invalid content (missing required fields)
513+
const mockInvalidContent = JSON.stringify({
514+
providerProfiles: { apiConfigs: {} },
515+
globalSettings: {},
516+
})
517+
518+
const result = await importSettingsFromContent(mockInvalidContent, {
519+
providerSettingsManager: mockProviderSettingsManager,
520+
contextProxy: mockContextProxy,
521+
customModesManager: mockCustomModesManager,
522+
})
523+
524+
expect(result.success).toBe(false)
525+
if (!result.success) {
526+
expect(result.error).toBe("[providerProfiles.currentApiConfigName]: Required")
527+
}
528+
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
529+
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
530+
})
531+
532+
it("should import settings successfully using importSettingsWithFeedback with fileContents", async () => {
533+
const mockFileContent = JSON.stringify({
534+
providerProfiles: {
535+
currentApiConfigName: "test",
536+
apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } },
537+
},
538+
globalSettings: { mode: "code", autoApprovalEnabled: true },
539+
})
540+
541+
const previousProviderProfiles = {
542+
currentApiConfigName: "default",
543+
apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
544+
}
545+
546+
mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
547+
mockProviderSettingsManager.listConfig.mockResolvedValue([
548+
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
549+
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
550+
])
551+
mockContextProxy.export.mockResolvedValue({ mode: "code" })
552+
553+
const mockProvider = {
554+
settingsImportedAt: 0,
555+
postStateToWebview: vi.fn().mockResolvedValue(undefined),
556+
}
557+
558+
const showInformationMessageSpy = vi
559+
.spyOn(vscode.window, "showInformationMessage")
560+
.mockResolvedValue(undefined)
561+
562+
await importSettingsWithFeedback(
563+
{
564+
providerSettingsManager: mockProviderSettingsManager,
565+
contextProxy: mockContextProxy,
566+
customModesManager: mockCustomModesManager,
567+
provider: mockProvider,
568+
},
569+
{ fileContents: mockFileContent },
570+
)
571+
572+
expect(vscode.window.showOpenDialog).not.toHaveBeenCalled()
573+
expect(fs.readFile).not.toHaveBeenCalled()
574+
expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({
575+
currentApiConfigName: "test",
576+
apiConfigs: {
577+
default: { apiProvider: "anthropic" as ProviderName, id: "default-id" },
578+
test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" },
579+
},
580+
modeApiConfigs: {},
581+
})
582+
expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true })
583+
expect(mockProvider.settingsImportedAt).toBeGreaterThan(0)
584+
expect(mockProvider.postStateToWebview).toHaveBeenCalled()
585+
expect(showInformationMessageSpy).toHaveBeenCalledWith(expect.stringContaining("info.settings_imported"))
586+
587+
showInformationMessageSpy.mockRestore()
588+
})
589+
590+
it("should show error message when importSettingsWithFeedback fails with invalid fileContents", async () => {
591+
const mockInvalidJson = "{ this is not valid JSON }"
592+
593+
const mockProvider = {
594+
settingsImportedAt: 0,
595+
postStateToWebview: vi.fn().mockResolvedValue(undefined),
596+
}
597+
598+
const showErrorMessageSpy = vi.spyOn(vscode.window, "showErrorMessage").mockResolvedValue(undefined)
599+
600+
await importSettingsWithFeedback(
601+
{
602+
providerSettingsManager: mockProviderSettingsManager,
603+
contextProxy: mockContextProxy,
604+
customModesManager: mockCustomModesManager,
605+
provider: mockProvider,
606+
},
607+
{ fileContents: mockInvalidJson },
608+
)
609+
610+
expect(vscode.window.showOpenDialog).not.toHaveBeenCalled()
611+
expect(fs.readFile).not.toHaveBeenCalled()
612+
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
613+
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
614+
expect(mockProvider.settingsImportedAt).toBe(0)
615+
expect(mockProvider.postStateToWebview).not.toHaveBeenCalled()
616+
expect(showErrorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("errors.settings_import_failed"))
617+
618+
showErrorMessageSpy.mockRestore()
619+
})
439620
})
440621

441622
describe("exportSettings", () => {

0 commit comments

Comments
 (0)