Skip to content

Commit d191542

Browse files
committed
fix: migrate venv paths for cross-editor compatibility and legacy naming
1 parent 627fc5d commit d191542

File tree

4 files changed

+224
-3
lines changed

4 files changed

+224
-3
lines changed

src/kernels/deepnote/deepnoteToolkitInstaller.node.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
8282
const venvKey = venvPath.fsPath;
8383

8484
logger.info(`Ensuring virtual environment at ${venvKey}`);
85+
logger.info(`Base interpreter: ${baseInterpreter.uri.fsPath}`);
86+
87+
// Validate that venv path is in current globalStorage (not from a different editor like VS Code)
88+
const expectedStoragePrefix = this.context.globalStorageUri.fsPath;
89+
if (!venvKey.startsWith(expectedStoragePrefix)) {
90+
const error = new Error(
91+
`Venv path mismatch! Expected venv under ${expectedStoragePrefix} but got ${venvKey}. ` +
92+
`This might happen if the notebook was previously used in a different editor (VS Code vs Cursor).`
93+
);
94+
logger.error(error.message);
95+
throw error;
96+
}
8597

8698
// Wait for any pending installation for this venv to complete
8799
const pendingInstall = this.pendingInstallations.get(venvKey);

src/kernels/deepnote/environments/deepnoteEnvironmentManager.node.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { inject, injectable, named } from 'inversify';
2+
import * as path from '../../../platform/vscode-path/path';
23
import { CancellationToken, EventEmitter, l10n, Uri } from 'vscode';
34
import { IExtensionSyncActivationService } from '../../../platform/activation/types';
45
import { Cancellation } from '../../../platform/common/cancellation';
@@ -52,10 +53,41 @@ export class DeepnoteEnvironmentManager implements IExtensionSyncActivationServi
5253
const configs = await this.storage.loadEnvironments();
5354
this.environments.clear();
5455

56+
let needsMigration = false;
57+
5558
for (const config of configs) {
59+
const venvDirName = path.basename(config.venvPath.fsPath);
60+
61+
// Check if venv path is under current globalStorage
62+
const expectedVenvParent = Uri.joinPath(this.context.globalStorageUri, 'deepnote-venvs').fsPath;
63+
const actualVenvParent = path.dirname(config.venvPath.fsPath);
64+
const isInCorrectStorage = actualVenvParent === expectedVenvParent;
65+
66+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
67+
const isHashBasedName = !uuidPattern.test(venvDirName);
68+
69+
const needsPathMigration = isHashBasedName || !isInCorrectStorage;
70+
71+
if (needsPathMigration) {
72+
logger.info(
73+
`Migrating environment "${config.name}" from old venv path ${config.venvPath.fsPath} to UUID-based path`
74+
);
75+
76+
config.venvPath = Uri.joinPath(this.context.globalStorageUri, 'deepnote-venvs', config.id);
77+
config.toolkitVersion = undefined;
78+
79+
logger.info(`New venv path: ${config.venvPath.fsPath} (will be recreated on next use)`);
80+
needsMigration = true;
81+
}
82+
5683
this.environments.set(config.id, config);
5784
}
5885

86+
if (needsMigration) {
87+
logger.info('Saving migrated environments to storage');
88+
await this.persistEnvironments();
89+
}
90+
5991
logger.info(`Initialized environment manager with ${this.environments.size} environments`);
6092

6193
// Fire event to notify tree view of loaded environments

src/kernels/deepnote/environments/deepnoteEnvironmentManager.unit.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,4 +266,157 @@ 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, /Cursor.*deepnote-venvs\/cursor-env-id$/);
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 UUID 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 migrate multiple environments at once', async () => {
376+
const configs = [
377+
{
378+
id: 'uuid1',
379+
name: 'Hash Config',
380+
pythonInterpreter: testInterpreter,
381+
venvPath: Uri.file('/global/storage/deepnote-venvs/venv_abc123-1.0.0'),
382+
createdAt: new Date(),
383+
lastUsedAt: new Date()
384+
},
385+
{
386+
id: 'uuid2',
387+
name: 'VS Code Config',
388+
pythonInterpreter: testInterpreter,
389+
venvPath: Uri.file('/Code/globalStorage/deepnote-venvs/uuid2'),
390+
createdAt: new Date(),
391+
lastUsedAt: new Date()
392+
},
393+
{
394+
id: 'uuid3',
395+
name: 'Correct Config',
396+
pythonInterpreter: testInterpreter,
397+
venvPath: Uri.file('/global/storage/deepnote-venvs/uuid3'),
398+
createdAt: new Date(),
399+
lastUsedAt: new Date()
400+
}
401+
];
402+
403+
when(mockStorage.loadEnvironments()).thenResolve(configs);
404+
when(mockStorage.saveEnvironments(anything())).thenResolve();
405+
when(mockContext.globalStorageUri).thenReturn(Uri.file('/global/storage'));
406+
407+
manager.activate();
408+
await manager.waitForInitialization();
409+
410+
const loaded = manager.listEnvironments();
411+
assert.strictEqual(loaded.length, 3);
412+
413+
// First two should be migrated
414+
assert.strictEqual(loaded[0].venvPath.fsPath, '/global/storage/deepnote-venvs/uuid1');
415+
assert.strictEqual(loaded[1].venvPath.fsPath, '/global/storage/deepnote-venvs/uuid2');
416+
// Third should remain unchanged
417+
assert.strictEqual(loaded[2].venvPath.fsPath, '/global/storage/deepnote-venvs/uuid3');
418+
419+
verify(mockStorage.saveEnvironments(anything())).once();
420+
});
421+
});
269422
});

src/notebooks/deepnote/deepnoteKernelAutoSelector.node.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,9 +508,30 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector,
508508
const existingEnvironmentId = this.notebookEnvironmentsIds.get(notebookKey);
509509

510510
if (existingEnvironmentId != null && existingController != null && existingEnvironmentId === configuration.id) {
511-
logger.info(`Existing controller found for notebook ${getDisplayPath(notebook.uri)}, selecting it`);
512-
await this.ensureControllerSelectedForNotebook(notebook, existingController, progressToken);
513-
return;
511+
logger.info(`Existing controller found for notebook ${getDisplayPath(notebook.uri)}, verifying connection`);
512+
513+
// Verify the controller's interpreter path matches the expected venv path
514+
// This handles cases where notebooks were used in VS Code and now opened in Cursor
515+
const existingInterpreter = existingController.connection.interpreter;
516+
if (existingInterpreter) {
517+
const expectedInterpreter =
518+
process.platform === 'win32'
519+
? Uri.joinPath(configuration.venvPath, 'Scripts', 'python.exe')
520+
: Uri.joinPath(configuration.venvPath, 'bin', 'python');
521+
522+
if (existingInterpreter.uri.fsPath !== expectedInterpreter.fsPath) {
523+
logger.warn(
524+
`Controller interpreter path mismatch! Expected: ${expectedInterpreter.fsPath}, Got: ${existingInterpreter.uri.fsPath}. Recreating controller.`
525+
);
526+
// Dispose old controller and recreate it
527+
existingController.dispose();
528+
this.notebookControllers.delete(notebookKey);
529+
} else {
530+
logger.info(`Controller verified, selecting it`);
531+
await this.ensureControllerSelectedForNotebook(notebook, existingController, progressToken);
532+
return;
533+
}
534+
}
514535
}
515536

516537
// Ensure server is running (startServer is idempotent - returns early if already running)
@@ -582,6 +603,9 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector,
582603
? Uri.joinPath(configuration.venvPath, 'Scripts', 'python.exe')
583604
: Uri.joinPath(configuration.venvPath, 'bin', 'python');
584605

606+
logger.info(`Using venv path: ${configuration.venvPath.fsPath}`);
607+
logger.info(`Venv interpreter path: ${venvInterpreter.fsPath}`);
608+
585609
// CRITICAL: Use unique notebook-based ID (includes query with notebook ID)
586610
// This ensures each notebook gets its own controller/kernel, even within the same project.
587611
// When switching environments, addOrUpdate will call updateConnection() on the existing

0 commit comments

Comments
 (0)