@@ -22,10 +22,13 @@ vi.mock("vscode", () => ({
2222 showSaveDialog : vi . fn ( ) ,
2323 showErrorMessage : vi . fn ( ) ,
2424 showInformationMessage : vi . fn ( ) ,
25+ showQuickPick : vi . fn ( ) ,
26+ showInputBox : vi . fn ( ) ,
2527 } ,
2628 Uri : {
2729 file : vi . fn ( ( filePath ) => ( { fsPath : filePath } ) ) ,
2830 } ,
31+ env : { } ,
2932} ) )
3033
3134vi . mock ( "fs/promises" , ( ) => ( {
@@ -34,6 +37,7 @@ vi.mock("fs/promises", () => ({
3437 mkdir : vi . fn ( ) ,
3538 writeFile : vi . fn ( ) ,
3639 access : vi . fn ( ) ,
40+ unlink : vi . fn ( ) ,
3741 constants : {
3842 F_OK : 0 ,
3943 R_OK : 4 ,
@@ -43,6 +47,7 @@ vi.mock("fs/promises", () => ({
4347 mkdir : vi . fn ( ) ,
4448 writeFile : vi . fn ( ) ,
4549 access : vi . fn ( ) ,
50+ unlink : vi . fn ( ) ,
4651 constants : {
4752 F_OK : 0 ,
4853 R_OK : 4 ,
@@ -52,8 +57,10 @@ vi.mock("fs/promises", () => ({
5257vi . mock ( "os" , ( ) => ( {
5358 default : {
5459 homedir : vi . fn ( ( ) => "/mock/home" ) ,
60+ tmpdir : vi . fn ( ( ) => "/tmp" ) ,
5561 } ,
5662 homedir : vi . fn ( ( ) => "/mock/home" ) ,
63+ tmpdir : vi . fn ( ( ) => "/tmp" ) ,
5764} ) )
5865
5966vi . mock ( "../../../utils/safeWriteJson" )
@@ -436,6 +443,261 @@ describe("importExport", () => {
436443
437444 showErrorMessageSpy . mockRestore ( )
438445 } )
446+
447+ describe ( "remote SSH environment" , ( ) => {
448+ beforeEach ( ( ) => {
449+ // Mock remote environment
450+ ; ( vscode . env as any ) . remoteName = "ssh-remote"
451+ } )
452+
453+ afterEach ( ( ) => {
454+ // Reset to local environment
455+ delete ( vscode . env as any ) . remoteName
456+ } )
457+
458+ it ( "should show quick pick for local vs remote file selection in remote environment" , async ( ) => {
459+ ; ( vscode . window . showQuickPick as Mock ) . mockResolvedValue ( undefined )
460+
461+ const result = await importSettings ( {
462+ providerSettingsManager : mockProviderSettingsManager ,
463+ contextProxy : mockContextProxy ,
464+ customModesManager : mockCustomModesManager ,
465+ } )
466+
467+ expect ( vscode . window . showQuickPick ) . toHaveBeenCalledWith (
468+ expect . arrayContaining ( [
469+ expect . objectContaining ( { value : "local" } ) ,
470+ expect . objectContaining ( { value : "remote" } ) ,
471+ ] ) ,
472+ expect . objectContaining ( {
473+ placeHolder : "Choose where to import settings from" ,
474+ title : "Import Settings" ,
475+ } ) ,
476+ )
477+
478+ expect ( result ) . toEqual ( { success : false , error : "User cancelled import" } )
479+ } )
480+
481+ it ( "should handle paste option for local file in remote environment" , async ( ) => {
482+ ; ( vscode . window . showQuickPick as Mock )
483+ . mockResolvedValueOnce ( { value : "local" } )
484+ . mockResolvedValueOnce ( { value : "paste" } )
485+
486+ const mockSettings = {
487+ providerProfiles : {
488+ currentApiConfigName : "test" ,
489+ apiConfigs : {
490+ test : { apiProvider : "openai" as ProviderName , apiKey : "test-key" , id : "test-id" } ,
491+ } ,
492+ } ,
493+ globalSettings : { mode : "code" } ,
494+ }
495+
496+ ; ( vscode . window . showInputBox as Mock ) . mockResolvedValue ( JSON . stringify ( mockSettings ) )
497+ ; ( fs . writeFile as Mock ) . mockResolvedValue ( undefined )
498+ ; ( fs . unlink as Mock ) . mockResolvedValue ( undefined )
499+ ; ( fs . readFile as Mock ) . mockResolvedValue ( JSON . stringify ( mockSettings ) )
500+
501+ mockProviderSettingsManager . export . mockResolvedValue ( {
502+ currentApiConfigName : "default" ,
503+ apiConfigs : { } ,
504+ } )
505+ mockProviderSettingsManager . listConfig . mockResolvedValue ( [ ] )
506+
507+ const result = await importSettings ( {
508+ providerSettingsManager : mockProviderSettingsManager ,
509+ contextProxy : mockContextProxy ,
510+ customModesManager : mockCustomModesManager ,
511+ } )
512+
513+ expect ( vscode . window . showInputBox ) . toHaveBeenCalledWith (
514+ expect . objectContaining ( {
515+ prompt : "Paste your settings JSON content here" ,
516+ ignoreFocusOut : true ,
517+ } ) ,
518+ )
519+
520+ expect ( fs . writeFile ) . toHaveBeenCalledWith (
521+ expect . stringContaining ( "roo-settings-import-" ) ,
522+ JSON . stringify ( mockSettings ) ,
523+ "utf-8" ,
524+ )
525+
526+ expect ( result . success ) . toBe ( true )
527+ } )
528+
529+ it ( "should validate JSON when pasting content" , async ( ) => {
530+ ; ( vscode . window . showQuickPick as Mock )
531+ . mockResolvedValueOnce ( { value : "local" } )
532+ . mockResolvedValueOnce ( { value : "paste" } )
533+ ; ( vscode . window . showInputBox as Mock ) . mockImplementation ( async ( options ) => {
534+ // Test the validation function
535+ const validateResult = options . validateInput ( '{"invalid": json}' )
536+ expect ( validateResult ) . toBe ( "Invalid JSON format" )
537+
538+ const validResult = options . validateInput ( '{"valid": "json"}' )
539+ expect ( validResult ) . toBeUndefined ( )
540+
541+ const emptyResult = options . validateInput ( "" )
542+ expect ( emptyResult ) . toBe ( "Please paste the settings content" )
543+
544+ return undefined // User cancels
545+ } )
546+
547+ const result = await importSettings ( {
548+ providerSettingsManager : mockProviderSettingsManager ,
549+ contextProxy : mockContextProxy ,
550+ customModesManager : mockCustomModesManager ,
551+ } )
552+
553+ expect ( result ) . toEqual ( { success : false , error : "User cancelled import" } )
554+ } )
555+
556+ it ( "should show info message when local file path is entered in remote environment" , async ( ) => {
557+ ; ( vscode . window . showQuickPick as Mock )
558+ . mockResolvedValueOnce ( { value : "local" } )
559+ . mockResolvedValueOnce ( { value : "path" } )
560+ ; ( vscode . window . showInputBox as Mock ) . mockResolvedValue ( "~/Documents/settings.json" )
561+
562+ const result = await importSettings ( {
563+ providerSettingsManager : mockProviderSettingsManager ,
564+ contextProxy : mockContextProxy ,
565+ customModesManager : mockCustomModesManager ,
566+ } )
567+
568+ expect ( vscode . window . showInformationMessage ) . toHaveBeenCalledWith (
569+ expect . stringContaining ( "To import from a local file in a remote SSH session" ) ,
570+ "OK" ,
571+ )
572+
573+ expect ( result ) . toEqual ( {
574+ success : false ,
575+ error : "Cannot directly access local files from remote environment" ,
576+ } )
577+ } )
578+
579+ it ( "should use standard dialog for remote file selection in remote environment" , async ( ) => {
580+ ; ( vscode . window . showQuickPick as Mock ) . mockResolvedValueOnce ( {
581+ label : "$(remote) Import from remote file" ,
582+ value : "remote" ,
583+ } )
584+ ; ( vscode . window . showOpenDialog as Mock ) . mockResolvedValue ( [ { fsPath : "/remote/path/settings.json" } ] )
585+
586+ const mockSettings = {
587+ providerProfiles : {
588+ currentApiConfigName : "test" ,
589+ apiConfigs : { test : { apiProvider : "openai" as ProviderName , id : "test-id" } } ,
590+ } ,
591+ }
592+
593+ ; ( fs . readFile as Mock ) . mockResolvedValue ( JSON . stringify ( mockSettings ) )
594+ mockProviderSettingsManager . export . mockResolvedValue ( {
595+ currentApiConfigName : "default" ,
596+ apiConfigs : { } ,
597+ } )
598+ mockProviderSettingsManager . listConfig . mockResolvedValue ( [ ] )
599+
600+ const result = await importSettings ( {
601+ providerSettingsManager : mockProviderSettingsManager ,
602+ contextProxy : mockContextProxy ,
603+ customModesManager : mockCustomModesManager ,
604+ } )
605+
606+ expect ( vscode . window . showOpenDialog ) . toHaveBeenCalledWith ( {
607+ filters : { JSON : [ "json" ] } ,
608+ canSelectMany : false ,
609+ } )
610+
611+ expect ( result . success ) . toBe ( true )
612+ } )
613+
614+ it ( "should clean up temp file even if import fails" , async ( ) => {
615+ ; ( vscode . window . showQuickPick as Mock )
616+ . mockResolvedValueOnce ( { value : "local" } )
617+ . mockResolvedValueOnce ( { value : "paste" } )
618+
619+ const invalidSettings = '{"invalid": "no provider profiles"}'
620+ ; ( vscode . window . showInputBox as Mock ) . mockResolvedValue ( invalidSettings )
621+ ; ( fs . writeFile as Mock ) . mockResolvedValue ( undefined )
622+ ; ( fs . unlink as Mock ) . mockResolvedValue ( undefined )
623+ ; ( fs . readFile as Mock ) . mockResolvedValue ( invalidSettings )
624+
625+ const result = await importSettings ( {
626+ providerSettingsManager : mockProviderSettingsManager ,
627+ contextProxy : mockContextProxy ,
628+ customModesManager : mockCustomModesManager ,
629+ } )
630+
631+ expect ( result . success ) . toBe ( false )
632+ expect ( fs . unlink ) . toHaveBeenCalledWith ( expect . stringContaining ( "roo-settings-import-" ) )
633+ } )
634+
635+ it ( "should handle file write errors when pasting content" , async ( ) => {
636+ ; ( vscode . window . showQuickPick as Mock )
637+ . mockResolvedValueOnce ( { value : "local" } )
638+ . mockResolvedValueOnce ( { value : "paste" } )
639+
640+ const mockSettings = {
641+ providerProfiles : {
642+ currentApiConfigName : "test" ,
643+ apiConfigs : { test : { apiProvider : "openai" as ProviderName , id : "test-id" } } ,
644+ } ,
645+ }
646+
647+ ; ( vscode . window . showInputBox as Mock ) . mockResolvedValue ( JSON . stringify ( mockSettings ) )
648+ ; ( fs . writeFile as Mock ) . mockRejectedValue ( new Error ( "Disk full" ) )
649+
650+ const result = await importSettings ( {
651+ providerSettingsManager : mockProviderSettingsManager ,
652+ contextProxy : mockContextProxy ,
653+ customModesManager : mockCustomModesManager ,
654+ } )
655+
656+ expect ( result ) . toEqual ( { success : false , error : "Failed to process settings: Error: Disk full" } )
657+ } )
658+ } )
659+
660+ describe ( "local environment" , ( ) => {
661+ beforeEach ( ( ) => {
662+ // Ensure we're in local environment
663+ delete ( vscode . env as any ) . remoteName
664+ } )
665+
666+ it ( "should use standard dialog in local environment" , async ( ) => {
667+ ; ( vscode . window . showOpenDialog as Mock ) . mockResolvedValue ( [ { fsPath : "/local/path/settings.json" } ] )
668+
669+ const mockSettings = {
670+ providerProfiles : {
671+ currentApiConfigName : "test" ,
672+ apiConfigs : { test : { apiProvider : "openai" as ProviderName , id : "test-id" } } ,
673+ } ,
674+ }
675+
676+ ; ( fs . readFile as Mock ) . mockResolvedValue ( JSON . stringify ( mockSettings ) )
677+ mockProviderSettingsManager . export . mockResolvedValue ( {
678+ currentApiConfigName : "default" ,
679+ apiConfigs : { } ,
680+ } )
681+ mockProviderSettingsManager . listConfig . mockResolvedValue ( [ ] )
682+
683+ const result = await importSettings ( {
684+ providerSettingsManager : mockProviderSettingsManager ,
685+ contextProxy : mockContextProxy ,
686+ customModesManager : mockCustomModesManager ,
687+ } )
688+
689+ // Should NOT show quick pick in local environment
690+ expect ( vscode . window . showQuickPick ) . not . toHaveBeenCalled ( )
691+
692+ // Should use standard dialog
693+ expect ( vscode . window . showOpenDialog ) . toHaveBeenCalledWith ( {
694+ filters : { JSON : [ "json" ] } ,
695+ canSelectMany : false ,
696+ } )
697+
698+ expect ( result . success ) . toBe ( true )
699+ } )
700+ } )
439701 } )
440702
441703 describe ( "exportSettings" , ( ) => {
0 commit comments