From b7a1305ea5ac7a6eeb515128526f199a8a43fae8 Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Tue, 14 Apr 2026 14:50:06 -0700 Subject: [PATCH] fix conda event firing --- src/managers/conda/condaEnvManager.ts | 9 ++ .../condaEnvManager.setEvents.unit.test.ts | 149 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/test/managers/conda/condaEnvManager.setEvents.unit.test.ts diff --git a/src/managers/conda/condaEnvManager.ts b/src/managers/conda/condaEnvManager.ts index 1db198b4..1654bc89 100644 --- a/src/managers/conda/condaEnvManager.ts +++ b/src/managers/conda/condaEnvManager.ts @@ -311,7 +311,12 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { : undefined; if (scope === undefined) { + const before = this.globalEnv; + this.globalEnv = checkedEnv; await setCondaForGlobal(checkedEnv?.environmentPath?.fsPath); + if (before?.envId.id !== checkedEnv?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: undefined, old: before, new: checkedEnv }); + } } else if (scope instanceof Uri) { const folder = this.api.getPythonProject(scope); const fsPath = folder?.uri?.fsPath ?? scope.fsPath; @@ -327,12 +332,16 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { } const normalizedFsPath = normalizePath(fsPath); + const before = this.fsPathToEnv.get(normalizedFsPath); if (checkedEnv) { this.fsPathToEnv.set(normalizedFsPath, checkedEnv); } else { this.fsPathToEnv.delete(normalizedFsPath); } await setCondaForWorkspace(fsPath, checkedEnv?.environmentPath.fsPath); + if (before?.envId.id !== checkedEnv?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: scope, old: before, new: checkedEnv }); + } } } else if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) { const projects: PythonProject[] = []; diff --git a/src/test/managers/conda/condaEnvManager.setEvents.unit.test.ts b/src/test/managers/conda/condaEnvManager.setEvents.unit.test.ts new file mode 100644 index 00000000..ddc2e37a --- /dev/null +++ b/src/test/managers/conda/condaEnvManager.setEvents.unit.test.ts @@ -0,0 +1,149 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { DidChangeEnvironmentEventArgs, PythonEnvironment, PythonEnvironmentApi, PythonProject } from '../../../api'; +import { normalizePath } from '../../../common/utils/pathUtils'; +import { PythonEnvironmentImpl } from '../../../internal.api'; +import { CondaEnvManager } from '../../../managers/conda/condaEnvManager'; +import * as condaUtils from '../../../managers/conda/condaUtils'; +import { NativePythonFinder } from '../../../managers/common/nativePythonFinder'; + +function makeEnv(name: string, envPath: string, version: string = '3.12.0'): PythonEnvironment { + return new PythonEnvironmentImpl( + { id: `${name}-test`, managerId: 'ms-python.python:conda' }, + { + name, + displayName: `${name} (${version})`, + displayPath: envPath, + version, + environmentPath: Uri.file(envPath), + sysPrefix: envPath, + execInfo: { + run: { executable: 'python' }, + }, + }, + ); +} + +function createManager(apiOverrides?: Partial): CondaEnvManager { + const api = { + getPythonProject: sinon.stub().returns(undefined), + ...apiOverrides, + } as any as PythonEnvironmentApi; + const manager = new CondaEnvManager( + {} as NativePythonFinder, + api, + { info: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } as any, + ); + (manager as any)._initialized = { completed: true, promise: Promise.resolve() }; + (manager as any).collection = []; + return manager; +} + +suite('CondaEnvManager.set - onDidChangeEnvironment event firing', () => { + let checkNoPythonStub: sinon.SinonStub; + + setup(() => { + sinon.stub(condaUtils, 'setCondaForGlobal').resolves(); + sinon.stub(condaUtils, 'setCondaForWorkspace').resolves(); + checkNoPythonStub = sinon.stub(condaUtils, 'checkForNoPythonCondaEnvironment'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('set(undefined, env) fires onDidChangeEnvironment for global scope', async () => { + const manager = createManager(); + const oldEnv = makeEnv('base', '/miniconda3', '3.11.0'); + const newEnv = makeEnv('myenv', '/miniconda3/envs/myenv', '3.12.0'); + (manager as any).globalEnv = oldEnv; + checkNoPythonStub.resolves(newEnv); + + const events: DidChangeEnvironmentEventArgs[] = []; + manager.onDidChangeEnvironment((e) => events.push(e)); + + await manager.set(undefined, newEnv); + + assert.strictEqual(events.length, 1, 'should fire exactly one event'); + assert.strictEqual(events[0].uri, undefined, 'uri should be undefined for global scope'); + assert.strictEqual(events[0].old, oldEnv); + assert.strictEqual(events[0].new, newEnv); + }); + + test('set(undefined, env) does not fire event when env is unchanged', async () => { + const manager = createManager(); + const env = makeEnv('base', '/miniconda3', '3.11.0'); + (manager as any).globalEnv = env; + checkNoPythonStub.resolves(env); + + const events: DidChangeEnvironmentEventArgs[] = []; + manager.onDidChangeEnvironment((e) => events.push(e)); + + await manager.set(undefined, env); + + assert.strictEqual(events.length, 0, 'should not fire event when env is unchanged'); + }); + + test('set(Uri, env) fires onDidChangeEnvironment for single Uri scope', async () => { + const projectUri = Uri.file('/workspace/project'); + const project = { uri: projectUri, name: 'project' } as PythonProject; + const manager = createManager({ + getPythonProject: sinon.stub().returns(project) as any, + }); + const newEnv = makeEnv('myenv', '/miniconda3/envs/myenv', '3.12.0'); + checkNoPythonStub.resolves(newEnv); + + const events: DidChangeEnvironmentEventArgs[] = []; + manager.onDidChangeEnvironment((e) => events.push(e)); + + await manager.set(projectUri, newEnv); + + assert.strictEqual(events.length, 1, 'should fire exactly one event'); + assert.strictEqual(events[0].uri, projectUri); + assert.strictEqual(events[0].old, undefined); + assert.strictEqual(events[0].new, newEnv); + }); + + test('set(Uri, env) does not fire event when env is unchanged', async () => { + const projectUri = Uri.file('/workspace/project'); + const project = { uri: projectUri, name: 'project' } as PythonProject; + const manager = createManager({ + getPythonProject: sinon.stub().returns(project) as any, + }); + const env = makeEnv('myenv', '/miniconda3/envs/myenv', '3.12.0'); + checkNoPythonStub.resolves(env); + + // Pre-populate the map with the same env + (manager as any).fsPathToEnv.set(normalizePath(projectUri.fsPath), env); + + const events: DidChangeEnvironmentEventArgs[] = []; + manager.onDidChangeEnvironment((e) => events.push(e)); + + await manager.set(projectUri, env); + + assert.strictEqual(events.length, 0, 'should not fire event when env is unchanged'); + }); + + test('set(Uri, undefined) fires event when clearing environment', async () => { + const projectUri = Uri.file('/workspace/project'); + const project = { uri: projectUri, name: 'project' } as PythonProject; + const manager = createManager({ + getPythonProject: sinon.stub().returns(project) as any, + }); + const oldEnv = makeEnv('myenv', '/miniconda3/envs/myenv', '3.12.0'); + + // Pre-populate the map + (manager as any).fsPathToEnv.set(normalizePath(projectUri.fsPath), oldEnv); + + const events: DidChangeEnvironmentEventArgs[] = []; + manager.onDidChangeEnvironment((e) => events.push(e)); + + await manager.set(projectUri, undefined); + + assert.strictEqual(events.length, 1, 'should fire event when clearing'); + assert.strictEqual(events[0].old, oldEnv); + assert.strictEqual(events[0].new, undefined); + }); +});