@@ -266,4 +266,189 @@ suite('DeepnoteEnvironmentManager', () => {
266266 // Should not throw
267267 } ) ;
268268 } ) ;
269+
270+ suite ( 'environment migration' , ( ) => {
271+ test ( 'should migrate hash-based venv paths to UUID-based paths' , async ( ) => {
272+ const oldHashBasedConfig = {
273+ id : 'abcd1234-5678-90ab-cdef-123456789012' ,
274+ name : 'Old Hash Config' ,
275+ pythonInterpreter : testInterpreter ,
276+ venvPath : Uri . file ( '/global/storage/deepnote-venvs/venv_7626587d-1.0.0' ) ,
277+ createdAt : new Date ( ) ,
278+ lastUsedAt : new Date ( )
279+ } ;
280+
281+ when ( mockStorage . loadEnvironments ( ) ) . thenResolve ( [ oldHashBasedConfig ] ) ;
282+ when ( mockStorage . saveEnvironments ( anything ( ) ) ) . thenResolve ( ) ;
283+ when ( mockContext . globalStorageUri ) . thenReturn ( Uri . file ( '/global/storage' ) ) ;
284+
285+ manager . activate ( ) ;
286+ await manager . waitForInitialization ( ) ;
287+
288+ const configs = manager . listEnvironments ( ) ;
289+ assert . strictEqual ( configs . length , 1 ) ;
290+
291+ // Should have migrated to UUID-based path
292+ assert . strictEqual (
293+ configs [ 0 ] . venvPath . fsPath ,
294+ '/global/storage/deepnote-venvs/abcd1234-5678-90ab-cdef-123456789012'
295+ ) ;
296+
297+ // Should clear toolkit version to force reinstallation
298+ assert . isUndefined ( configs [ 0 ] . toolkitVersion ) ;
299+
300+ // Should have saved the migration
301+ verify ( mockStorage . saveEnvironments ( anything ( ) ) ) . once ( ) ;
302+ } ) ;
303+
304+ test ( 'should migrate VS Code storage paths to Cursor storage paths' , async ( ) => {
305+ const vsCodeConfig = {
306+ id : 'cursor-env-id' ,
307+ name : 'VS Code Environment' ,
308+ pythonInterpreter : testInterpreter ,
309+ venvPath : Uri . file (
310+ '/Library/Application Support/Code/User/globalStorage/deepnote.vscode-deepnote/deepnote-venvs/cursor-env-id'
311+ ) ,
312+ createdAt : new Date ( ) ,
313+ lastUsedAt : new Date ( ) ,
314+ toolkitVersion : '1.0.0'
315+ } ;
316+
317+ when ( mockStorage . loadEnvironments ( ) ) . thenResolve ( [ vsCodeConfig ] ) ;
318+ when ( mockStorage . saveEnvironments ( anything ( ) ) ) . thenResolve ( ) ;
319+ when ( mockContext . globalStorageUri ) . thenReturn (
320+ Uri . file ( '/Library/Application Support/Cursor/User/globalStorage/deepnote.vscode-deepnote' )
321+ ) ;
322+
323+ manager . activate ( ) ;
324+ await manager . waitForInitialization ( ) ;
325+
326+ const configs = manager . listEnvironments ( ) ;
327+ assert . strictEqual ( configs . length , 1 ) ;
328+
329+ // Should have migrated to Cursor storage
330+ assert . match ( configs [ 0 ] . venvPath . fsPath , / C u r s o r .* d e e p n o t e - v e n v s \/ c u r s o r - e n v - i d $ / ) ;
331+
332+ // Should clear toolkit version to force reinstallation
333+ assert . isUndefined ( configs [ 0 ] . toolkitVersion ) ;
334+
335+ verify ( mockStorage . saveEnvironments ( anything ( ) ) ) . once ( ) ;
336+ } ) ;
337+
338+ test ( 'should not migrate environments with correct ID-based paths in correct storage' , async ( ) => {
339+ const testDate = new Date ( ) ;
340+ const correctConfig = {
341+ id : '12345678-1234-1234-1234-123456789abc' ,
342+ name : 'Correct Config' ,
343+ pythonInterpreter : testInterpreter ,
344+ venvPath : Uri . file ( '/global/storage/deepnote-venvs/12345678-1234-1234-1234-123456789abc' ) ,
345+ createdAt : testDate ,
346+ lastUsedAt : testDate ,
347+ toolkitVersion : '1.0.0' ,
348+ packages : [ ]
349+ } ;
350+
351+ when ( mockStorage . loadEnvironments ( ) ) . thenResolve ( [ correctConfig ] ) ;
352+ when ( mockStorage . saveEnvironments ( anything ( ) ) ) . thenResolve ( ) ;
353+ when ( mockContext . globalStorageUri ) . thenReturn ( Uri . file ( '/global/storage' ) ) ;
354+
355+ manager . activate ( ) ;
356+ await manager . waitForInitialization ( ) ;
357+
358+ const configs = manager . listEnvironments ( ) ;
359+ assert . strictEqual ( configs . length , 1 ) ;
360+
361+ // Path should remain unchanged
362+ assert . strictEqual (
363+ configs [ 0 ] . venvPath . fsPath ,
364+ '/global/storage/deepnote-venvs/12345678-1234-1234-1234-123456789abc'
365+ ) ;
366+
367+ // ID and name should be preserved
368+ assert . strictEqual ( configs [ 0 ] . id , '12345678-1234-1234-1234-123456789abc' ) ;
369+ assert . strictEqual ( configs [ 0 ] . name , 'Correct Config' ) ;
370+
371+ // Should NOT have saved (no migration needed)
372+ verify ( mockStorage . saveEnvironments ( anything ( ) ) ) . never ( ) ;
373+ } ) ;
374+
375+ test ( 'should not migrate environments with non-UUID IDs when path already matches' , async ( ) => {
376+ const testDate = new Date ( ) ;
377+ const customIdConfig = {
378+ id : 'my-custom-env-id' ,
379+ name : 'Custom ID Environment' ,
380+ pythonInterpreter : testInterpreter ,
381+ venvPath : Uri . file ( '/global/storage/deepnote-venvs/my-custom-env-id' ) ,
382+ createdAt : testDate ,
383+ lastUsedAt : testDate ,
384+ toolkitVersion : '1.0.0'
385+ } ;
386+
387+ when ( mockStorage . loadEnvironments ( ) ) . thenResolve ( [ customIdConfig ] ) ;
388+ when ( mockStorage . saveEnvironments ( anything ( ) ) ) . thenResolve ( ) ;
389+ when ( mockContext . globalStorageUri ) . thenReturn ( Uri . file ( '/global/storage' ) ) ;
390+
391+ manager . activate ( ) ;
392+ await manager . waitForInitialization ( ) ;
393+
394+ const configs = manager . listEnvironments ( ) ;
395+ assert . strictEqual ( configs . length , 1 ) ;
396+
397+ // Path should remain unchanged
398+ assert . strictEqual ( configs [ 0 ] . venvPath . fsPath , '/global/storage/deepnote-venvs/my-custom-env-id' ) ;
399+
400+ // Toolkit version should NOT be cleared
401+ assert . strictEqual ( configs [ 0 ] . toolkitVersion , '1.0.0' ) ;
402+
403+ // Should NOT have saved (no migration needed)
404+ verify ( mockStorage . saveEnvironments ( anything ( ) ) ) . never ( ) ;
405+ } ) ;
406+
407+ test ( 'should migrate multiple environments at once' , async ( ) => {
408+ const configs = [
409+ {
410+ id : 'uuid1' ,
411+ name : 'Hash Config' ,
412+ pythonInterpreter : testInterpreter ,
413+ venvPath : Uri . file ( '/global/storage/deepnote-venvs/venv_abc123-1.0.0' ) ,
414+ createdAt : new Date ( ) ,
415+ lastUsedAt : new Date ( )
416+ } ,
417+ {
418+ id : 'uuid2' ,
419+ name : 'VS Code Config' ,
420+ pythonInterpreter : testInterpreter ,
421+ venvPath : Uri . file ( '/Code/globalStorage/deepnote-venvs/uuid2' ) ,
422+ createdAt : new Date ( ) ,
423+ lastUsedAt : new Date ( )
424+ } ,
425+ {
426+ id : 'uuid3' ,
427+ name : 'Correct Config' ,
428+ pythonInterpreter : testInterpreter ,
429+ venvPath : Uri . file ( '/global/storage/deepnote-venvs/uuid3' ) ,
430+ createdAt : new Date ( ) ,
431+ lastUsedAt : new Date ( )
432+ }
433+ ] ;
434+
435+ when ( mockStorage . loadEnvironments ( ) ) . thenResolve ( configs ) ;
436+ when ( mockStorage . saveEnvironments ( anything ( ) ) ) . thenResolve ( ) ;
437+ when ( mockContext . globalStorageUri ) . thenReturn ( Uri . file ( '/global/storage' ) ) ;
438+
439+ manager . activate ( ) ;
440+ await manager . waitForInitialization ( ) ;
441+
442+ const loaded = manager . listEnvironments ( ) ;
443+ assert . strictEqual ( loaded . length , 3 ) ;
444+
445+ // First two should be migrated
446+ assert . strictEqual ( loaded [ 0 ] . venvPath . fsPath , '/global/storage/deepnote-venvs/uuid1' ) ;
447+ assert . strictEqual ( loaded [ 1 ] . venvPath . fsPath , '/global/storage/deepnote-venvs/uuid2' ) ;
448+ // Third should remain unchanged
449+ assert . strictEqual ( loaded [ 2 ] . venvPath . fsPath , '/global/storage/deepnote-venvs/uuid3' ) ;
450+
451+ verify ( mockStorage . saveEnvironments ( anything ( ) ) ) . once ( ) ;
452+ } ) ;
453+ } ) ;
269454} ) ;
0 commit comments