From 1b87faa41565c49186fd431e2646eb7daf25c8c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:11:13 +0000 Subject: [PATCH 1/3] Initial plan From 4f04e7255eec71a7f33257e867fdfa4d765f0696 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:18:55 +0000 Subject: [PATCH 2/3] Lazy-register Conda manager; defer discovery until first use --- docs/startup-flow.md | 3 +- src/managers/conda/condaEnvManager.ts | 72 ++++++++++++++++++++++++++- src/managers/conda/main.ts | 55 ++++---------------- 3 files changed, 80 insertions(+), 50 deletions(-) diff --git a/docs/startup-flow.md b/docs/startup-flow.md index 29acf0e1..6c6bbc2b 100644 --- a/docs/startup-flow.md +++ b/docs/startup-flow.md @@ -21,8 +21,7 @@ python environments extension begins activation 1. sets up a JSON-RPC connection to it over stdin/stdout 2. register all built-in managers in parallel (Promise.all): - system: create SysPythonManager + VenvManager + PipPackageManager, register immediately (✅ NO PET call, sets up file watcher) - - conda: `getConda(nativeFinder)` checks settings → cache → persistent state → PATH - - pyenv & pipenv & poetry: create PyEnvManager, register immediately + - conda & pyenv & pipenv & poetry: create manager, register immediately - ✅ NO PET call — always registers unconditionally (lazy discovery) - shellStartupVars: initialize - all managers fire `onDidChangeEnvironmentManager` → ManagerReady resolves diff --git a/src/managers/conda/condaEnvManager.ts b/src/managers/conda/condaEnvManager.ts index 6eaeac91..8a2de74a 100644 --- a/src/managers/conda/condaEnvManager.ts +++ b/src/managers/conda/condaEnvManager.ts @@ -20,21 +20,29 @@ import { SetEnvironmentScope, } from '../../api'; import { CondaStrings } from '../../common/localize'; -import { traceError } from '../../common/logging'; +import { traceError, traceInfo } from '../../common/logging'; +import { StopWatch } from '../../common/stopWatch'; +import { EventNames } from '../../common/telemetry/constants'; +import { classifyError } from '../../common/telemetry/errorClassifier'; +import { sendTelemetryEvent } from '../../common/telemetry/sender'; import { createDeferred, Deferred } from '../../common/utils/deferred'; import { normalizePath } from '../../common/utils/pathUtils'; import { showErrorMessage, showInformationMessage, withProgress } from '../../common/window.apis'; +import { PythonProjectManager } from '../../internal.api'; import { getProjectFsPathForScope, tryFastPathGet } from '../common/fastPath'; import { NativePythonFinder } from '../common/nativePythonFinder'; -import { CondaSourcingStatus } from './condaSourcingUtils'; +import { notifyMissingManagerIfDefault } from '../common/utils'; +import { constructCondaSourcingStatus, CondaSourcingStatus } from './condaSourcingUtils'; import { checkForNoPythonCondaEnvironment, clearCondaCache, createCondaEnvironment, deleteCondaEnvironment, generateName, + getConda, getCondaForGlobal, getCondaForWorkspace, + getCondaPathSetting, getDefaultCondaPrefix, quickCreateConda, refreshCondaEnvs, @@ -61,6 +69,7 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { private readonly nativeFinder: NativePythonFinder, private readonly api: PythonEnvironmentApi, public readonly log: LogOutputChannel, + private readonly projectManager?: PythonProjectManager, ) { this.name = 'conda'; this.displayName = 'Conda'; @@ -87,8 +96,27 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { } this._initialized = createDeferred(); + const stopWatch = new StopWatch(); + let result: 'success' | 'tool_not_found' | 'error' = 'success'; + let envCount = 0; + let toolSource = 'none'; + let errorType: string | undefined; try { + // Check if tool is findable before PET refresh (settings/cache/persistent state/PATH only, no PET). + // Calling getConda() without a native finder skips the PET refresh path inside getCondaExecutable. + const hasExplicitSetting = !!getCondaPathSetting(); + let condaTool: string | undefined; + try { + condaTool = await getConda(); + } catch { + condaTool = undefined; + } + const preRefreshTool = condaTool; + if (preRefreshTool) { + toolSource = hasExplicitSetting ? 'settings' : 'local'; + } + await withProgress( { location: ProgressLocation.Window, @@ -104,7 +132,47 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { ); }, ); + + envCount = this.collection.length; + + // If tool wasn't found via local lookup, check if refresh discovered it via PET + // (refreshCondaEnvs persists the conda path it found from native data). + if (!preRefreshTool) { + try { + condaTool = await getConda(); + } catch { + condaTool = undefined; + } + toolSource = condaTool ? 'pet' : 'none'; + } + + if (toolSource === 'none') { + result = 'tool_not_found'; + traceInfo('Conda not found, conda features will be inactive.'); + if (this.projectManager) { + await notifyMissingManagerIfDefault('ms-python.python:conda', this.projectManager, this.api); + } + } else if (condaTool) { + // Conda was found — populate sourcing information for activation/shell support. + try { + this.sourcingInformation = await constructCondaSourcingStatus(condaTool); + traceInfo(this.sourcingInformation.toString()); + } catch (ex) { + traceError('Failed to construct conda sourcing status', ex); + } + } + } catch (ex) { + result = 'error'; + errorType = classifyError(ex); + traceError('Conda lazy initialization failed', ex); } finally { + sendTelemetryEvent(EventNames.MANAGER_LAZY_INIT, stopWatch.elapsedTime, { + managerName: 'conda', + result, + envCount, + toolSource, + errorType, + }); this._initialized.resolve(); } } diff --git a/src/managers/conda/main.ts b/src/managers/conda/main.ts index 00ce3d16..8e02b269 100644 --- a/src/managers/conda/main.ts +++ b/src/managers/conda/main.ts @@ -1,16 +1,11 @@ import { Disposable, LogOutputChannel } from 'vscode'; import { PythonEnvironmentApi } from '../../api'; import { traceInfo } from '../../common/logging'; -import { EventNames } from '../../common/telemetry/constants'; -import { sendTelemetryEvent } from '../../common/telemetry/sender'; import { getPythonApi } from '../../features/pythonApi'; import { PythonProjectManager } from '../../internal.api'; import { NativePythonFinder } from '../common/nativePythonFinder'; -import { notifyMissingManagerIfDefault } from '../common/utils'; import { CondaEnvManager } from './condaEnvManager'; import { CondaPackageManager } from './condaPackageManager'; -import { CondaSourcingStatus, constructCondaSourcingStatus } from './condaSourcingUtils'; -import { getConda } from './condaUtils'; export async function registerCondaFeatures( nativeFinder: NativePythonFinder, @@ -18,48 +13,16 @@ export async function registerCondaFeatures( log: LogOutputChannel, projectManager: PythonProjectManager, ): Promise { - let stage = 'getPythonApi'; const api: PythonEnvironmentApi = await getPythonApi(); - let condaPath: string | undefined; - try { - // get Conda will return only ONE conda manager, that correlates to a single conda install - stage = 'getConda'; - condaPath = await getConda(nativeFinder); - } catch (ex) { - traceInfo('Conda not found, turning off conda features.', ex); - sendTelemetryEvent(EventNames.MANAGER_REGISTRATION_SKIPPED, undefined, { - managerName: 'conda', - reason: 'tool_not_found', - }); - await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); - return; - } + traceInfo('Registering conda manager (environments will be discovered lazily)'); + const envManager = new CondaEnvManager(nativeFinder, api, log, projectManager); + const packageManager = new CondaPackageManager(api, log); - // Conda was found — errors below are real registration failures (let safeRegister handle telemetry) - try { - stage = 'constructCondaSourcingStatus'; - const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath); - traceInfo(sourcingStatus.toString()); - - stage = 'createEnvManager'; - const envManager = new CondaEnvManager(nativeFinder, api, log); - stage = 'createPkgManager'; - const packageManager = new CondaPackageManager(api, log); - - envManager.sourcingInformation = sourcingStatus; - - stage = 'registerManagers'; - disposables.push( - envManager, - packageManager, - api.registerEnvironmentManager(envManager), - api.registerPackageManager(packageManager), - ); - } catch (ex) { - await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); - const err = ex instanceof Error ? ex : new Error(String(ex)); - (err as Error & { failureStage?: string }).failureStage = stage; - throw err; - } + disposables.push( + envManager, + packageManager, + api.registerEnvironmentManager(envManager), + api.registerPackageManager(packageManager), + ); } From 9947c2707c5ab5026a95ff9ee20b79b7d5f92f4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:10:01 +0000 Subject: [PATCH 3/3] Add unit tests for conda lazy registration flow and failure handling --- .../condaEnvManager.initialize.unit.test.ts | 226 ++++++++++++++++++ .../conda/registerCondaFeatures.unit.test.ts | 104 ++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/test/managers/conda/condaEnvManager.initialize.unit.test.ts create mode 100644 src/test/managers/conda/registerCondaFeatures.unit.test.ts diff --git a/src/test/managers/conda/condaEnvManager.initialize.unit.test.ts b/src/test/managers/conda/condaEnvManager.initialize.unit.test.ts new file mode 100644 index 00000000..a061ef83 --- /dev/null +++ b/src/test/managers/conda/condaEnvManager.initialize.unit.test.ts @@ -0,0 +1,226 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { PythonEnvironmentApi } from '../../../api'; +import * as logging from '../../../common/logging'; +import { EventNames } from '../../../common/telemetry/constants'; +import * as telemetrySender from '../../../common/telemetry/sender'; +import * as windowApis from '../../../common/window.apis'; +import { PythonProjectManager } from '../../../internal.api'; +import * as commonUtils from '../../../managers/common/utils'; +import { NativePythonFinder } from '../../../managers/common/nativePythonFinder'; +import { CondaEnvManager } from '../../../managers/conda/condaEnvManager'; +import * as condaSourcingUtils from '../../../managers/conda/condaSourcingUtils'; +import * as condaUtils from '../../../managers/conda/condaUtils'; +import { makeMockCondaEnvironment as makeEnv } from '../../mocks/pythonEnvironment'; + +/** + * Tests for the lazy-registration flow on CondaEnvManager.initialize(). + * Covers: + * - success path (conda found locally / via settings / via PET) + * - tool_not_found path (no conda, notify missing-default) + * - error path (refresh throws) + * - telemetry emission for all three outcomes + * - sourcing information construction (and graceful handling of its failure) + * - idempotency of initialize() + */ +suite('CondaEnvManager.initialize - lazy registration flow', () => { + let getCondaStub: sinon.SinonStub; + let getCondaPathSettingStub: sinon.SinonStub; + let refreshCondaEnvsStub: sinon.SinonStub; + let constructSourcingStub: sinon.SinonStub; + let notifyMissingStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + + setup(() => { + getCondaStub = sinon.stub(condaUtils, 'getConda'); + getCondaPathSettingStub = sinon.stub(condaUtils, 'getCondaPathSetting').returns(undefined); + refreshCondaEnvsStub = sinon.stub(condaUtils, 'refreshCondaEnvs').resolves([]); + sinon.stub(condaUtils, 'getCondaForGlobal').resolves(undefined); + constructSourcingStub = sinon.stub(condaSourcingUtils, 'constructCondaSourcingStatus'); + notifyMissingStub = sinon.stub(commonUtils, 'notifyMissingManagerIfDefault').resolves(); + sendTelemetryStub = sinon.stub(telemetrySender, 'sendTelemetryEvent'); + withProgressStub = sinon.stub(windowApis, 'withProgress').callsFake(async (_options, task) => { + return await (task as any)({ report: sinon.stub() }, { isCancellationRequested: false } as any); + }); + sinon.stub(logging, 'traceInfo'); + sinon.stub(logging, 'traceError'); + }); + + teardown(() => { + sinon.restore(); + }); + + function createManager(opts?: { + projectManager?: PythonProjectManager; + api?: Partial; + }): CondaEnvManager { + const api = { + getPythonProjects: sinon.stub().returns([]), + getPythonProject: sinon.stub().returns(undefined), + ...opts?.api, + } as any as PythonEnvironmentApi; + return new CondaEnvManager( + {} as NativePythonFinder, + api, + { info: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } as any, + opts?.projectManager, + ); + } + + function getLazyInitTelemetry(): any | undefined { + const call = sendTelemetryStub + .getCalls() + .find((c) => c.args[0] === EventNames.MANAGER_LAZY_INIT); + return call?.args[2]; + } + + test('success path: conda found via local lookup → toolSource=local, registers sourcing info', async () => { + getCondaStub.resolves('/usr/bin/conda'); + const sourcing = { toString: () => 'sourcing' } as any; + constructSourcingStub.resolves(sourcing); + refreshCondaEnvsStub.resolves([makeEnv('base', '/opt/miniconda3', '3.11.0')]); + + const mgr = createManager(); + await mgr.initialize(); + + // refresh was invoked (i.e. the work was actually done) + assert.strictEqual(refreshCondaEnvsStub.callCount, 1, 'refreshCondaEnvs should be called once'); + // sourcing info is constructed using the resolved conda path + assert.strictEqual(constructSourcingStub.callCount, 1); + assert.strictEqual(constructSourcingStub.firstCall.args[0], '/usr/bin/conda'); + assert.strictEqual(mgr.sourcingInformation, sourcing); + // missing-default notification is NOT shown when conda was found + assert.strictEqual(notifyMissingStub.callCount, 0); + + const props = getLazyInitTelemetry(); + assert.deepStrictEqual( + { managerName: props.managerName, result: props.result, envCount: props.envCount, toolSource: props.toolSource }, + { managerName: 'conda', result: 'success', envCount: 1, toolSource: 'local' }, + ); + }); + + test('success path: conda from explicit setting → toolSource=settings', async () => { + getCondaPathSettingStub.returns('/opt/conda/bin/conda'); + getCondaStub.resolves('/opt/conda/bin/conda'); + constructSourcingStub.resolves({ toString: () => '' } as any); + + const mgr = createManager(); + await mgr.initialize(); + + const props = getLazyInitTelemetry(); + assert.strictEqual(props.toolSource, 'settings'); + assert.strictEqual(props.result, 'success'); + }); + + test('success path: conda discovered via PET after refresh → toolSource=pet', async () => { + // pre-refresh: getConda throws (not found locally) + getCondaStub.onFirstCall().rejects(new Error('Conda not found')); + // post-refresh: getConda resolves (PET persisted the path) + getCondaStub.onSecondCall().resolves('/home/user/miniconda3/bin/conda'); + constructSourcingStub.resolves({ toString: () => '' } as any); + + const mgr = createManager(); + await mgr.initialize(); + + // both pre- and post-refresh lookups happened + assert.strictEqual(getCondaStub.callCount, 2, 'getConda should be called twice (pre/post refresh)'); + assert.strictEqual(constructSourcingStub.callCount, 1); + + const props = getLazyInitTelemetry(); + assert.strictEqual(props.result, 'success'); + assert.strictEqual(props.toolSource, 'pet'); + assert.strictEqual(notifyMissingStub.callCount, 0); + }); + + test('tool_not_found: conda not found pre- or post-refresh → notifies and emits tool_not_found telemetry', async () => { + getCondaStub.rejects(new Error('Conda not found')); + const projectManager = {} as PythonProjectManager; + + const mgr = createManager({ projectManager }); + await mgr.initialize(); + + // refresh was still attempted (PET may have run, but didn't surface conda) + assert.strictEqual(refreshCondaEnvsStub.callCount, 1); + // sourcing info is NOT constructed when conda isn't found + assert.strictEqual(constructSourcingStub.callCount, 0); + assert.strictEqual(mgr.sourcingInformation, undefined); + // missing-default notification was shown + assert.strictEqual(notifyMissingStub.callCount, 1); + assert.strictEqual(notifyMissingStub.firstCall.args[0], 'ms-python.python:conda'); + assert.strictEqual(notifyMissingStub.firstCall.args[1], projectManager); + + const props = getLazyInitTelemetry(); + assert.strictEqual(props.result, 'tool_not_found'); + assert.strictEqual(props.toolSource, 'none'); + assert.strictEqual(props.envCount, 0); + }); + + test('tool_not_found without projectManager: skips missing-default notification', async () => { + getCondaStub.rejects(new Error('Conda not found')); + + const mgr = createManager(); // no projectManager + await mgr.initialize(); + + assert.strictEqual(notifyMissingStub.callCount, 0, 'no notify call when projectManager is absent'); + const props = getLazyInitTelemetry(); + assert.strictEqual(props.result, 'tool_not_found'); + }); + + test('error path: refreshCondaEnvs throws → result=error, errorType is classified, no throw to caller', async () => { + getCondaStub.resolves('/usr/bin/conda'); + refreshCondaEnvsStub.rejects(new Error('boom')); + + const mgr = createManager(); + await assert.doesNotReject(mgr.initialize(), 'initialize() must never throw to its caller'); + + const props = getLazyInitTelemetry(); + assert.strictEqual(props.result, 'error'); + assert.ok(props.errorType, 'errorType should be set on error path'); + // sourcing info not populated when refresh fails + assert.strictEqual(mgr.sourcingInformation, undefined); + }); + + test('error path: sourcing status failure is swallowed and does not flip result to error', async () => { + getCondaStub.resolves('/usr/bin/conda'); + constructSourcingStub.rejects(new Error('sourcing-failed')); + + const mgr = createManager(); + await mgr.initialize(); + + const props = getLazyInitTelemetry(); + // refresh succeeded, so overall result is still success + assert.strictEqual(props.result, 'success'); + // but sourcingInformation is not set + assert.strictEqual(mgr.sourcingInformation, undefined); + }); + + test('idempotency: concurrent initialize() calls share a single run', async () => { + getCondaStub.resolves('/usr/bin/conda'); + constructSourcingStub.resolves({ toString: () => '' } as any); + + const mgr = createManager(); + await Promise.all([mgr.initialize(), mgr.initialize(), mgr.initialize()]); + + assert.strictEqual(refreshCondaEnvsStub.callCount, 1, 'refresh should run exactly once'); + // telemetry should fire exactly once across concurrent + sequential calls + const lazyInitCalls = sendTelemetryStub + .getCalls() + .filter((c) => c.args[0] === EventNames.MANAGER_LAZY_INIT); + assert.strictEqual(lazyInitCalls.length, 1); + + // a subsequent call after completion is also a no-op + await mgr.initialize(); + assert.strictEqual(refreshCondaEnvsStub.callCount, 1); + }); + + test('no PET refresh is triggered before initialize(): construction alone does no work', () => { + // Simply constructing the manager must not call into discovery. + createManager(); + assert.strictEqual(getCondaStub.callCount, 0); + assert.strictEqual(refreshCondaEnvsStub.callCount, 0); + assert.strictEqual(constructSourcingStub.callCount, 0); + assert.strictEqual(withProgressStub.callCount, 0); + }); +}); diff --git a/src/test/managers/conda/registerCondaFeatures.unit.test.ts b/src/test/managers/conda/registerCondaFeatures.unit.test.ts new file mode 100644 index 00000000..acbc4ca5 --- /dev/null +++ b/src/test/managers/conda/registerCondaFeatures.unit.test.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { Disposable, LogOutputChannel } from 'vscode'; +import { PythonEnvironmentApi } from '../../../api'; +import * as logging from '../../../common/logging'; +import * as pythonApi from '../../../features/pythonApi'; +import { PythonProjectManager } from '../../../internal.api'; +import { NativePythonFinder } from '../../../managers/common/nativePythonFinder'; +import * as condaSourcingUtils from '../../../managers/conda/condaSourcingUtils'; +import * as condaUtils from '../../../managers/conda/condaUtils'; +import { registerCondaFeatures } from '../../../managers/conda/main'; + +/** + * Tests for the unconditional, lazy registration entrypoint. + * + * The lazy-registration contract is: + * 1. Conda manager is ALWAYS registered (so it shows up in the picker), regardless of + * whether the conda CLI exists on the machine. + * 2. registerCondaFeatures does NO PET / sourcing-status work at activation time — + * that cost is deferred to CondaEnvManager.initialize() on first use. + * 3. Both the env manager and the package manager are pushed onto disposables and + * registered with the api. + */ +suite('registerCondaFeatures - unconditional lazy registration', () => { + let getCondaStub: sinon.SinonStub; + let getCondaPathSettingStub: sinon.SinonStub; + let refreshCondaEnvsStub: sinon.SinonStub; + let constructSourcingStub: sinon.SinonStub; + let getPythonApiStub: sinon.SinonStub; + let registerEnvManagerStub: sinon.SinonStub; + let registerPackageManagerStub: sinon.SinonStub; + + setup(() => { + // Stubs on every discovery side-effect: if any of these fire, the test fails + // because activation is no longer lazy. + getCondaStub = sinon.stub(condaUtils, 'getConda'); + getCondaPathSettingStub = sinon.stub(condaUtils, 'getCondaPathSetting').returns(undefined); + refreshCondaEnvsStub = sinon.stub(condaUtils, 'refreshCondaEnvs').resolves([]); + constructSourcingStub = sinon.stub(condaSourcingUtils, 'constructCondaSourcingStatus'); + + registerEnvManagerStub = sinon.stub().returns({ dispose: sinon.stub() }); + registerPackageManagerStub = sinon.stub().returns({ dispose: sinon.stub() }); + const api = { + registerEnvironmentManager: registerEnvManagerStub, + registerPackageManager: registerPackageManagerStub, + } as any as PythonEnvironmentApi; + getPythonApiStub = sinon.stub(pythonApi, 'getPythonApi').resolves(api); + + sinon.stub(logging, 'traceInfo'); + sinon.stub(logging, 'traceError'); + }); + + teardown(() => { + sinon.restore(); + }); + + const log = { info: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } as any as LogOutputChannel; + const nativeFinder = {} as NativePythonFinder; + const projectManager = {} as PythonProjectManager; + + test('registers env manager and package manager unconditionally', async () => { + const disposables: Disposable[] = []; + + await registerCondaFeatures(nativeFinder, disposables, log, projectManager); + + assert.strictEqual(getPythonApiStub.callCount, 1, 'should fetch the python api'); + assert.strictEqual(registerEnvManagerStub.callCount, 1, 'env manager must be registered'); + assert.strictEqual(registerPackageManagerStub.callCount, 1, 'package manager must be registered'); + // env manager + package manager + their two registration disposables + assert.strictEqual(disposables.length, 4, 'four disposables expected'); + }); + + test('does NOT call getConda / refreshCondaEnvs / constructCondaSourcingStatus at registration', async () => { + await registerCondaFeatures(nativeFinder, [], log, projectManager); + + // These are the hot-path / PET-triggering calls that the previous (non-lazy) + // implementation made during activation. Their absence is the whole point of + // the lazy-registration change. + assert.strictEqual(getCondaStub.callCount, 0, 'getConda must not be invoked at registration'); + assert.strictEqual(refreshCondaEnvsStub.callCount, 0, 'refreshCondaEnvs must not be invoked at registration'); + assert.strictEqual( + constructSourcingStub.callCount, + 0, + 'constructCondaSourcingStatus must not be invoked at registration', + ); + // We also don't inspect the conda path setting at registration time. + assert.strictEqual(getCondaPathSettingStub.callCount, 0); + }); + + test('registers even when conda would not be found (no early-return on missing tool)', async () => { + // Even if getConda would throw, registration must still succeed. We don't actually + // call it during registerCondaFeatures, but we configure the stub to reject so a + // regression that re-introduces the call would also fail this test. + getCondaStub.rejects(new Error('Conda not found')); + const disposables: Disposable[] = []; + + await registerCondaFeatures(nativeFinder, disposables, log, projectManager); + + assert.strictEqual(registerEnvManagerStub.callCount, 1); + assert.strictEqual(registerPackageManagerStub.callCount, 1); + assert.strictEqual(disposables.length, 4); + }); +});