@@ -7,7 +7,13 @@ import { migrateSettings } from "../utils/migrateSettings"
77
88// Mock dependencies
99jest . mock ( "vscode" )
10- jest . mock ( "fs/promises" )
10+ jest . mock ( "fs/promises" , ( ) => ( {
11+ mkdir : jest . fn ( ) . mockResolvedValue ( undefined ) ,
12+ readFile : jest . fn ( ) ,
13+ writeFile : jest . fn ( ) . mockResolvedValue ( undefined ) ,
14+ rename : jest . fn ( ) . mockResolvedValue ( undefined ) ,
15+ unlink : jest . fn ( ) . mockResolvedValue ( undefined ) ,
16+ } ) )
1117jest . mock ( "fs" )
1218jest . mock ( "../utils/fs" )
1319
@@ -18,11 +24,12 @@ describe("Settings Migration", () => {
1824 const mockSettingsDir = path . join ( mockStoragePath , "settings" )
1925
2026 // Legacy file names
21- const legacyCustomModesPath = path . join ( mockSettingsDir , "cline_custom_modes.json" )
27+ const legacyCustomModesJson = path . join ( mockSettingsDir , "custom_modes.json" )
28+ const legacyClineCustomModesPath = path . join ( mockSettingsDir , "cline_custom_modes.json" )
2229 const legacyMcpSettingsPath = path . join ( mockSettingsDir , "cline_mcp_settings.json" )
2330
2431 // New file names
25- const newCustomModesPath = path . join ( mockSettingsDir , GlobalFileNames . customModes )
32+ const newCustomModesYaml = path . join ( mockSettingsDir , GlobalFileNames . customModes )
2633 const newMcpSettingsPath = path . join ( mockSettingsDir , GlobalFileNames . mcpSettings )
2734
2835 beforeEach ( ( ) => {
@@ -43,66 +50,83 @@ describe("Settings Migration", () => {
4350 globalStorageUri : { fsPath : mockStoragePath } ,
4451 } as unknown as vscode . ExtensionContext
4552
46- // The fs/promises mock is already set up in src/__mocks__/fs/promises.ts
47- // We don't need to manually mock these methods
48-
4953 // Set global outputChannel for all tests
5054 ; ( global as any ) . outputChannel = mockOutputChannel
5155 } )
5256
5357 it ( "should migrate custom modes file if old file exists and new file doesn't" , async ( ) => {
54- // Mock file existence checks
58+ // Clear all previous mocks to ensure clean test environment
59+ jest . clearAllMocks ( )
60+
61+ // Setup mock for rename function
62+ const mockRename = ( fs . rename as jest . Mock ) . mockResolvedValue ( undefined )
63+
64+ // Mock file existence checks - only return true for paths we want to exist
5565 ; ( fileExistsAtPath as jest . Mock ) . mockImplementation ( async ( path : string ) => {
5666 if ( path === mockSettingsDir ) return true
57- if ( path === legacyCustomModesPath ) return true
58- if ( path === newCustomModesPath ) return false
59- return false
67+ if ( path === legacyClineCustomModesPath ) return true
68+ return false // All other paths don't exist, including destination files
6069 } )
6170
71+ // Run the migration
6272 await migrateSettings ( mockContext , mockOutputChannel )
6373
64- // Verify file was renamed
65- expect ( fs . rename ) . toHaveBeenCalledWith ( legacyCustomModesPath , newCustomModesPath )
74+ // Verify expected rename call - cline_custom_modes.json should be renamed to custom_modes.json
75+ expect ( mockRename ) . toHaveBeenCalledWith ( legacyClineCustomModesPath , legacyCustomModesJson )
6676 } )
6777
6878 it ( "should migrate MCP settings file if old file exists and new file doesn't" , async ( ) => {
69- // Mock file existence checks
79+ // Clear all previous mocks to ensure clean test environment
80+ jest . clearAllMocks ( )
81+
82+ // Setup mock for rename function
83+ const mockRename = ( fs . rename as jest . Mock ) . mockResolvedValue ( undefined )
84+
85+ // Ensure the other files don't interfere with this test
7086 ; ( fileExistsAtPath as jest . Mock ) . mockImplementation ( async ( path : string ) => {
7187 if ( path === mockSettingsDir ) return true
7288 if ( path === legacyMcpSettingsPath ) return true
73- if ( path === newMcpSettingsPath ) return false
74- return false
89+ if ( path === legacyClineCustomModesPath ) return false // Ensure this file doesn't exist
90+ if ( path === legacyCustomModesJson ) return false // Ensure this file doesn't exist
91+ return false // All other paths don't exist, including destination files
7592 } )
7693
94+ // Run the migration
7795 await migrateSettings ( mockContext , mockOutputChannel )
7896
79- // Verify file was renamed
80- expect ( fs . rename ) . toHaveBeenCalledWith ( legacyMcpSettingsPath , newMcpSettingsPath )
97+ // Verify expected rename call
98+ expect ( mockRename ) . toHaveBeenCalledWith ( legacyMcpSettingsPath , newMcpSettingsPath )
8199 } )
82100
83101 it ( "should not migrate if new file already exists" , async ( ) => {
84- // Mock file existence checks
102+ // Clear all previous mocks to ensure clean test environment
103+ jest . clearAllMocks ( )
104+
105+ // Setup mock for rename function
106+ const mockRename = ( fs . rename as jest . Mock ) . mockResolvedValue ( undefined )
107+
108+ // Mock file existence checks - both source and destination exist
85109 ; ( fileExistsAtPath as jest . Mock ) . mockImplementation ( async ( path : string ) => {
86110 if ( path === mockSettingsDir ) return true
87- if ( path === legacyCustomModesPath ) return true
88- if ( path === newCustomModesPath ) return true
111+ if ( path === legacyClineCustomModesPath ) return true
112+ if ( path === legacyCustomModesJson ) return true // Destination already exists
89113 if ( path === legacyMcpSettingsPath ) return true
90114 if ( path === newMcpSettingsPath ) return true
91115 return false
92116 } )
93117
94118 await migrateSettings ( mockContext , mockOutputChannel )
95119
96- // Verify no files were renamed
97- expect ( fs . rename ) . not . toHaveBeenCalled ( )
120+ // Verify rename was not called since destination files exist
121+ expect ( mockRename ) . not . toHaveBeenCalled ( )
98122 } )
99123
100124 it ( "should handle errors gracefully" , async ( ) => {
101- // Mock file existence checks to throw an error
102- ; ( fileExistsAtPath as jest . Mock ) . mockRejectedValue ( new Error ( "Test error" ) )
125+ // Clear mocks
126+ jest . clearAllMocks ( )
103127
104- // Set the global outputChannel for the test
105- ; ( global as any ) . outputChannel = mockOutputChannel
128+ // Mock file existence to throw error
129+ ; ( fileExistsAtPath as jest . Mock ) . mockRejectedValue ( new Error ( "Test error" ) )
106130
107131 await migrateSettings ( mockContext , mockOutputChannel )
108132
@@ -111,4 +135,119 @@ describe("Settings Migration", () => {
111135 expect . stringContaining ( "Error migrating settings files" ) ,
112136 )
113137 } )
138+
139+ it ( "should convert custom_modes.json to YAML format" , async ( ) => {
140+ // Clear all previous mocks to ensure clean test environment
141+ jest . clearAllMocks ( )
142+
143+ const testJsonContent = JSON . stringify ( { customModes : [ { slug : "test-mode" , name : "Test Mode" } ] } )
144+
145+ // Setup mock functions
146+ const mockWrite = ( fs . writeFile as jest . Mock ) . mockResolvedValue ( undefined )
147+ const mockUnlink = ( fs . unlink as jest . Mock ) . mockResolvedValue ( undefined )
148+
149+ // Mock file read to return JSON content
150+ ; ( fs . readFile as jest . Mock ) . mockImplementation ( async ( path : any ) => {
151+ if ( path === legacyCustomModesJson ) {
152+ return testJsonContent
153+ }
154+ throw new Error ( "File not found: " + path )
155+ } )
156+
157+ // Isolate this test by making sure only the specific JSON file exists
158+ ; ( fileExistsAtPath as jest . Mock ) . mockImplementation ( async ( path : string ) => {
159+ if ( path === mockSettingsDir ) return true
160+ if ( path === legacyCustomModesJson ) return true
161+ if ( path === legacyClineCustomModesPath ) return false
162+ if ( path === legacyMcpSettingsPath ) return false
163+ return false
164+ } )
165+
166+ await migrateSettings ( mockContext , mockOutputChannel )
167+
168+ // Verify file operations
169+ expect ( mockWrite ) . toHaveBeenCalledWith ( newCustomModesYaml , expect . any ( String ) , "utf-8" )
170+ // We don't delete the original JSON file to allow for rollback
171+ expect ( mockUnlink ) . not . toHaveBeenCalled ( )
172+
173+ // Verify log message mentions preservation of original file
174+ expect ( mockOutputChannel . appendLine ) . toHaveBeenCalledWith (
175+ expect . stringContaining ( "original JSON file preserved for rollback purposes" ) ,
176+ )
177+ } )
178+
179+ it ( "should handle corrupt JSON gracefully" , async ( ) => {
180+ // Clear all previous mocks to ensure clean test environment
181+ jest . clearAllMocks ( )
182+
183+ // Setup mock functions
184+ const mockWrite = ( fs . writeFile as jest . Mock ) . mockResolvedValue ( undefined )
185+ const mockUnlink = ( fs . unlink as jest . Mock ) . mockResolvedValue ( undefined )
186+
187+ // Mock file read to return corrupt JSON
188+ ; ( fs . readFile as jest . Mock ) . mockImplementation ( async ( path : any ) => {
189+ if ( path === legacyCustomModesJson ) {
190+ return "{ invalid json content" // This will cause an error when parsed
191+ }
192+ throw new Error ( "File not found: " + path )
193+ } )
194+
195+ // Isolate this test
196+ ; ( fileExistsAtPath as jest . Mock ) . mockImplementation ( async ( path : string ) => {
197+ if ( path === mockSettingsDir ) return true
198+ if ( path === legacyCustomModesJson ) return true
199+ if ( path === legacyClineCustomModesPath ) return false
200+ if ( path === legacyMcpSettingsPath ) return false
201+ return false
202+ } )
203+
204+ await migrateSettings ( mockContext , mockOutputChannel )
205+
206+ // Verify error was logged
207+ expect ( mockOutputChannel . appendLine ) . toHaveBeenCalledWith (
208+ expect . stringContaining ( "Error parsing custom_modes.json" ) ,
209+ )
210+
211+ // Verify no write/unlink operations were performed
212+ expect ( mockWrite ) . not . toHaveBeenCalled ( )
213+ expect ( mockUnlink ) . not . toHaveBeenCalled ( )
214+ } )
215+
216+ it ( "should skip migration when YAML file already exists" , async ( ) => {
217+ // Clear all previous mocks to ensure clean test environment
218+ jest . clearAllMocks ( )
219+
220+ // Setup mock functions
221+ const mockWrite = ( fs . writeFile as jest . Mock ) . mockResolvedValue ( undefined )
222+ const mockUnlink = ( fs . unlink as jest . Mock ) . mockResolvedValue ( undefined )
223+
224+ // Mock file read
225+ ; ( fs . readFile as jest . Mock ) . mockImplementation ( async ( path : any ) => {
226+ if ( path === legacyCustomModesJson ) {
227+ return JSON . stringify ( { customModes : [ ] } )
228+ }
229+ throw new Error ( "File not found: " + path )
230+ } )
231+
232+ // Mock file existence checks - both source and yaml destination exist
233+ ; ( fileExistsAtPath as jest . Mock ) . mockImplementation ( async ( path : string ) => {
234+ if ( path === mockSettingsDir ) return true
235+ if ( path === legacyCustomModesJson ) return true
236+ if ( path === newCustomModesYaml ) return true // YAML already exists
237+ if ( path === legacyClineCustomModesPath ) return false
238+ if ( path === legacyMcpSettingsPath ) return false
239+ return false
240+ } )
241+
242+ await migrateSettings ( mockContext , mockOutputChannel )
243+
244+ // Verify skip message was logged
245+ expect ( mockOutputChannel . appendLine ) . toHaveBeenCalledWith (
246+ "custom_modes.yaml already exists, skipping migration" ,
247+ )
248+
249+ // Verify no file operations occurred
250+ expect ( mockWrite ) . not . toHaveBeenCalled ( )
251+ expect ( mockUnlink ) . not . toHaveBeenCalled ( )
252+ } )
114253} )
0 commit comments